Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,23 @@

> Lightweight standard for organizing Markdown documentation in codebases

SimpleDoc defines a small set of rules for the naming and placement of Markdown files in a codebase, agnostic of any documentation framework:
SimpleDoc defines a small set of rules for the naming and placement of Markdown files in a codebase, agnostic of any documentation framework.

## Install (Agent Instructions)

Install the bundled agent skill + `AGENTS.md` instructions (no doc migrations):

```bash
npx -y @simpledoc/simpledoc install
```

If you prefer to install the skill bundle directly into a repo-scoped Codex skill store:

```bash
npx -y @simpledoc/simpledoc --skill export simpledoc | skill-install --agent codex --scope repo
```

(`skill-install` is provided by the `skillflag` package.)

## Specification

Expand Down Expand Up @@ -34,9 +50,9 @@ SimpleDoc defines two types of files:
- Capitalized files SHOULD be used for general documents that are not tied to a specific time, e.g. `README.md`, `AGENTS.md`, `INSTALL.md`, `HOW_TO_DEBUG.md`.
- If a capitalized filename has multiple words, it SHOULD use underscores (`CODE_OF_CONDUCT.md`). Dashes are common in the wild but not preferred by this spec.

## Installation
## Migration

Run the migrator from the repo root:
Run the migrator from the repo root to rename/move docs and add frontmatter as needed:

```bash
npx -y @simpledoc/simpledoc migrate
Expand Down
16 changes: 15 additions & 1 deletion src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import process from "node:process";
import { Command, CommanderError } from "commander";

import { runCheck } from "./check.js";
import { runInstall } from "./install.js";
import { runMigrate } from "./migrate.js";

type MigrateOptions = {
Expand All @@ -10,6 +11,10 @@ type MigrateOptions = {
force: boolean;
author?: string;
};
type InstallOptions = {
dryRun: boolean;
yes: boolean;
};

function getErrorMessage(err: unknown): string {
if (err instanceof Error) return err.message;
Expand Down Expand Up @@ -41,7 +46,7 @@ export async function runCli(argv: string[]): Promise<void> {
program
.command("migrate")
.description(
"Install SimpleDoc agent docs and migrate a repo's docs to SimpleDoc conventions.",
"Install SimpleDoc agent instructions and migrate a repo's docs to SimpleDoc conventions.",
)
.option("--dry-run", "Print planned changes and exit", false)
.option("-y, --yes", "Apply defaults without prompts", false)
Expand All @@ -54,6 +59,15 @@ export async function runCli(argv: string[]): Promise<void> {
await runMigrate(options);
});

program
.command("install")
.description("Install SimpleDoc agent instructions (no doc migrations).")
.option("--dry-run", "Print planned changes and exit", false)
.option("-y, --yes", "Apply defaults without prompts", false)
.action(async (options: InstallOptions) => {
await runInstall(options);
});

program
.command("check")
.description("Fail if the repo violates SimpleDoc conventions (use in CI).")
Expand Down
109 changes: 109 additions & 0 deletions src/cli/install.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import process from "node:process";
import { cancel, intro, outro, spinner } from "@clack/prompts";

import {
applyInstallationActions,
buildInstallationActions,
formatInstallActions,
getInstallationStatus,
} from "../installer.js";
import { createGitClient } from "../git.js";
import type { InstallAction } from "../installer.js";
import { noteWrapped, promptConfirm } from "./ui.js";
import { runInstallSteps } from "./steps/install.js";

type InstallOptions = {
dryRun: boolean;
yes: boolean;
};

function abort(message = "Aborted."): void {
cancel(message);
process.exitCode = 1;
}

function getErrorMessage(err: unknown): string {
if (err instanceof Error) return err.message;
return String(err);
}

function printPreview(actions: InstallAction[]): void {
process.stdout.write("Planned changes:\n");
const preview = formatInstallActions(actions).trim();
if (preview) process.stdout.write(`\n${preview}\n`);
}

export async function runInstall(options: InstallOptions): Promise<void> {
try {
const git = createGitClient();
const repoRootAbs = await git.getRepoRoot(process.cwd());
const installStatus = await getInstallationStatus(repoRootAbs);

const installActionsAll = await buildInstallationActions({
createAgentsFile: !installStatus.agentsExists,
addAttentionLine:
installStatus.agentsExists && !installStatus.agentsHasAttentionLine,
addSkill: !installStatus.skillExists,
});

if (installActionsAll.length === 0) {
process.stdout.write("No installation needed.\n");
return;
}

const hasTty = Boolean(process.stdin.isTTY && process.stdout.isTTY);

if (options.dryRun) {
printPreview(installActionsAll);
return;
}

if (!hasTty && !options.yes) {
printPreview(installActionsAll);
process.stderr.write(
"\nRefusing to apply changes without a TTY. Re-run with --yes.\n",
);
process.exitCode = 2;
return;
}

if (options.yes) {
printPreview(installActionsAll);
process.stderr.write("Applying changes...\n");
await applyInstallationActions(repoRootAbs, installActionsAll);
process.stdout.write(
"Done. Review with `git status` / `git diff` and commit when ready.\n",
);
return;
}

intro("simpledoc install");

const installSel = await runInstallSteps(installStatus);
if (installSel === null) return abort("Operation cancelled.");

const selectedInstallActions = await buildInstallationActions(installSel);
if (selectedInstallActions.length === 0) {
outro("Nothing selected.");
return;
}

noteWrapped(
formatInstallActions(selectedInstallActions),
"Summary of selected changes",
);

const apply = await promptConfirm("Apply these changes now?", true);
if (apply === null) return abort("Operation cancelled.");
if (!apply) return abort();

const s = spinner();
s.start("Applying changes...");
await applyInstallationActions(repoRootAbs, selectedInstallActions);
s.stop("Done.");
outro("Review with `git status` / `git diff` and commit when ready.");
} catch (err) {
process.stderr.write(`${getErrorMessage(err)}\n`);
process.exitCode = 1;
}
}