alignfirst 0.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +31 -0
  2. package/bin/alignfirst.mjs +3 -0
  3. package/dist/cli-error.d.ts +2 -0
  4. package/dist/cli-error.js +2 -0
  5. package/dist/cli.d.ts +10 -0
  6. package/dist/cli.js +98 -0
  7. package/dist/command-form.d.ts +3 -0
  8. package/dist/command-form.js +8 -0
  9. package/dist/commands/config.d.ts +2 -0
  10. package/dist/commands/config.js +55 -0
  11. package/dist/commands/developers.d.ts +2 -0
  12. package/dist/commands/developers.js +36 -0
  13. package/dist/commands/docmap.d.ts +2 -0
  14. package/dist/commands/docmap.js +22 -0
  15. package/dist/commands/doctor.d.ts +2 -0
  16. package/dist/commands/doctor.js +167 -0
  17. package/dist/commands/guide.d.ts +2 -0
  18. package/dist/commands/guide.js +126 -0
  19. package/dist/commands/plans.d.ts +2 -0
  20. package/dist/commands/plans.js +154 -0
  21. package/dist/commands/setup.d.ts +2 -0
  22. package/dist/commands/setup.js +254 -0
  23. package/dist/commands/sync.d.ts +2 -0
  24. package/dist/commands/sync.js +63 -0
  25. package/dist/commands/ticket.d.ts +2 -0
  26. package/dist/commands/ticket.js +119 -0
  27. package/dist/context.d.ts +15 -0
  28. package/dist/context.js +1 -0
  29. package/dist/errors.d.ts +2 -0
  30. package/dist/errors.js +6 -0
  31. package/dist/executables.d.ts +1 -0
  32. package/dist/executables.js +24 -0
  33. package/dist/git.d.ts +5 -0
  34. package/dist/git.js +52 -0
  35. package/dist/overlay.d.ts +19 -0
  36. package/dist/overlay.js +83 -0
  37. package/dist/parse-args.d.ts +1 -0
  38. package/dist/parse-args.js +11 -0
  39. package/dist/plans/archive.d.ts +4 -0
  40. package/dist/plans/archive.js +68 -0
  41. package/dist/plans/layout.d.ts +6 -0
  42. package/dist/plans/layout.js +19 -0
  43. package/dist/plans/link.d.ts +2 -0
  44. package/dist/plans/link.js +36 -0
  45. package/dist/plans/mode.d.ts +9 -0
  46. package/dist/plans/mode.js +31 -0
  47. package/dist/plans/ticket.d.ts +21 -0
  48. package/dist/plans/ticket.js +104 -0
  49. package/dist/project-config.d.ts +27 -0
  50. package/dist/project-config.js +79 -0
  51. package/dist/protocols.d.ts +2 -0
  52. package/dist/protocols.js +9 -0
  53. package/dist/skills.d.ts +9 -0
  54. package/dist/skills.js +54 -0
  55. package/dist/version-guard.d.ts +8 -0
  56. package/dist/version-guard.js +24 -0
  57. package/package.json +48 -0
  58. package/templates/guide/code-review/correctness-reviewer.md +53 -0
  59. package/templates/guide/code-review/intent-reviewer.md +22 -0
  60. package/templates/guide/code-review/module-javascript.md +80 -0
  61. package/templates/guide/code-review/module-python.md +77 -0
  62. package/templates/guide/code-review/module-typescript-strict.md +84 -0
  63. package/templates/guide/code-review/quality-reviewer.md +54 -0
  64. package/templates/guide/code-review/reviewer-common.md +62 -0
  65. package/templates/guide/code-review/safety-reviewer.md +66 -0
  66. package/templates/guide/core.md +54 -0
  67. package/templates/guide/overview.md +57 -0
  68. package/templates/guide/protocols/aad.md +85 -0
  69. package/templates/guide/protocols/catchup.md +13 -0
  70. package/templates/guide/protocols/description.md +51 -0
  71. package/templates/guide/protocols/merge.md +65 -0
  72. package/templates/guide/protocols/plan.md +256 -0
  73. package/templates/guide/protocols/review.md +114 -0
  74. package/templates/guide/protocols/spec.md +78 -0
