alignfirst 0.1.0-beta.0 → 0.1.0-beta.1
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 +2 -2
- 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 +1 -1
- 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
|
@@ -21,9 +21,9 @@ Commands:
|
|
|
21
21
|
- `sync` — Synchronize shared plans.
|
|
22
22
|
- `plans` — Set up, check and archive plans.
|
|
23
23
|
- `docmap` — Browse project documentation.
|
|
24
|
+
- `conventions` — Print the effective project conventions.
|
|
25
|
+
- `context` — Print the conventions and the documentation map.
|
|
24
26
|
- `config` — Report the effective project configuration.
|
|
25
|
-
- `DEVELOPERS.md` — Print the project developer guide.
|
|
26
|
-
- `setup` — Prepare an AlignFirst project.
|
|
27
27
|
- `doctor` — Diagnose an AlignFirst setup.
|
|
28
28
|
|
|
29
29
|
Run `alignfirst --help` for command usage or `alignfirst guide` for the collaboration guide.
|
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);
|
package/dist/commands/sync.js
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { parseArgs } from "node:util";
|
|
3
|
+
import { CliError } from "../cli-error.js";
|
|
3
4
|
import { git, gitOutput, gitSucceeds } from "../git.js";
|
|
4
5
|
import { parseCommandArgs } from "../parse-args.js";
|
|
5
6
|
import { archiveThresholdDays, autoArchive } from "../plans/archive.js";
|
|
6
7
|
import { resolvePlansMode } from "../plans/mode.js";
|
|
8
|
+
import { findStoppedRebase, renderStoppedRebase } from "../plans/rebase.js";
|
|
7
9
|
export function runSync(ctx, args) {
|
|
8
|
-
const usage = `Usage: ${ctx.form} sync [--auto-archive]\n`;
|
|
10
|
+
const usage = `Usage: ${ctx.form} sync [--auto-archive | --no-auto-archive]\n`;
|
|
9
11
|
const options = parseSyncArgs(ctx, args, usage);
|
|
10
12
|
if (options === undefined)
|
|
11
13
|
return 0;
|
|
12
|
-
const
|
|
14
|
+
const enabled = options.autoArchive === true
|
|
15
|
+
? true
|
|
16
|
+
: options.noAutoArchive === true
|
|
17
|
+
? false
|
|
18
|
+
: (ctx.projectConfig?.config.plans?.autoArchive ?? false);
|
|
19
|
+
const thresholdDays = enabled ? archiveThresholdDays(ctx.env) : undefined;
|
|
13
20
|
const mode = resolvePlansMode(ctx.cwd, ctx.form);
|
|
14
21
|
const plansDir = join(ctx.cwd, ".plans");
|
|
15
22
|
if (mode.kind === "local") {
|
|
@@ -19,15 +26,31 @@ export function runSync(ctx, args) {
|
|
|
19
26
|
return 0;
|
|
20
27
|
}
|
|
21
28
|
const repoDir = mode.repoToplevel;
|
|
22
|
-
|
|
23
|
-
git(repoDir, "pull", "--rebase", "--autostash");
|
|
24
|
-
if (thresholdDays !== undefined)
|
|
25
|
-
autoArchive(plansDir, thresholdDays, ctx.stdout);
|
|
29
|
+
assertNoStoppedRebase(repoDir, ctx.form);
|
|
26
30
|
git(repoDir, "add", "-A");
|
|
27
31
|
if (hasStagedChanges(repoDir))
|
|
28
32
|
git(repoDir, "commit", "--quiet", "-m", "sync");
|
|
29
|
-
if (
|
|
30
|
-
|
|
33
|
+
if (hasUpstream(repoDir)) {
|
|
34
|
+
try {
|
|
35
|
+
git(repoDir, "pull", "--rebase");
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
assertNoStoppedRebase(repoDir, ctx.form);
|
|
39
|
+
throw new CliError("git pull failed. See the git output above.");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (thresholdDays !== undefined && autoArchive(plansDir, thresholdDays, ctx.stdout)) {
|
|
43
|
+
git(repoDir, "add", "-A");
|
|
44
|
+
if (hasStagedChanges(repoDir))
|
|
45
|
+
git(repoDir, "commit", "--quiet", "-m", "sync");
|
|
46
|
+
}
|
|
47
|
+
if (hasCommitsToSend(repoDir)) {
|
|
48
|
+
try {
|
|
49
|
+
git(repoDir, "push", "--quiet", "-u", "origin", "HEAD");
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
throw new CliError(`git push failed. See the git output above. Another synchronization may have landed first: run ${ctx.form} sync again.`);
|
|
53
|
+
}
|
|
31
54
|
ctx.stdout.write("Plans synchronized: local changes sent.\n");
|
|
32
55
|
}
|
|
33
56
|
else {
|
|
@@ -40,6 +63,7 @@ function parseSyncArgs(ctx, args, usage) {
|
|
|
40
63
|
args,
|
|
41
64
|
options: {
|
|
42
65
|
"auto-archive": { type: "boolean", default: false },
|
|
66
|
+
"no-auto-archive": { type: "boolean", default: false },
|
|
43
67
|
help: { type: "boolean", short: "h", default: false },
|
|
44
68
|
},
|
|
45
69
|
strict: true,
|
|
@@ -48,15 +72,27 @@ function parseSyncArgs(ctx, args, usage) {
|
|
|
48
72
|
ctx.stdout.write(usage);
|
|
49
73
|
return;
|
|
50
74
|
}
|
|
51
|
-
|
|
75
|
+
if (values["auto-archive"] && values["no-auto-archive"])
|
|
76
|
+
throw new CliError(`--auto-archive and --no-auto-archive are mutually exclusive.\n\n${usage}`);
|
|
77
|
+
return {
|
|
78
|
+
autoArchive: values["auto-archive"],
|
|
79
|
+
noAutoArchive: values["no-auto-archive"],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function assertNoStoppedRebase(repoDir, form) {
|
|
83
|
+
const stopped = findStoppedRebase(repoDir);
|
|
84
|
+
if (stopped !== undefined)
|
|
85
|
+
throw new CliError(renderStoppedRebase(stopped, form));
|
|
52
86
|
}
|
|
53
|
-
function
|
|
54
|
-
return gitSucceeds(dir, "rev-parse", "--verify", "-q", "
|
|
87
|
+
function hasUpstream(dir) {
|
|
88
|
+
return gitSucceeds(dir, "rev-parse", "--verify", "-q", "@{u}");
|
|
55
89
|
}
|
|
56
90
|
function hasStagedChanges(dir) {
|
|
57
91
|
return !gitSucceeds(dir, "diff", "--cached", "--quiet");
|
|
58
92
|
}
|
|
59
93
|
function hasCommitsToSend(dir) {
|
|
94
|
+
if (!gitSucceeds(dir, "rev-parse", "--verify", "-q", "HEAD"))
|
|
95
|
+
return false;
|
|
60
96
|
if (!gitSucceeds(dir, "rev-parse", "--verify", "-q", "@{u}"))
|
|
61
97
|
return true;
|
|
62
98
|
return gitOutput(dir, "rev-list", "--count", "@{u}..HEAD") !== "0";
|