Skip to content

Commit ca8fda6

Browse files
authored
Refactor install flow (#11)
* Refactor install flow and add CLI test * Bump skillflag to 0.1.1 * Use npx skillflag install in README * Simplify install heading * Remove skillflag install note * Reorder install section and rename migrate * Remove agent skill bundle section * Prompt to migrate when install detects violations * Add installer tests * Always prompt to add bundled skill
1 parent 92f874e commit ca8fda6

13 files changed

Lines changed: 317 additions & 122 deletions

File tree

README.md

Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,6 @@
44
55
SimpleDoc defines a small set of rules for the naming and placement of Markdown files in a codebase, agnostic of any documentation framework.
66

7-
## Install (Agent Instructions)
8-
9-
Install the bundled agent skill + `AGENTS.md` instructions (no doc migrations):
10-
11-
```bash
12-
npx -y @simpledoc/simpledoc install
13-
```
14-
15-
If you prefer to install the skill bundle directly into a repo-scoped Codex skill store:
16-
17-
```bash
18-
npx -y @simpledoc/simpledoc --skill export simpledoc | skill-install --agent codex --scope repo
19-
```
20-
21-
(`skill-install` is provided by the `skillflag` package.)
22-
237
## Specification
248

259
SimpleDoc defines two types of files:
@@ -50,25 +34,23 @@ SimpleDoc defines two types of files:
5034
- 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`.
5135
- 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.
5236

53-
## Migration
37+
## Install
5438

55-
Run the migrator from the repo root to rename/move docs and add frontmatter as needed:
39+
Install the bundled agent skill + `AGENTS.md` instructions (no doc migrations):
5640

5741
```bash
58-
npx -y @simpledoc/simpledoc migrate
42+
npx -y @simpledoc/simpledoc install
5943
```
6044

61-
This will start a step-by-step wizard to migrate existing documentation to SimpleDoc and add instructions to `AGENTS.md` to follow it.
62-
63-
### Agent skill bundle
45+
## Migrate
6446

65-
SimpleDoc ships a bundled `simpledoc` skill for agent instructions. To install it into a repo-scoped Codex skill store:
47+
Run the migrator from the repo root to rename/move docs and add frontmatter as needed:
6648

6749
```bash
68-
npx -y @simpledoc/simpledoc --skill export simpledoc | skill-install --agent codex --scope repo
50+
npx -y @simpledoc/simpledoc migrate
6951
```
7052

71-
(`skill-install` is provided by the `skillflag` package.)
53+
This will start a step-by-step wizard to migrate existing documentation to SimpleDoc and add instructions to `AGENTS.md` to follow it.
7254

7355
## CI / Enforcement
7456

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
"dependencies": {
5151
"@clack/prompts": "^0.11.0",
5252
"commander": "^11.0.0",
53-
"skillflag": "^0.1.0"
53+
"skillflag": "^0.1.1"
5454
},
5555
"devDependencies": {
5656
"@eslint/js": "^9.0.0",

src/bin/simpledoc.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env node
22
import process from "node:process";
3-
import { handleSkillflag } from "skillflag/dist/index.js";
3+
import { handleSkillflag } from "../cli/skillflag.js";
44

55
import { runCli } from "../cli/index.js";
66

src/cli/flow.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import process from "node:process";
2+
import { cancel } from "@clack/prompts";
3+
4+
export function abort(message = "Aborted."): void {
5+
cancel(message);
6+
process.exitCode = 1;
7+
}
8+
9+
export function getErrorMessage(err: unknown): string {
10+
if (err instanceof Error) return err.message;
11+
return String(err);
12+
}
13+
14+
export function hasInteractiveTty(): boolean {
15+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
16+
}

src/cli/install-helpers.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { InstallationStatus, InstallAction } from "../installer.js";
2+
import { buildInstallationActions } from "../installer.js";
3+
4+
export type InstallSelection = {
5+
createAgentsFile: boolean;
6+
addAttentionLine: boolean;
7+
addSkill: boolean;
8+
};
9+
10+
export function buildDefaultInstallSelection(
11+
status: InstallationStatus,
12+
): InstallSelection {
13+
return {
14+
createAgentsFile: !status.agentsExists,
15+
addAttentionLine: status.agentsExists && !status.agentsHasAttentionLine,
16+
addSkill: !status.skillExists,
17+
};
18+
}
19+
20+
export async function buildDefaultInstallActions(
21+
status: InstallationStatus,
22+
): Promise<InstallAction[]> {
23+
return buildInstallationActions(buildDefaultInstallSelection(status));
24+
}

src/cli/install.ts

Lines changed: 154 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import process from "node:process";
2-
import { cancel, intro, outro, spinner } from "@clack/prompts";
2+
import { intro, outro, spinner } from "@clack/prompts";
33

44
import {
55
applyInstallationActions,
@@ -9,57 +9,130 @@ import {
99
} from "../installer.js";
1010
import { createGitClient } from "../git.js";
1111
import type { InstallAction } from "../installer.js";
12-
import { noteWrapped, promptConfirm } from "./ui.js";
12+
import {
13+
formatActions,
14+
planMigration,
15+
type FrontmatterAction,
16+
type ReferenceUpdateAction,
17+
type RenameAction,
18+
} from "../migrator.js";
19+
import { buildDefaultInstallActions } from "./install-helpers.js";
20+
import { abort, getErrorMessage, hasInteractiveTty } from "./flow.js";
21+
import {
22+
MAX_STEP_FILE_PREVIEW_LINES,
23+
createScanProgressBarReporter,
24+
limitLines,
25+
noteWrapped,
26+
promptConfirm,
27+
} from "./ui.js";
28+
import { runMigrate } from "./migrate.js";
1329
import { runInstallSteps } from "./steps/install.js";
1430

1531
type InstallOptions = {
1632
dryRun: boolean;
1733
yes: boolean;
1834
};
1935

20-
function abort(message = "Aborted."): void {
21-
cancel(message);
22-
process.exitCode = 1;
23-
}
24-
25-
function getErrorMessage(err: unknown): string {
26-
if (err instanceof Error) return err.message;
27-
return String(err);
28-
}
29-
3036
function printPreview(actions: InstallAction[]): void {
3137
process.stdout.write("Planned changes:\n");
3238
const preview = formatInstallActions(actions).trim();
3339
if (preview) process.stdout.write(`\n${preview}\n`);
3440
}
3541

42+
type MigrationInfo = {
43+
renames: RenameAction[];
44+
frontmatters: FrontmatterAction[];
45+
references: ReferenceUpdateAction[];
46+
summaryLines: string[];
47+
preview: string;
48+
hasIssues: boolean;
49+
};
50+
51+
function buildMigrationInfo(plan: {
52+
actions: Array<RenameAction | FrontmatterAction | ReferenceUpdateAction>;
53+
}): MigrationInfo {
54+
const renames = plan.actions.filter(
55+
(a): a is RenameAction => a.type === "rename",
56+
);
57+
const frontmatters = plan.actions.filter(
58+
(a): a is FrontmatterAction => a.type === "frontmatter",
59+
);
60+
const references = plan.actions.filter(
61+
(a): a is ReferenceUpdateAction => a.type === "references",
62+
);
63+
64+
const summaryLines: string[] = [];
65+
if (renames.length > 0)
66+
summaryLines.push(
67+
`- Rename/move Markdown files: ${renames.length} file${renames.length === 1 ? "" : "s"}`,
68+
);
69+
if (frontmatters.length > 0)
70+
summaryLines.push(
71+
`- Insert YAML frontmatter: ${frontmatters.length} file${frontmatters.length === 1 ? "" : "s"}`,
72+
);
73+
if (references.length > 0)
74+
summaryLines.push(
75+
`- Update references to renamed docs: ${references.length} file${references.length === 1 ? "" : "s"}`,
76+
);
77+
78+
const previewLines: string[] = [];
79+
if (renames.length > 0) previewLines.push(formatActions(renames));
80+
if (frontmatters.length > 0) previewLines.push(formatActions(frontmatters));
81+
if (references.length > 0) previewLines.push(formatActions(references));
82+
const preview = previewLines.filter(Boolean).join("\n");
83+
84+
return {
85+
renames,
86+
frontmatters,
87+
references,
88+
summaryLines,
89+
preview,
90+
hasIssues: renames.length + frontmatters.length + references.length > 0,
91+
};
92+
}
93+
94+
function printMigrationSummary(info: MigrationInfo, includePreview: boolean) {
95+
if (!info.hasIssues) return;
96+
process.stdout.write("SimpleDoc check failed.\n\n");
97+
if (info.summaryLines.length > 0) {
98+
process.stdout.write(`${info.summaryLines.join("\n")}\n\n`);
99+
}
100+
if (includePreview && info.preview) {
101+
const limited = limitLines(info.preview, MAX_STEP_FILE_PREVIEW_LINES);
102+
process.stdout.write(`${limited}\n\n`);
103+
}
104+
}
105+
36106
export async function runInstall(options: InstallOptions): Promise<void> {
37107
try {
38108
const git = createGitClient();
39109
const repoRootAbs = await git.getRepoRoot(process.cwd());
40110
const installStatus = await getInstallationStatus(repoRootAbs);
41111

42-
const installActionsAll = await buildInstallationActions({
43-
createAgentsFile: !installStatus.agentsExists,
44-
addAttentionLine:
45-
installStatus.agentsExists && !installStatus.agentsHasAttentionLine,
46-
addSkill: !installStatus.skillExists,
112+
const hasTty = hasInteractiveTty();
113+
const scanProgress = createScanProgressBarReporter(hasTty);
114+
const migrationPlan = await planMigration({
115+
cwd: repoRootAbs,
116+
onProgress: scanProgress,
47117
});
118+
const migrationInfo = buildMigrationInfo(migrationPlan);
48119

49-
if (installActionsAll.length === 0) {
120+
const installActionsAll = await buildDefaultInstallActions(installStatus);
121+
122+
if (installActionsAll.length === 0 && !migrationInfo.hasIssues) {
50123
process.stdout.write("No installation needed.\n");
51124
return;
52125
}
53126

54-
const hasTty = Boolean(process.stdin.isTTY && process.stdout.isTTY);
55-
56127
if (options.dryRun) {
57-
printPreview(installActionsAll);
128+
if (installActionsAll.length > 0) printPreview(installActionsAll);
129+
if (migrationInfo.hasIssues) printMigrationSummary(migrationInfo, true);
58130
return;
59131
}
60132

61133
if (!hasTty && !options.yes) {
62-
printPreview(installActionsAll);
134+
if (installActionsAll.length > 0) printPreview(installActionsAll);
135+
if (migrationInfo.hasIssues) printMigrationSummary(migrationInfo, true);
63136
process.stderr.write(
64137
"\nRefusing to apply changes without a TTY. Re-run with --yes.\n",
65138
);
@@ -68,39 +141,73 @@ export async function runInstall(options: InstallOptions): Promise<void> {
68141
}
69142

70143
if (options.yes) {
71-
printPreview(installActionsAll);
72-
process.stderr.write("Applying changes...\n");
73-
await applyInstallationActions(repoRootAbs, installActionsAll);
74-
process.stdout.write(
75-
"Done. Review with `git status` / `git diff` and commit when ready.\n",
76-
);
144+
if (installActionsAll.length > 0) {
145+
printPreview(installActionsAll);
146+
process.stderr.write("Applying changes...\n");
147+
await applyInstallationActions(repoRootAbs, installActionsAll);
148+
}
149+
if (migrationInfo.hasIssues) {
150+
printMigrationSummary(migrationInfo, false);
151+
process.stdout.write("Run `simpledoc migrate` to fix.\n");
152+
} else {
153+
process.stdout.write(
154+
"Done. Review with `git status` / `git diff` and commit when ready.\n",
155+
);
156+
}
77157
return;
78158
}
79159

80160
intro("simpledoc install");
81161

82-
const installSel = await runInstallSteps(installStatus);
83-
if (installSel === null) return abort("Operation cancelled.");
84-
85-
const selectedInstallActions = await buildInstallationActions(installSel);
86-
if (selectedInstallActions.length === 0) {
87-
outro("Nothing selected.");
88-
return;
162+
if (installActionsAll.length > 0) {
163+
const installSel = await runInstallSteps(installStatus);
164+
if (installSel === null) return abort("Operation cancelled.");
165+
166+
const selectedInstallActions = await buildInstallationActions(installSel);
167+
if (selectedInstallActions.length > 0) {
168+
noteWrapped(
169+
formatInstallActions(selectedInstallActions),
170+
"Summary of selected changes",
171+
);
172+
173+
const apply = await promptConfirm("Apply these changes now?", true);
174+
if (apply === null) return abort("Operation cancelled.");
175+
if (!apply) return abort();
176+
177+
const s = spinner();
178+
s.start("Applying changes...");
179+
await applyInstallationActions(repoRootAbs, selectedInstallActions);
180+
s.stop("Done.");
181+
}
89182
}
90183

91-
noteWrapped(
92-
formatInstallActions(selectedInstallActions),
93-
"Summary of selected changes",
94-
);
95-
96-
const apply = await promptConfirm("Apply these changes now?", true);
97-
if (apply === null) return abort("Operation cancelled.");
98-
if (!apply) return abort();
184+
if (migrationInfo.hasIssues) {
185+
noteWrapped(
186+
migrationInfo.summaryLines.join("\n"),
187+
"SimpleDoc check failed",
188+
);
189+
if (migrationInfo.preview) {
190+
const limited = limitLines(
191+
migrationInfo.preview,
192+
MAX_STEP_FILE_PREVIEW_LINES,
193+
);
194+
process.stdout.write(`${limited}\n\n`);
195+
}
196+
const migrateNow = await promptConfirm(
197+
"Run `simpledoc migrate` now?",
198+
true,
199+
);
200+
if (migrateNow === null) return abort("Operation cancelled.");
201+
if (migrateNow) {
202+
await runMigrate({
203+
dryRun: false,
204+
yes: false,
205+
force: false,
206+
});
207+
return;
208+
}
209+
}
99210

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

0 commit comments

Comments
 (0)