alignfirst 0.1.0-beta.0 → 0.1.0-beta.2
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.
- package/README.md +79 -6
- package/dist/cli.js +11 -12
- package/dist/commands/config.js +2 -6
- package/dist/commands/context.d.ts +2 -0
- package/dist/commands/context.js +11 -0
- package/dist/commands/conventions.d.ts +2 -0
- package/dist/commands/conventions.js +9 -0
- package/dist/commands/docmap.js +1 -13
- package/dist/commands/doctor.js +33 -55
- package/dist/commands/guide.js +80 -16
- package/dist/commands/plans.js +33 -22
- package/dist/commands/sync.js +47 -11
- package/dist/commands/ticket.js +12 -3
- package/dist/context.d.ts +1 -2
- package/dist/conventions.d.ts +8 -0
- package/dist/conventions.js +86 -0
- package/dist/default-branch.d.ts +8 -0
- package/dist/default-branch.js +32 -0
- package/dist/parse-args.d.ts +2 -0
- package/dist/parse-args.js +15 -0
- package/dist/plans/layout.d.ts +3 -0
- package/dist/plans/layout.js +7 -1
- package/dist/plans/mode.js +2 -2
- package/dist/plans/rebase.d.ts +6 -0
- package/dist/plans/rebase.js +27 -0
- package/dist/plans/ticket.d.ts +12 -1
- package/dist/plans/ticket.js +16 -8
- package/dist/project-config.d.ts +19 -12
- package/dist/project-config.js +37 -34
- package/dist/skills.d.ts +0 -2
- package/dist/skills.js +0 -21
- package/dist/version-guard.d.ts +0 -1
- package/dist/version-guard.js +0 -7
- package/package.json +2 -2
- package/templates/guide/core.md +9 -9
- package/templates/guide/protocols/aad.md +3 -3
- package/templates/guide/protocols/catchup.md +1 -1
- package/templates/guide/protocols/description.md +3 -3
- package/templates/guide/protocols/merge.md +3 -3
- package/templates/guide/protocols/plan.md +3 -3
- package/templates/guide/protocols/review.md +3 -3
- package/templates/guide/protocols/spec.md +3 -3
- package/dist/commands/developers.d.ts +0 -2
- package/dist/commands/developers.js +0 -36
- package/dist/commands/setup.d.ts +0 -2
- package/dist/commands/setup.js +0 -254
- package/dist/overlay.d.ts +0 -19
- package/dist/overlay.js +0 -83
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# alignfirst
|
|
2
2
|
|
|
3
|
-
The AlignFirst CLI provides
|
|
3
|
+
The AlignFirst CLI provides collaborative software-development workflows, task files, shared plans, project conventions, documentation discovery, and setup diagnostics.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
4
6
|
|
|
5
7
|
Install it globally:
|
|
6
8
|
|
|
@@ -11,21 +13,92 @@ npm install -g alignfirst
|
|
|
11
13
|
Or run the current version without installing it:
|
|
12
14
|
|
|
13
15
|
```sh
|
|
14
|
-
npx
|
|
16
|
+
npx alignfirst
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Setup with your agent
|
|
20
|
+
|
|
21
|
+
Temporarily install the setup-guide skill:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npx skills add https://github.com/paleo/alignfirst --skill alignfirst-setup-guide
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Then ask your agent:
|
|
28
|
+
|
|
29
|
+
```text
|
|
30
|
+
Use your alignfirst-setup-guide skill. Set up AlignFirst in this project. Ask before installing anything globally.
|
|
15
31
|
```
|
|
16
32
|
|
|
17
|
-
|
|
33
|
+
The guide installs the selected components and configures the repository. Remove the setup-guide skill when setup is complete.
|
|
34
|
+
|
|
35
|
+
## Commands
|
|
18
36
|
|
|
19
37
|
- `guide` — Print an AlignFirst protocol.
|
|
20
38
|
- `ticket` — Resolve a ticket directory and its next file.
|
|
21
39
|
- `sync` — Synchronize shared plans.
|
|
22
40
|
- `plans` — Set up, check and archive plans.
|
|
23
41
|
- `docmap` — Browse project documentation.
|
|
42
|
+
- `conventions` — Print the effective project conventions.
|
|
43
|
+
- `context` — Print the conventions and the documentation map.
|
|
24
44
|
- `config` — Report the effective project configuration.
|
|
25
|
-
- `DEVELOPERS.md` — Print the project developer guide.
|
|
26
|
-
- `setup` — Prepare an AlignFirst project.
|
|
27
45
|
- `doctor` — Diagnose an AlignFirst setup.
|
|
28
46
|
|
|
29
47
|
Run `alignfirst --help` for command usage or `alignfirst guide` for the collaboration guide.
|
|
30
48
|
|
|
31
|
-
|
|
49
|
+
## Agent skills
|
|
50
|
+
|
|
51
|
+
Eight optional Agent Skill stubs expose the CLI to GitHub Copilot, Cursor, Claude Code, and Codex. The skills contain no protocols; they invoke `npx alignfirst guide`.
|
|
52
|
+
|
|
53
|
+
Install them globally:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
npx skills add https://github.com/paleo/alignfirst --global \
|
|
57
|
+
--skill alignfirst --skill al --skill alplan --skill alspec \
|
|
58
|
+
--skill aldescription --skill alreview --skill alcatchup --skill almerge
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Restart the agent after installation. Claude Code, GitHub Copilot, and Cursor expose skills with `/`; Codex uses `$`.
|
|
62
|
+
|
|
63
|
+
## Workflows
|
|
64
|
+
|
|
65
|
+
These examples use the `/` form. Replace it with `$` in Codex.
|
|
66
|
+
|
|
67
|
+
| Workflow | Command | Result |
|
|
68
|
+
| --- | --- | --- |
|
|
69
|
+
| Specification | `/alspec <request>` | Discuss and write a technical specification. |
|
|
70
|
+
| Planning | `/alplan` | Turn a specification into one or more implementation plans. |
|
|
71
|
+
| Align and do | `/al <request>` | Discuss and implement a small change, then write a summary. |
|
|
72
|
+
| Description | `/aldescription` | Summarize the work and propose a commit message. |
|
|
73
|
+
| Review | `/alreview` | Review the current branch against its base. |
|
|
74
|
+
| Merge | `/almerge` | Resolve merge or rebase conflicts. |
|
|
75
|
+
| Catch up | `/alcatchup` | Load the current task history and continue. |
|
|
76
|
+
|
|
77
|
+
To implement a plan, start a fresh agent context and ask it to execute the plan file.
|
|
78
|
+
|
|
79
|
+
AlignFirst stores specifications, plans, and summaries in `.plans/<ticket-id>/`. It normally derives the ticket ID from the request or branch and asks when none is available. Files use a cycle letter and sequence number, such as `A1-spec.md` and `A2-plan.md`.
|
|
80
|
+
|
|
81
|
+
## Updates
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
npm update -g alignfirst
|
|
85
|
+
npx skills update --global --yes
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Upgrade from v1, v2, or v3
|
|
89
|
+
|
|
90
|
+
Install the setup-guide skill and ask your agent to run its upgrade route:
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
npx skills add https://github.com/paleo/alignfirst --skill alignfirst-setup-guide
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```text
|
|
97
|
+
Use your alignfirst-setup-guide skill. Upgrade AlignFirst in this project.
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
_Note: the setup-guide skill can be removed safely after it's done._
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
CC0 1.0 Universal.
|
package/dist/cli.js
CHANGED
|
@@ -3,15 +3,15 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { CliError } from "./cli-error.js";
|
|
4
4
|
import { resolveCommandForm } from "./command-form.js";
|
|
5
5
|
import { runConfig } from "./commands/config.js";
|
|
6
|
-
import {
|
|
6
|
+
import { runContext } from "./commands/context.js";
|
|
7
|
+
import { runConventions } from "./commands/conventions.js";
|
|
7
8
|
import { runDoctor } from "./commands/doctor.js";
|
|
8
9
|
import { runDocmap } from "./commands/docmap.js";
|
|
9
10
|
import { runGuide } from "./commands/guide.js";
|
|
10
11
|
import { runPlans } from "./commands/plans.js";
|
|
11
|
-
import { runSetup } from "./commands/setup.js";
|
|
12
12
|
import { runSync } from "./commands/sync.js";
|
|
13
13
|
import { runTicket } from "./commands/ticket.js";
|
|
14
|
-
import { resolveProjectConfig } from "./
|
|
14
|
+
import { resolveProjectConfig } from "./project-config.js";
|
|
15
15
|
import { checkCliRange } from "./version-guard.js";
|
|
16
16
|
export async function main(options) {
|
|
17
17
|
const argv = options?.argv ?? process.argv;
|
|
@@ -36,8 +36,7 @@ export async function main(options) {
|
|
|
36
36
|
return 0;
|
|
37
37
|
}
|
|
38
38
|
if (command !== "config" && command !== "doctor") {
|
|
39
|
-
ctx.projectConfig = resolveProjectConfig(ctx.cwd
|
|
40
|
-
ctx.overlay = ctx.projectConfig?.overlay;
|
|
39
|
+
ctx.projectConfig = resolveProjectConfig(ctx.cwd);
|
|
41
40
|
checkCliRange(ctx.projectConfig?.config, ctx.version, [command, ...args]);
|
|
42
41
|
}
|
|
43
42
|
return dispatch(ctx, command, args);
|
|
@@ -61,12 +60,12 @@ function renderHelp(ctx) {
|
|
|
61
60
|
Usage:
|
|
62
61
|
${ctx.form} guide [<protocol>]
|
|
63
62
|
${ctx.form} ticket [<id>]
|
|
64
|
-
${ctx.form} sync [--auto-archive]
|
|
63
|
+
${ctx.form} sync [--auto-archive | --no-auto-archive]
|
|
65
64
|
${ctx.form} plans <command>
|
|
66
65
|
${ctx.form} docmap [<arguments>]
|
|
66
|
+
${ctx.form} conventions
|
|
67
|
+
${ctx.form} context
|
|
67
68
|
${ctx.form} config [--json]
|
|
68
|
-
${ctx.form} DEVELOPERS.md
|
|
69
|
-
${ctx.form} setup [<options>]
|
|
70
69
|
${ctx.form} doctor
|
|
71
70
|
${ctx.form} --help
|
|
72
71
|
${ctx.form} --version
|
|
@@ -84,12 +83,12 @@ function dispatch(ctx, command, args) {
|
|
|
84
83
|
return runPlans(ctx, args);
|
|
85
84
|
case "docmap":
|
|
86
85
|
return runDocmap(ctx, args);
|
|
86
|
+
case "conventions":
|
|
87
|
+
return runConventions(ctx, args);
|
|
88
|
+
case "context":
|
|
89
|
+
return runContext(ctx, args);
|
|
87
90
|
case "config":
|
|
88
91
|
return runConfig(ctx, args);
|
|
89
|
-
case "DEVELOPERS.md":
|
|
90
|
-
return runDevelopers(ctx, args);
|
|
91
|
-
case "setup":
|
|
92
|
-
return runSetup(ctx, args);
|
|
93
92
|
case "doctor":
|
|
94
93
|
return runDoctor(ctx, args);
|
|
95
94
|
default:
|
package/dist/commands/config.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { CliError } from "../cli-error.js";
|
|
2
2
|
import { parseArgs } from "node:util";
|
|
3
|
-
import { resolveProjectConfig } from "../overlay.js";
|
|
4
3
|
import { parseCommandArgs } from "../parse-args.js";
|
|
4
|
+
import { resolveProjectConfig } from "../project-config.js";
|
|
5
5
|
import { cliRangeResult } from "../version-guard.js";
|
|
6
6
|
export function runConfig(ctx, args) {
|
|
7
7
|
const usage = `Usage: ${ctx.form} config [--json]\n`;
|
|
8
8
|
const json = parseConfigArgs(ctx, args, usage);
|
|
9
9
|
if (json === undefined)
|
|
10
10
|
return 0;
|
|
11
|
-
const resolved = resolveProjectConfig(ctx.cwd
|
|
11
|
+
const resolved = resolveProjectConfig(ctx.cwd);
|
|
12
12
|
const report = buildConfigReport(ctx, resolved);
|
|
13
13
|
ctx.stdout.write(json ? `${JSON.stringify(report, undefined, 2)}\n` : renderConfigReport(report));
|
|
14
14
|
return 0;
|
|
@@ -32,19 +32,15 @@ function parseConfigArgs(ctx, args, usage) {
|
|
|
32
32
|
return values.json;
|
|
33
33
|
}
|
|
34
34
|
function buildConfigReport(ctx, resolved) {
|
|
35
|
-
const overlay = resolved?.overlay;
|
|
36
35
|
const cli = cliRangeResult(resolved?.config, ctx.version);
|
|
37
36
|
return {
|
|
38
37
|
source: resolved?.source ?? null,
|
|
39
|
-
overlay: overlay ? { dir: overlay.dir, matchedBy: overlay.matchedBy } : null,
|
|
40
38
|
cli: cli ? { installed: ctx.version, range: cli.range, satisfied: cli.satisfied } : null,
|
|
41
39
|
config: resolved?.config ?? null,
|
|
42
40
|
};
|
|
43
41
|
}
|
|
44
42
|
function renderConfigReport(report) {
|
|
45
43
|
const lines = [`Source: ${report.source ?? "none"}`];
|
|
46
|
-
if (report.overlay)
|
|
47
|
-
lines.push(`Overlay: ${report.overlay.dir} (matched by ${report.overlay.matchedBy})`);
|
|
48
44
|
if (report.cli)
|
|
49
45
|
lines.push(`CLI range: ${report.cli.range}, ${report.cli.satisfied ? "satisfied" : "not satisfied"} by ${report.cli.installed}`);
|
|
50
46
|
else
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { renderConventions } from "../conventions.js";
|
|
2
|
+
import { parseBareCommandArgs } from "../parse-args.js";
|
|
3
|
+
import { runDocmap } from "./docmap.js";
|
|
4
|
+
export function runContext(ctx, args) {
|
|
5
|
+
const usage = `Usage: ${ctx.form} context\n`;
|
|
6
|
+
if (parseBareCommandArgs(ctx, args, usage))
|
|
7
|
+
return 0;
|
|
8
|
+
ctx.stdout.write(renderConventions(ctx));
|
|
9
|
+
ctx.stdout.write("\n");
|
|
10
|
+
return runDocmap(ctx, []);
|
|
11
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { renderConventions } from "../conventions.js";
|
|
2
|
+
import { parseBareCommandArgs } from "../parse-args.js";
|
|
3
|
+
export function runConventions(ctx, args) {
|
|
4
|
+
const usage = `Usage: ${ctx.form} conventions\n`;
|
|
5
|
+
if (parseBareCommandArgs(ctx, args, usage))
|
|
6
|
+
return 0;
|
|
7
|
+
ctx.stdout.write(renderConventions(ctx));
|
|
8
|
+
return 0;
|
|
9
|
+
}
|
package/dist/commands/docmap.js
CHANGED
|
@@ -1,22 +1,10 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
1
|
import { main as docmapMain } from "@paleo/docmap";
|
|
4
|
-
import { resolveProjectFile } from "../overlay.js";
|
|
5
2
|
export function runDocmap(ctx, args) {
|
|
6
|
-
const docmapArgs = withOverlayRoot(ctx, args);
|
|
7
3
|
return docmapMain({
|
|
8
|
-
argv: ["node", "docmap", ...
|
|
4
|
+
argv: ["node", "docmap", ...args],
|
|
9
5
|
cwd: ctx.cwd,
|
|
10
6
|
stdout: ctx.stdout,
|
|
11
7
|
stderr: ctx.stderr,
|
|
12
8
|
commands: { base: `${ctx.form} docmap`, withArgs: `${ctx.form} docmap` },
|
|
13
9
|
});
|
|
14
10
|
}
|
|
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
|
-
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,45 +1,35 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { realpathSync } from "node:fs";
|
|
2
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
+
import { join } from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { parseArgs } from "node:util";
|
|
6
6
|
import semver from "semver";
|
|
7
|
-
import {
|
|
7
|
+
import { resolveDefaultBranch } from "../default-branch.js";
|
|
8
8
|
import { errorMessage } from "../errors.js";
|
|
9
9
|
import { findExecutable } from "../executables.js";
|
|
10
|
-
import {
|
|
11
|
-
import { parseCommandArgs } from "../parse-args.js";
|
|
10
|
+
import { parseBareCommandArgs } from "../parse-args.js";
|
|
12
11
|
import { resolvePlansMode } from "../plans/mode.js";
|
|
12
|
+
import { findStoppedRebase } from "../plans/rebase.js";
|
|
13
|
+
import { resolveProjectConfig } from "../project-config.js";
|
|
13
14
|
import { findInstalledSkill, STUB_SKILLS } from "../skills.js";
|
|
14
15
|
import { cliRangeResult } from "../version-guard.js";
|
|
15
|
-
const PROJECT_CONFIG_NAME = ".alignfirst.json";
|
|
16
16
|
export function runDoctor(ctx, args) {
|
|
17
17
|
const usage = `Usage: ${ctx.form} doctor\n`;
|
|
18
|
-
if (
|
|
18
|
+
if (parseBareCommandArgs(ctx, args, usage))
|
|
19
19
|
return 0;
|
|
20
20
|
writeSection(ctx, "CLI", () => inspectCli(ctx));
|
|
21
|
-
|
|
21
|
+
let resolved;
|
|
22
|
+
writeSection(ctx, "Config", () => {
|
|
23
|
+
resolved = resolveProjectConfig(ctx.cwd);
|
|
24
|
+
return inspectConfig(ctx, resolved);
|
|
25
|
+
});
|
|
26
|
+
writeSection(ctx, "Git", () => inspectGit(ctx, resolved));
|
|
22
27
|
writeSection(ctx, "Plans", () => inspectPlans(ctx));
|
|
23
28
|
writeSection(ctx, "Docmap", () => inspectDocmap(ctx));
|
|
24
29
|
writeSection(ctx, "Skills", () => inspectSkills(ctx));
|
|
25
|
-
writeSection(ctx, "Overlay", () => inspectOverlay(ctx));
|
|
26
30
|
writeSection(ctx, "Companion", () => inspectCompanion(ctx));
|
|
27
31
|
return 0;
|
|
28
32
|
}
|
|
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
33
|
function writeSection(ctx, section, inspect) {
|
|
44
34
|
let lines;
|
|
45
35
|
try {
|
|
@@ -63,9 +53,8 @@ function inspectCli(ctx) {
|
|
|
63
53
|
},
|
|
64
54
|
];
|
|
65
55
|
}
|
|
66
|
-
function inspectConfig(ctx) {
|
|
67
|
-
const
|
|
68
|
-
const lines = [{ level: "ok", text: `source ${configSource(resolved)}` }];
|
|
56
|
+
function inspectConfig(ctx, resolved) {
|
|
57
|
+
const lines = [{ level: "ok", text: resolved?.source ?? "none" }];
|
|
69
58
|
const result = cliRangeResult(resolved?.config, ctx.version);
|
|
70
59
|
if (result === undefined) {
|
|
71
60
|
lines.push({ level: "ok", text: "no cli range" });
|
|
@@ -79,13 +68,17 @@ function inspectConfig(ctx) {
|
|
|
79
68
|
lines.push({ level: "warn", text: `${ctx.version} is ahead of ${result.range}` });
|
|
80
69
|
return lines;
|
|
81
70
|
}
|
|
82
|
-
function
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
71
|
+
function inspectGit(ctx, resolved) {
|
|
72
|
+
const branch = resolveDefaultBranch(ctx.cwd, resolved?.config);
|
|
73
|
+
if (branch === undefined)
|
|
74
|
+
return [{ level: "warn", text: "default branch unresolved" }];
|
|
75
|
+
const source = branch.source === "config" ? "git.defaultBranch" : `cached ${branch.remote}/HEAD`;
|
|
76
|
+
return [{ level: "ok", text: `default branch ${branch.name} (${source})` }];
|
|
86
77
|
}
|
|
87
78
|
function inspectPlans(ctx) {
|
|
88
79
|
const mode = resolvePlansMode(ctx.cwd, ctx.form);
|
|
80
|
+
if (mode.kind === "shared" && findStoppedRebase(mode.repoToplevel) !== undefined)
|
|
81
|
+
return [{ level: "error", text: `rebase stopped on a conflict in ${mode.repoToplevel}` }];
|
|
89
82
|
return [
|
|
90
83
|
{
|
|
91
84
|
level: "ok",
|
|
@@ -94,11 +87,9 @@ function inspectPlans(ctx) {
|
|
|
94
87
|
];
|
|
95
88
|
}
|
|
96
89
|
function inspectDocmap(ctx) {
|
|
97
|
-
const
|
|
98
|
-
const docs = resolveProjectFile(ctx.cwd, overlay, "docs");
|
|
99
|
-
const source = docs?.source ?? "none";
|
|
90
|
+
const present = existsSync(join(ctx.cwd, "docs"));
|
|
100
91
|
return [
|
|
101
|
-
{ level:
|
|
92
|
+
{ level: "ok", text: `docs/ ${present ? "present" : "none"}` },
|
|
102
93
|
{ level: "ok", text: `embedded docmap ${readDocmapVersion()}` },
|
|
103
94
|
];
|
|
104
95
|
}
|
|
@@ -117,29 +108,16 @@ function inspectSkills(ctx) {
|
|
|
117
108
|
const installed = findInstalledSkill(ctx.home, name);
|
|
118
109
|
if (installed === undefined)
|
|
119
110
|
return { level: "warn", text: `${name} missing` };
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
111
|
+
const version = installed.version ?? "unknown";
|
|
112
|
+
const parsed = semver.parse(installed.version);
|
|
113
|
+
if (parsed === null || parsed.major < 4)
|
|
114
|
+
return {
|
|
115
|
+
level: "warn",
|
|
116
|
+
text: `${name} ${version} predates v4; update: npx -y skills update --global --yes`,
|
|
117
|
+
};
|
|
118
|
+
return { level: "ok", text: `${name} ${version} (${installed.root})` };
|
|
124
119
|
});
|
|
125
120
|
}
|
|
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
121
|
function inspectCompanion(ctx) {
|
|
144
122
|
const executable = findExecutable(ctx.env, "alcode");
|
|
145
123
|
if (executable === undefined)
|
package/dist/commands/guide.js
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { lstatSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
2
3
|
import { parseArgs } from "node:util";
|
|
3
4
|
import { CliError } from "../cli-error.js";
|
|
4
5
|
import { renderCommandForm } from "../command-form.js";
|
|
5
|
-
import {
|
|
6
|
+
import { commitSubject, renderConventions } from "../conventions.js";
|
|
7
|
+
import { resolveDefaultBranch } from "../default-branch.js";
|
|
6
8
|
import { parseCommandArgs } from "../parse-args.js";
|
|
9
|
+
import { missingPlansMessage } from "../plans/layout.js";
|
|
10
|
+
import { resolvePlansMode } from "../plans/mode.js";
|
|
11
|
+
import { detectTicketFromBranch } from "../plans/ticket.js";
|
|
7
12
|
import { PROTOCOLS } from "../protocols.js";
|
|
8
|
-
const
|
|
13
|
+
const TICKET_CMD_PLACEHOLDER = "{{TICKET_CMD}}";
|
|
14
|
+
const TICKET_CONTEXT_PLACEHOLDER = "{{TICKET_CONTEXT}}";
|
|
15
|
+
const PLANS_STATE_PLACEHOLDER = "{{PLANS_STATE}}";
|
|
16
|
+
const COMMIT_RULE_PLACEHOLDER = "{{COMMIT_RULE}}";
|
|
17
|
+
const BASE_BRANCH_RULE_PLACEHOLDER = "{{BASE_BRANCH_RULE}}";
|
|
9
18
|
const PERSPECTIVES = ["intent", "correctness", "safety", "quality"];
|
|
10
19
|
const MODULES = ["typescript-strict", "javascript", "python"];
|
|
11
20
|
const PROTOCOL_LIST = `${PROTOCOLS.join(", ")}, or overview`;
|
|
@@ -88,12 +97,14 @@ function renderGuide(ctx, options) {
|
|
|
88
97
|
return renderReviewerGuide(options.reviewer, options.modules);
|
|
89
98
|
if (options.protocol === "overview")
|
|
90
99
|
return readGuideTemplate("overview.md");
|
|
100
|
+
const placeholders = buildGuidePlaceholders(ctx, options.protocol);
|
|
91
101
|
if (options.protocolOnly && options.protocol !== undefined)
|
|
92
|
-
return readProtocolTemplate(options.protocol);
|
|
93
|
-
const core = renderCoreGuide(ctx);
|
|
102
|
+
return applyPlaceholders(readProtocolTemplate(options.protocol), placeholders);
|
|
103
|
+
const core = renderCoreGuide(ctx, placeholders);
|
|
94
104
|
if (options.protocol === undefined)
|
|
95
105
|
return core;
|
|
96
|
-
|
|
106
|
+
const protocol = applyPlaceholders(readProtocolTemplate(options.protocol), placeholders);
|
|
107
|
+
return `${core.trimEnd()}\n\n${protocol.trimEnd()}`;
|
|
97
108
|
}
|
|
98
109
|
function renderReviewerGuide(perspective, modules) {
|
|
99
110
|
const templates = [
|
|
@@ -103,20 +114,73 @@ function renderReviewerGuide(perspective, modules) {
|
|
|
103
114
|
];
|
|
104
115
|
return templates.map((template) => template.trimEnd()).join("\n\n");
|
|
105
116
|
}
|
|
106
|
-
function renderCoreGuide(ctx) {
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
const projectConventions = resolveProjectFile(ctx.cwd, ctx.overlay, "AGENTS.md");
|
|
110
|
-
if (projectConventions?.source !== "overlay")
|
|
117
|
+
function renderCoreGuide(ctx, placeholders) {
|
|
118
|
+
const core = applyPlaceholders(readGuideTemplate("core.md"), placeholders);
|
|
119
|
+
if (ctx.projectConfig !== undefined)
|
|
111
120
|
return core;
|
|
112
|
-
|
|
113
|
-
return `${core.trimEnd()}\n\n## Project conventions\n\n${content}`;
|
|
121
|
+
return `${core.trimEnd()}\n\n## Project conventions\n\n${renderConventions(ctx).trimEnd()}`;
|
|
114
122
|
}
|
|
115
|
-
function
|
|
116
|
-
const pattern = ctx.projectConfig?.config.
|
|
123
|
+
function buildGuidePlaceholders(ctx, protocol) {
|
|
124
|
+
const pattern = ctx.projectConfig?.config.ticketIdPattern;
|
|
125
|
+
const detection = pattern === undefined ? undefined : detectTicketFromBranch(ctx.cwd, pattern);
|
|
126
|
+
return {
|
|
127
|
+
ticketCommand: detection?.kind === "detected" ? "{{CMD}} ticket" : "{{CMD}} ticket <id>",
|
|
128
|
+
ticketContext: renderTicketContext(pattern, detection),
|
|
129
|
+
plansState: renderPlansState(ctx),
|
|
130
|
+
commitRule: renderCommitRule(ctx),
|
|
131
|
+
baseBranchRule: renderBaseBranchRule(ctx, protocol),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function renderTicketContext(pattern, detection) {
|
|
117
135
|
if (pattern === undefined)
|
|
118
136
|
return "Ask the user for the ticket ID when it is not given.";
|
|
119
|
-
|
|
137
|
+
if (detection?.kind === "detected")
|
|
138
|
+
return `Current ticket: \`${detection.id}\` (from branch \`${detection.branch}\`). The id argument of \`{{CMD}} ticket\` is optional and defaults to it; pass an id only when the user names another ticket.`;
|
|
139
|
+
if (detection?.kind === "noMatch")
|
|
140
|
+
return `No ticket id on branch \`${detection.branch}\`. Ask the user for the id.`;
|
|
141
|
+
return "No ticket id: detached HEAD. Ask the user for the id.";
|
|
142
|
+
}
|
|
143
|
+
function renderPlansState(ctx) {
|
|
144
|
+
const entry = lstatSync(join(ctx.cwd, ".plans"), { throwIfNoEntry: false });
|
|
145
|
+
if (entry === undefined)
|
|
146
|
+
return `\`\`\`text\n${missingPlansMessage(ctx.form)}\n\`\`\``;
|
|
147
|
+
try {
|
|
148
|
+
return resolvePlansMode(ctx.cwd, ctx.form).kind === "shared"
|
|
149
|
+
? "After every change in TASK_DIR, run `{{CMD}} sync`."
|
|
150
|
+
: "";
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return "";
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function renderCommitRule(ctx) {
|
|
157
|
+
const commit = ctx.projectConfig?.config.git?.commit;
|
|
158
|
+
if (commit === undefined)
|
|
159
|
+
return "(follow the convention you are aware of, or default to `<type>: [<ticket_id>] very short description`)";
|
|
160
|
+
const { subject, side } = commitSubject(commit);
|
|
161
|
+
const rule = side === undefined ? subject : `${subject}; ${side}`;
|
|
162
|
+
return `(project convention: ${rule})`;
|
|
163
|
+
}
|
|
164
|
+
function renderBaseBranchRule(ctx, protocol) {
|
|
165
|
+
const branch = resolveDefaultBranch(ctx.cwd, ctx.projectConfig?.config);
|
|
166
|
+
if (protocol === "review")
|
|
167
|
+
return branch === undefined
|
|
168
|
+
? "the **base branch** to compare against - use the branch provided by the user, or fall back to the default branch."
|
|
169
|
+
: `the **base branch** to compare against - use the branch provided by the user, or fall back to \`${branch.name}\`, the default branch.`;
|
|
170
|
+
if (protocol === "merge")
|
|
171
|
+
return branch === undefined
|
|
172
|
+
? "otherwise ask which branch to merge."
|
|
173
|
+
: `otherwise merge \`${branch.name}\`, the default branch.`;
|
|
174
|
+
return "";
|
|
175
|
+
}
|
|
176
|
+
function applyPlaceholders(template, values) {
|
|
177
|
+
return template
|
|
178
|
+
.replaceAll(`${PLANS_STATE_PLACEHOLDER}\n\n`, () => values.plansState === "" ? "" : `${values.plansState}\n\n`)
|
|
179
|
+
.replaceAll(TICKET_CMD_PLACEHOLDER, () => values.ticketCommand)
|
|
180
|
+
.replaceAll(TICKET_CONTEXT_PLACEHOLDER, () => values.ticketContext)
|
|
181
|
+
.replaceAll(PLANS_STATE_PLACEHOLDER, () => values.plansState)
|
|
182
|
+
.replaceAll(COMMIT_RULE_PLACEHOLDER, () => values.commitRule)
|
|
183
|
+
.replaceAll(BASE_BRANCH_RULE_PLACEHOLDER, () => values.baseBranchRule);
|
|
120
184
|
}
|
|
121
185
|
function readProtocolTemplate(protocol) {
|
|
122
186
|
return readGuideTemplate(`protocols/${protocol}.md`);
|
package/dist/commands/plans.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, realpathSync, statSync } from "node:fs";
|
|
2
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
2
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
4
|
import { CliError } from "../cli-error.js";
|
|
5
5
|
import { assertMainWorktreeRoot } from "../git.js";
|
|
6
|
-
import { parseCommandArgs } from "../parse-args.js";
|
|
6
|
+
import { parseBareCommandArgs, parseCommandArgs } from "../parse-args.js";
|
|
7
7
|
import { archiveEntry, archiveThresholdDays, autoArchive } from "../plans/archive.js";
|
|
8
8
|
import { linkPlans } from "../plans/link.js";
|
|
9
9
|
import { resolvePlansMode } from "../plans/mode.js";
|
|
10
|
+
import { findStoppedRebase, renderStoppedRebase } from "../plans/rebase.js";
|
|
11
|
+
const RESERVED_PLANS_FOLDERS = new Set([".git", ".plans", "_archives", "_project"]);
|
|
10
12
|
export function runPlans(ctx, args) {
|
|
11
13
|
const [command, ...rest] = args;
|
|
12
14
|
switch (command) {
|
|
@@ -42,11 +44,29 @@ function runSetup(ctx, args) {
|
|
|
42
44
|
assertMainWorktreeRoot(ctx.cwd);
|
|
43
45
|
const cloneDir = resolve(ctx.cwd, parsed.dir);
|
|
44
46
|
checkClone(ctx, cloneDir);
|
|
45
|
-
const projectDir =
|
|
46
|
-
mkdirSync(projectDir, { recursive: true });
|
|
47
|
+
const projectDir = createPlansDirectory(cloneDir, parsed.folder);
|
|
47
48
|
linkPlans(ctx, projectDir);
|
|
48
49
|
return 0;
|
|
49
50
|
}
|
|
51
|
+
function createPlansDirectory(cloneDir, folder) {
|
|
52
|
+
if (folder.length === 0 || folder === "." || folder === ".." || /[\\/]/u.test(folder)) {
|
|
53
|
+
throw new CliError(`Plans folder "${folder}" must be a single path segment.`);
|
|
54
|
+
}
|
|
55
|
+
if (RESERVED_PLANS_FOLDERS.has(folder.toLowerCase())) {
|
|
56
|
+
throw new CliError(`Plans folder "${folder}" is reserved.`);
|
|
57
|
+
}
|
|
58
|
+
const cloneRoot = realpathSync(cloneDir);
|
|
59
|
+
const projectDir = join(cloneRoot, folder);
|
|
60
|
+
mkdirSync(projectDir, { recursive: true });
|
|
61
|
+
const relativeTarget = relative(cloneRoot, realpathSync(projectDir));
|
|
62
|
+
if (relativeTarget === "" ||
|
|
63
|
+
relativeTarget === ".." ||
|
|
64
|
+
relativeTarget.startsWith(`..${sep}`) ||
|
|
65
|
+
isAbsolute(relativeTarget)) {
|
|
66
|
+
throw new CliError(`Plans folder "${folder}" must resolve inside ${cloneRoot}.`);
|
|
67
|
+
}
|
|
68
|
+
return projectDir;
|
|
69
|
+
}
|
|
50
70
|
function parseSetupArgs(ctx, args, usage) {
|
|
51
71
|
const { values, positionals } = parseCommandArgs(usage, () => parseArgs({
|
|
52
72
|
args,
|
|
@@ -64,8 +84,8 @@ function parseSetupArgs(ctx, args, usage) {
|
|
|
64
84
|
if (positionals.length !== 1)
|
|
65
85
|
throw new CliError(usage.trimEnd());
|
|
66
86
|
const configFolder = ctx.projectConfig?.config.plans?.folder;
|
|
67
|
-
if (values.folder !== undefined && configFolder !== undefined)
|
|
68
|
-
throw new CliError(".
|
|
87
|
+
if (values.folder !== undefined && configFolder !== undefined && values.folder !== configFolder)
|
|
88
|
+
throw new CliError(`--folder "${values.folder}" differs from plans.folder "${configFolder}" in .alignfirst.json.`);
|
|
69
89
|
const folder = values.folder ?? configFolder;
|
|
70
90
|
if (folder === undefined)
|
|
71
91
|
throw new CliError("Pass --folder <name> or set plans.folder in .alignfirst.json.");
|
|
@@ -81,32 +101,23 @@ function checkClone(ctx, cloneDir) {
|
|
|
81
101
|
}
|
|
82
102
|
function runCheck(ctx, args) {
|
|
83
103
|
const usage = `Usage: ${ctx.form} plans check\n`;
|
|
84
|
-
if (
|
|
104
|
+
if (parseBareCommandArgs(ctx, args, usage))
|
|
85
105
|
return 0;
|
|
86
106
|
const mode = resolvePlansMode(ctx.cwd, ctx.form);
|
|
107
|
+
if (mode.kind === "shared") {
|
|
108
|
+
const stopped = findStoppedRebase(mode.repoToplevel);
|
|
109
|
+
if (stopped !== undefined)
|
|
110
|
+
throw new CliError(renderStoppedRebase(stopped, ctx.form));
|
|
111
|
+
}
|
|
87
112
|
if (mode.kind === "shared")
|
|
88
113
|
ctx.stdout.write(".plans is linked to the team plans repository.\n");
|
|
89
114
|
else
|
|
90
115
|
ctx.stdout.write(".plans is a local directory (local plans mode): synchronization is disabled.\n");
|
|
91
116
|
return 0;
|
|
92
117
|
}
|
|
93
|
-
function handleBareHelp(ctx, args, usage) {
|
|
94
|
-
const { values, positionals } = parseCommandArgs(usage, () => parseArgs({
|
|
95
|
-
args,
|
|
96
|
-
options: { help: { type: "boolean", short: "h", default: false } },
|
|
97
|
-
strict: true,
|
|
98
|
-
allowPositionals: true,
|
|
99
|
-
}));
|
|
100
|
-
if (positionals.length > 0)
|
|
101
|
-
throw new CliError(`Unexpected argument: ${positionals[0]}\n\n${usage}`);
|
|
102
|
-
if (!values.help)
|
|
103
|
-
return false;
|
|
104
|
-
ctx.stdout.write(usage);
|
|
105
|
-
return true;
|
|
106
|
-
}
|
|
107
118
|
function runAutoArchive(ctx, args) {
|
|
108
119
|
const usage = `Usage: ${ctx.form} plans auto-archive\n`;
|
|
109
|
-
if (
|
|
120
|
+
if (parseBareCommandArgs(ctx, args, usage))
|
|
110
121
|
return 0;
|
|
111
122
|
const mode = resolvePlansMode(ctx.cwd, ctx.form);
|
|
112
123
|
const archived = autoArchive(join(ctx.cwd, ".plans"), archiveThresholdDays(ctx.env), ctx.stdout);
|