package/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # alignfirst
2
+
3
+ The AlignFirst CLI provides protocols, shared plans and project documentation in one command.
4
+
5
+ Install it globally:
6
+
7
+ ```sh
8
+ npm install -g alignfirst
9
+ ```
10
+
11
+ Or run the current version without installing it:
12
+
13
+ ```sh
14
+ npx -y alignfirst
15
+ ```
16
+
17
+ Commands:
18
+
19
+ - `guide` — Print an AlignFirst protocol.
20
+ - `ticket` — Resolve a ticket directory and its next file.
21
+ - `sync` — Synchronize shared plans.
22
+ - `plans` — Set up, check and archive plans.
23
+ - `docmap` — Browse project documentation.
24
+ - `config` — Report the effective project configuration.
25
+ - `DEVELOPERS.md` — Print the project developer guide.
26
+ - `setup` — Prepare an AlignFirst project.
27
+ - `doctor` — Diagnose an AlignFirst setup.
28
+
29
+ Run `alignfirst --help` for command usage or `alignfirst guide` for the collaboration guide.
30
+
31
+ `@paleo/alcode` is the companion CLI for the AlignFirst Developer.
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "../dist/cli.js";
3
+ process.exit(await main());
@@ -0,0 +1,2 @@
1
+ export declare class CliError extends Error {
2
+ }
@@ -0,0 +1,2 @@
1
+ export class CliError extends Error {
2
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { Output } from "./context.js";
2
+ export interface MainOptions {
3
+ argv?: string[];
4
+ cwd?: string;
5
+ env?: NodeJS.ProcessEnv;
6
+ home?: string;
7
+ stdout?: Output;
8
+ stderr?: Output;
9
+ }
10
+ export declare function main(options?: MainOptions): Promise<number>;
package/dist/cli.js ADDED
@@ -0,0 +1,98 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { CliError } from "./cli-error.js";
4
+ import { resolveCommandForm } from "./command-form.js";
5
+ import { runConfig } from "./commands/config.js";
6
+ import { runDevelopers } from "./commands/developers.js";
7
+ import { runDoctor } from "./commands/doctor.js";
8
+ import { runDocmap } from "./commands/docmap.js";
9
+ import { runGuide } from "./commands/guide.js";
10
+ import { runPlans } from "./commands/plans.js";
11
+ import { runSetup } from "./commands/setup.js";
12
+ import { runSync } from "./commands/sync.js";
13
+ import { runTicket } from "./commands/ticket.js";
14
+ import { resolveProjectConfig } from "./overlay.js";
15
+ import { checkCliRange } from "./version-guard.js";
16
+ export async function main(options) {
17
+ const argv = options?.argv ?? process.argv;
18
+ const env = options?.env ?? process.env;
19
+ const ctx = {
20
+ cwd: options?.cwd ?? process.cwd(),
21
+ env,
22
+ home: options?.home ?? env.HOME ?? env.USERPROFILE ?? homedir(),
23
+ stdout: options?.stdout ?? process.stdout,
24
+ stderr: options?.stderr ?? process.stderr,
25
+ form: resolveCommandForm(env),
26
+ version: readPackageVersion(),
27
+ };
28
+ const [command, ...args] = argv.slice(2);
29
+ try {
30
+ if (command === "--version" || command === "-v") {
31
+ ctx.stdout.write(`${ctx.version}\n`);
32
+ return 0;
33
+ }
34
+ if (command === undefined || command === "--help" || command === "-h") {
35
+ ctx.stdout.write(renderHelp(ctx));
36
+ return 0;
37
+ }
38
+ if (command !== "config" && command !== "doctor") {
39
+ ctx.projectConfig = resolveProjectConfig(ctx.cwd, ctx.env, ctx.home);
40
+ ctx.overlay = ctx.projectConfig?.overlay;
41
+ checkCliRange(ctx.projectConfig?.config, ctx.version, [command, ...args]);
42
+ }
43
+ return dispatch(ctx, command, args);
44
+ }
45
+ catch (error) {
46
+ if (!(error instanceof CliError))
47
+ throw error;
48
+ ctx.stderr.write(`${error.message}\n`);
49
+ return 1;
50
+ }
51
+ }
52
+ function readPackageVersion() {
53
+ const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
54
+ if (pkg.version === undefined)
55
+ throw new Error("alignfirst: package.json is missing 'version'");
56
+ return pkg.version;
57
+ }
58
+ function renderHelp(ctx) {
59
+ return `alignfirst — protocols, plans and docs in one command.
60
+
61
+ Usage:
62
+ ${ctx.form} guide [<protocol>]
63
+ ${ctx.form} ticket [<id>]
64
+ ${ctx.form} sync [--auto-archive]
65
+ ${ctx.form} plans <command>
66
+ ${ctx.form} docmap [<arguments>]
67
+ ${ctx.form} config [--json]
68
+ ${ctx.form} DEVELOPERS.md
69
+ ${ctx.form} setup [<options>]
70
+ ${ctx.form} doctor
71
+ ${ctx.form} --help
72
+ ${ctx.form} --version
73
+ `;
74
+ }
75
+ function dispatch(ctx, command, args) {
76
+ switch (command) {
77
+ case "guide":
78
+ return runGuide(ctx, args);
79
+ case "ticket":
80
+ return runTicket(ctx, args);
81
+ case "sync":
82
+ return runSync(ctx, args);
83
+ case "plans":
84
+ return runPlans(ctx, args);
85
+ case "docmap":
86
+ return runDocmap(ctx, args);
87
+ case "config":
88
+ return runConfig(ctx, args);
89
+ case "DEVELOPERS.md":
90
+ return runDevelopers(ctx, args);
91
+ case "setup":
92
+ return runSetup(ctx, args);
93
+ case "doctor":
94
+ return runDoctor(ctx, args);
95
+ default:
96
+ throw new CliError(`Error: unknown command "${command}".\n\n${renderHelp(ctx)}`);
97
+ }
98
+ }
@@ -0,0 +1,3 @@
1
+ export declare const CMD_PLACEHOLDER = "{{CMD}}";
2
+ export declare function resolveCommandForm(env: NodeJS.ProcessEnv): string;
3
+ export declare function renderCommandForm(text: string, form: string): string;
@@ -0,0 +1,8 @@
1
+ export const CMD_PLACEHOLDER = "{{CMD}}";
2
+ export function resolveCommandForm(env) {
3
+ const userAgent = env.npm_config_user_agent;
4
+ return userAgent === undefined || userAgent === "" ? "alignfirst" : "npx -y alignfirst";
5
+ }
6
+ export function renderCommandForm(text, form) {
7
+ return text.replaceAll(CMD_PLACEHOLDER, form);
8
+ }
@@ -0,0 +1,2 @@
1
+ import type { CommandContext } from "../context.js";
2
+ export declare function runConfig(ctx: CommandContext, args: string[]): number;
@@ -0,0 +1,55 @@
1
+ import { CliError } from "../cli-error.js";
2
+ import { parseArgs } from "node:util";
3
+ import { resolveProjectConfig } from "../overlay.js";
4
+ import { parseCommandArgs } from "../parse-args.js";
5
+ import { cliRangeResult } from "../version-guard.js";
6
+ export function runConfig(ctx, args) {
7
+ const usage = `Usage: ${ctx.form} config [--json]\n`;
8
+ const json = parseConfigArgs(ctx, args, usage);
9
+ if (json === undefined)
10
+ return 0;
11
+ const resolved = resolveProjectConfig(ctx.cwd, ctx.env, ctx.home);
12
+ const report = buildConfigReport(ctx, resolved);
13
+ ctx.stdout.write(json ? `${JSON.stringify(report, undefined, 2)}\n` : renderConfigReport(report));
14
+ return 0;
15
+ }
16
+ function parseConfigArgs(ctx, args, usage) {
17
+ const { values, positionals } = parseCommandArgs(usage, () => parseArgs({
18
+ args,
19
+ options: {
20
+ json: { type: "boolean", default: false },
21
+ help: { type: "boolean", short: "h", default: false },
22
+ },
23
+ strict: true,
24
+ allowPositionals: true,
25
+ }));
26
+ if (values.help) {
27
+ ctx.stdout.write(usage);
28
+ return;
29
+ }
30
+ if (positionals.length > 0)
31
+ throw new CliError(`Unexpected argument: ${positionals[0]}\n\n${usage}`);
32
+ return values.json;
33
+ }
34
+ function buildConfigReport(ctx, resolved) {
35
+ const overlay = resolved?.overlay;
36
+ const cli = cliRangeResult(resolved?.config, ctx.version);
37
+ return {
38
+ source: resolved?.source ?? null,
39
+ overlay: overlay ? { dir: overlay.dir, matchedBy: overlay.matchedBy } : null,
40
+ cli: cli ? { installed: ctx.version, range: cli.range, satisfied: cli.satisfied } : null,
41
+ config: resolved?.config ?? null,
42
+ };
43
+ }
44
+ function renderConfigReport(report) {
45
+ const lines = [`Source: ${report.source ?? "none"}`];
46
+ if (report.overlay)
47
+ lines.push(`Overlay: ${report.overlay.dir} (matched by ${report.overlay.matchedBy})`);
48
+ if (report.cli)
49
+ lines.push(`CLI range: ${report.cli.range}, ${report.cli.satisfied ? "satisfied" : "not satisfied"} by ${report.cli.installed}`);
50
+ else
51
+ lines.push("CLI range: none");
52
+ if (report.config)
53
+ lines.push("Config:", JSON.stringify(report.config, undefined, 2));
54
+ return `${lines.join("\n")}\n`;
55
+ }
@@ -0,0 +1,2 @@
1
+ import type { CommandContext } from "../context.js";
2
+ export declare function runDevelopers(ctx: CommandContext, args: string[]): number;
@@ -0,0 +1,36 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { parseArgs } from "node:util";
4
+ import { CliError } from "../cli-error.js";
5
+ import { resolveProjectFile } from "../overlay.js";
6
+ import { parseCommandArgs } from "../parse-args.js";
7
+ export function runDevelopers(ctx, args) {
8
+ const usage = `Usage: ${ctx.form} DEVELOPERS.md\n`;
9
+ if (parseDevelopersArgs(ctx, args, usage))
10
+ return 0;
11
+ const file = resolveProjectFile(ctx.cwd, ctx.overlay, "DEVELOPERS.md");
12
+ if (file === undefined)
13
+ throw missingDevelopersError(ctx);
14
+ ctx.stdout.write(readFileSync(file.path, "utf-8"));
15
+ return 0;
16
+ }
17
+ function parseDevelopersArgs(ctx, args, usage) {
18
+ const { values, positionals } = parseCommandArgs(usage, () => parseArgs({
19
+ args,
20
+ options: { help: { type: "boolean", short: "h", default: false } },
21
+ strict: true,
22
+ allowPositionals: true,
23
+ }));
24
+ if (positionals.length > 0)
25
+ throw new CliError(`Unexpected argument: ${positionals[0]}\n\n${usage}`);
26
+ if (!values.help)
27
+ return false;
28
+ ctx.stdout.write(usage);
29
+ return true;
30
+ }
31
+ function missingDevelopersError(ctx) {
32
+ const tried = [join(ctx.cwd, "DEVELOPERS.md")];
33
+ if (ctx.overlay !== undefined)
34
+ tried.push(join(ctx.overlay.dir, "DEVELOPERS.md"));
35
+ return new CliError(`No DEVELOPERS.md found. Tried: ${tried.join(", ")}.`);
36
+ }
@@ -0,0 +1,2 @@
1
+ import type { CommandContext } from "../context.js";
2
+ export declare function runDocmap(ctx: CommandContext, args: string[]): number;
@@ -0,0 +1,22 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { main as docmapMain } from "@paleo/docmap";
4
+ import { resolveProjectFile } from "../overlay.js";
5
+ export function runDocmap(ctx, args) {
6
+ const docmapArgs = withOverlayRoot(ctx, args);
7
+ return docmapMain({
8
+ argv: ["node", "docmap", ...docmapArgs],
9
+ cwd: ctx.cwd,
10
+ stdout: ctx.stdout,
11
+ stderr: ctx.stderr,
12
+ commands: { base: `${ctx.form} docmap`, withArgs: `${ctx.form} docmap` },
13
+ });
14
+ }
15
+ function withOverlayRoot(ctx, args) {
16
+ if (args.includes("--root") || existsSync(join(ctx.cwd, "docs")))
17
+ return args;
18
+ const docs = resolveProjectFile(ctx.cwd, ctx.overlay, "docs");
19
+ if (docs?.source !== "overlay")
20
+ return args;
21
+ return [...args, "--root", docs.path];
22
+ }
@@ -0,0 +1,2 @@
1
+ import type { CommandContext } from "../context.js";
2
+ export declare function runDoctor(ctx: CommandContext, args: string[]): number;
@@ -0,0 +1,167 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { realpathSync } from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ import { fileURLToPath } from "node:url";
5
+ import { parseArgs } from "node:util";
6
+ import semver from "semver";
7
+ import { CliError } from "../cli-error.js";
8
+ import { errorMessage } from "../errors.js";
9
+ import { findExecutable } from "../executables.js";
10
+ import { findOverlay, resolveProjectConfig, resolveProjectFile, } from "../overlay.js";
11
+ import { parseCommandArgs } from "../parse-args.js";
12
+ import { resolvePlansMode } from "../plans/mode.js";
13
+ import { findInstalledSkill, STUB_SKILLS } from "../skills.js";
14
+ import { cliRangeResult } from "../version-guard.js";
15
+ const PROJECT_CONFIG_NAME = ".alignfirst.json";
16
+ export function runDoctor(ctx, args) {
17
+ const usage = `Usage: ${ctx.form} doctor\n`;
18
+ if (parseDoctorArgs(ctx, args, usage))
19
+ return 0;
20
+ writeSection(ctx, "CLI", () => inspectCli(ctx));
21
+ writeSection(ctx, "Config", () => inspectConfig(ctx));
22
+ writeSection(ctx, "Plans", () => inspectPlans(ctx));
23
+ writeSection(ctx, "Docmap", () => inspectDocmap(ctx));
24
+ writeSection(ctx, "Skills", () => inspectSkills(ctx));
25
+ writeSection(ctx, "Overlay", () => inspectOverlay(ctx));
26
+ writeSection(ctx, "Companion", () => inspectCompanion(ctx));
27
+ return 0;
28
+ }
29
+ function parseDoctorArgs(ctx, args, usage) {
30
+ const { values, positionals } = parseCommandArgs(usage, () => parseArgs({
31
+ args,
32
+ options: { help: { type: "boolean", short: "h", default: false } },
33
+ strict: true,
34
+ allowPositionals: true,
35
+ }));
36
+ if (positionals.length > 0)
37
+ throw new CliError(`Unexpected argument: ${positionals[0]}\n\n${usage}`);
38
+ if (!values.help)
39
+ return false;
40
+ ctx.stdout.write(usage);
41
+ return true;
42
+ }
43
+ function writeSection(ctx, section, inspect) {
44
+ let lines;
45
+ try {
46
+ lines = inspect();
47
+ }
48
+ catch (error) {
49
+ lines = [{ level: "error", text: firstLine(errorMessage(error)) }];
50
+ }
51
+ for (const line of lines)
52
+ ctx.stdout.write(`[${line.level}] ${section}: ${line.text}\n`);
53
+ }
54
+ function firstLine(text) {
55
+ return text.split("\n", 1)[0];
56
+ }
57
+ function inspectCli(ctx) {
58
+ const invokedPath = process.argv[1] ?? fileURLToPath(import.meta.url);
59
+ return [
60
+ {
61
+ level: "ok",
62
+ text: `${ctx.version}, ${realpathSync(invokedPath)}, launched as ${ctx.form}`,
63
+ },
64
+ ];
65
+ }
66
+ function inspectConfig(ctx) {
67
+ const resolved = resolveProjectConfig(ctx.cwd, ctx.env, ctx.home);
68
+ const lines = [{ level: "ok", text: `source ${configSource(resolved)}` }];
69
+ const result = cliRangeResult(resolved?.config, ctx.version);
70
+ if (result === undefined) {
71
+ lines.push({ level: "ok", text: "no cli range" });
72
+ return lines;
73
+ }
74
+ lines.push({
75
+ level: result.satisfied ? "ok" : "error",
76
+ text: `${result.satisfied ? "satisfies" : "does not satisfy"} ${result.range}`,
77
+ });
78
+ if (semver.gtr(ctx.version, result.range))
79
+ lines.push({ level: "warn", text: `${ctx.version} is ahead of ${result.range}` });
80
+ return lines;
81
+ }
82
+ function configSource(resolved) {
83
+ if (resolved === undefined)
84
+ return "none";
85
+ return resolved.source === "root" ? "root" : (resolved.overlay?.dir ?? "overlay");
86
+ }
87
+ function inspectPlans(ctx) {
88
+ const mode = resolvePlansMode(ctx.cwd, ctx.form);
89
+ return [
90
+ {
91
+ level: "ok",
92
+ text: mode.kind === "shared" ? `shared (${mode.repoToplevel})` : "local",
93
+ },
94
+ ];
95
+ }
96
+ function inspectDocmap(ctx) {
97
+ const overlay = findOverlay(ctx.cwd, ctx.env, ctx.home);
98
+ const docs = resolveProjectFile(ctx.cwd, overlay, "docs");
99
+ const source = docs?.source ?? "none";
100
+ return [
101
+ { level: docs === undefined ? "warn" : "ok", text: `docs/ source ${source}` },
102
+ { level: "ok", text: `embedded docmap ${readDocmapVersion()}` },
103
+ ];
104
+ }
105
+ function readDocmapVersion() {
106
+ const require = createRequire(import.meta.url);
107
+ const pkg = require("@paleo/docmap/package.json");
108
+ if (!isRecord(pkg) || typeof pkg.version !== "string")
109
+ throw new Error("@paleo/docmap package.json has no version");
110
+ return pkg.version;
111
+ }
112
+ function isRecord(value) {
113
+ return typeof value === "object" && value !== null;
114
+ }
115
+ function inspectSkills(ctx) {
116
+ return STUB_SKILLS.map((name) => {
117
+ const installed = findInstalledSkill(ctx.home, name);
118
+ if (installed === undefined)
119
+ return { level: "warn", text: `${name} missing` };
120
+ return {
121
+ level: "ok",
122
+ text: `${name} ${installed.version ?? "unknown"} (${installed.root})`,
123
+ };
124
+ });
125
+ }
126
+ function inspectOverlay(ctx) {
127
+ const configured = ctx.env.ALIGNFIRST_OVERLAYS;
128
+ if (configured === undefined || configured === "")
129
+ return [{ level: "ok", text: "no overlays directory" }];
130
+ const overlay = findOverlay(ctx.cwd, ctx.env, ctx.home);
131
+ if (overlay === undefined)
132
+ return [{ level: "ok", text: "no overlay matches" }];
133
+ const lines = [
134
+ { level: "ok", text: `${overlay.dir} (matched by ${overlay.matchedBy})` },
135
+ ];
136
+ for (const name of [PROJECT_CONFIG_NAME, "AGENTS.md", "DEVELOPERS.md", "docs"])
137
+ lines.push({
138
+ level: "ok",
139
+ text: `${name} ${resolveProjectFile(ctx.cwd, overlay, name)?.source ?? "none"}`,
140
+ });
141
+ return lines;
142
+ }
143
+ function inspectCompanion(ctx) {
144
+ const executable = findExecutable(ctx.env, "alcode");
145
+ if (executable === undefined)
146
+ return [
147
+ {
148
+ level: "warn",
149
+ text: "alcode not installed (optional; npm install -g @paleo/alcode)",
150
+ },
151
+ ];
152
+ try {
153
+ const version = execFileSync(executable, ["--version"], {
154
+ encoding: "utf-8",
155
+ env: ctx.env,
156
+ }).trim();
157
+ return [{ level: "ok", text: `alcode ${version} (${executable})` }];
158
+ }
159
+ catch (error) {
160
+ return [{ level: "error", text: `alcode ${executable}: ${commandError(error)}` }];
161
+ }
162
+ }
163
+ function commandError(error) {
164
+ if (isRecord(error) && typeof error.stderr === "string" && error.stderr !== "")
165
+ return firstLine(error.stderr);
166
+ return firstLine(errorMessage(error));
167
+ }
@@ -0,0 +1,2 @@
1
+ import type { CommandContext } from "../context.js";
2
+ export declare function runGuide(ctx: CommandContext, args: string[]): number;
@@ -0,0 +1,126 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { parseArgs } from "node:util";
3
+ import { CliError } from "../cli-error.js";
4
+ import { renderCommandForm } from "../command-form.js";
5
+ import { resolveProjectFile } from "../overlay.js";
6
+ import { parseCommandArgs } from "../parse-args.js";
7
+ import { PROTOCOLS } from "../protocols.js";
8
+ const TICKET_ID_RULE_PLACEHOLDER = "{{TICKET_ID_RULE}}";
9
+ const PERSPECTIVES = ["intent", "correctness", "safety", "quality"];
10
+ const MODULES = ["typescript-strict", "javascript", "python"];
11
+ const PROTOCOL_LIST = `${PROTOCOLS.join(", ")}, or overview`;
12
+ const PERSPECTIVE_LIST = PERSPECTIVES.join(", ");
13
+ const MODULE_LIST = MODULES.join(", ");
14
+ export function runGuide(ctx, args) {
15
+ const usage = renderUsage(ctx);
16
+ const options = parseGuideArgs(ctx, args, usage);
17
+ if (options === undefined)
18
+ return 0;
19
+ const guide = renderGuide(ctx, options);
20
+ ctx.stdout.write(`${renderCommandForm(guide, ctx.form).trimEnd()}\n`);
21
+ return 0;
22
+ }
23
+ function renderUsage(ctx) {
24
+ return `Usage:
25
+ ${ctx.form} guide [<protocol>] [--protocol-only]
26
+ ${ctx.form} guide overview
27
+ ${ctx.form} guide review --reviewer <perspective> [--module <module>]...
28
+ `;
29
+ }
30
+ function parseGuideArgs(ctx, args, usage) {
31
+ const { values, positionals } = parseCommandArgs(usage, () => parseArgs({
32
+ args,
33
+ options: {
34
+ "protocol-only": { type: "boolean", default: false },
35
+ reviewer: { type: "string" },
36
+ module: { type: "string", multiple: true },
37
+ help: { type: "boolean", short: "h", default: false },
38
+ },
39
+ strict: true,
40
+ allowPositionals: true,
41
+ }));
42
+ if (values.help) {
43
+ ctx.stdout.write(usage);
44
+ return;
45
+ }
46
+ if (positionals.length > 1)
47
+ throw new CliError(`Expected at most one protocol.\n\n${usage}`);
48
+ const protocol = parseProtocol(positionals[0]);
49
+ const reviewer = parsePerspective(values.reviewer);
50
+ const modules = (values.module ?? []).map(parseModule);
51
+ validateOptions(protocol, values["protocol-only"], reviewer, modules);
52
+ return { protocol, protocolOnly: values["protocol-only"], reviewer, modules };
53
+ }
54
+ function parseProtocol(value) {
55
+ if (value === undefined || value === "overview")
56
+ return value;
57
+ const protocol = PROTOCOLS.find((candidate) => candidate === value);
58
+ if (protocol === undefined)
59
+ throw new CliError(`Unknown protocol "${value}". Protocols: ${PROTOCOL_LIST}.`);
60
+ return protocol;
61
+ }
62
+ function parsePerspective(value) {
63
+ if (value === undefined)
64
+ return;
65
+ const perspective = PERSPECTIVES.find((candidate) => candidate === value);
66
+ if (perspective === undefined)
67
+ throw new CliError(`Unknown reviewer "${value}". Reviewers: ${PERSPECTIVE_LIST}.`);
68
+ return perspective;
69
+ }
70
+ function parseModule(value) {
71
+ const module = MODULES.find((candidate) => candidate === value);
72
+ if (module === undefined)
73
+ throw new CliError(`Unknown module "${value}". Modules: ${MODULE_LIST}.`);
74
+ return module;
75
+ }
76
+ function validateOptions(protocol, protocolOnly, reviewer, modules) {
77
+ if (protocolOnly && protocol === undefined)
78
+ throw new CliError("--protocol-only requires a protocol.");
79
+ if (protocolOnly && protocol === "overview")
80
+ throw new CliError("--protocol-only cannot be used with overview.");
81
+ if (reviewer !== undefined && protocol !== "review")
82
+ throw new CliError("--reviewer can only be used with review.");
83
+ if (modules.length > 0 && reviewer === undefined)
84
+ throw new CliError("--module requires --reviewer.");
85
+ }
86
+ function renderGuide(ctx, options) {
87
+ if (options.reviewer !== undefined)
88
+ return renderReviewerGuide(options.reviewer, options.modules);
89
+ if (options.protocol === "overview")
90
+ return readGuideTemplate("overview.md");
91
+ if (options.protocolOnly && options.protocol !== undefined)
92
+ return readProtocolTemplate(options.protocol);
93
+ const core = renderCoreGuide(ctx);
94
+ if (options.protocol === undefined)
95
+ return core;
96
+ return `${core.trimEnd()}\n\n${readProtocolTemplate(options.protocol).trimEnd()}`;
97
+ }
98
+ function renderReviewerGuide(perspective, modules) {
99
+ const templates = [
100
+ readGuideTemplate("code-review/reviewer-common.md"),
101
+ readGuideTemplate(`code-review/${perspective}-reviewer.md`),
102
+ ...modules.map((module) => readGuideTemplate(`code-review/module-${module}.md`)),
103
+ ];
104
+ return templates.map((template) => template.trimEnd()).join("\n\n");
105
+ }
106
+ function renderCoreGuide(ctx) {
107
+ const ticketRule = renderTicketIdRule(ctx);
108
+ const core = readGuideTemplate("core.md").replaceAll(TICKET_ID_RULE_PLACEHOLDER, () => ticketRule);
109
+ const projectConventions = resolveProjectFile(ctx.cwd, ctx.overlay, "AGENTS.md");
110
+ if (projectConventions?.source !== "overlay")
111
+ return core;
112
+ const content = readFileSync(projectConventions.path, "utf-8").trimEnd();
113
+ return `${core.trimEnd()}\n\n## Project conventions\n\n${content}`;
114
+ }
115
+ function renderTicketIdRule(ctx) {
116
+ const pattern = ctx.projectConfig?.config.ticketPattern;
117
+ if (pattern === undefined)
118
+ return "Ask the user for the ticket ID when it is not given.";
119
+ return `Ticket IDs match \`${pattern}\`. When the user gives no id, run \`{{CMD}} ticket\` without an id: it deduces the id from the current branch.`;
120
+ }
121
+ function readProtocolTemplate(protocol) {
122
+ return readGuideTemplate(`protocols/${protocol}.md`);
123
+ }
124
+ function readGuideTemplate(path) {
125
+ return readFileSync(new URL(`../../templates/guide/${path}`, import.meta.url), "utf-8").trimEnd();
126
+ }
@@ -0,0 +1,2 @@
1
+ import type { CommandContext } from "../context.js";
2
+ export declare function runPlans(ctx: CommandContext, args: string[]): number;