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
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
You need:
|
|
6
6
|
|
|
7
|
-
- the TASK_DIR — run `{{
|
|
8
|
-
- the CYCLE_LETTER and FILE_NUMBER — continue the current cycle: `{{
|
|
7
|
+
- the TASK_DIR — run `{{TICKET_CMD}}` (`{{CMD}} ticket --side` when there is no ticket)
|
|
8
|
+
- the CYCLE_LETTER and FILE_NUMBER — continue the current cycle: `{{TICKET_CMD}} --next AAD.summary.md` prints the file to create
|
|
9
9
|
|
|
10
10
|
Identify and state these values before starting the protocol.
|
|
11
11
|
|
|
@@ -54,7 +54,7 @@ Use subagents (your subagent tool) for distinct, isolated units of work when ben
|
|
|
54
54
|
|
|
55
55
|
Finalize the summary file: replace the working notes with the final content described below.
|
|
56
56
|
|
|
57
|
-
Start the summary with a header, then a suggested commit message
|
|
57
|
+
Start the summary with a header, then a suggested commit message {{COMMIT_RULE}}. The shorter the better. Omit any field with nothing to list. Always exclude `alignfirst` from skills.
|
|
58
58
|
|
|
59
59
|
Example:
|
|
60
60
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Pre-requisites
|
|
4
4
|
|
|
5
|
-
You need the TASK_DIR — run `{{
|
|
5
|
+
You need the TASK_DIR — run `{{TICKET_CMD}}` (`{{CMD}} ticket --side` when there is no ticket).
|
|
6
6
|
|
|
7
7
|
## Steps
|
|
8
8
|
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
You need:
|
|
6
6
|
|
|
7
|
-
- the TASK_DIR — run `{{
|
|
8
|
-
- the CYCLE_LETTER and FILE_NUMBER — start a new cycle: `{{
|
|
7
|
+
- the TASK_DIR — run `{{TICKET_CMD}}` (`{{CMD}} ticket --side` when there is no ticket)
|
|
8
|
+
- the CYCLE_LETTER and FILE_NUMBER — start a new cycle: `{{TICKET_CMD}} --next description.md --new-cycle` prints the file to create
|
|
9
9
|
|
|
10
10
|
Identify and state these values before starting the protocol.
|
|
11
11
|
|
|
@@ -29,7 +29,7 @@ Identify and state these values before starting the protocol.
|
|
|
29
29
|
[description body]
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
Start with a suggested commit message
|
|
32
|
+
Start with a suggested commit message {{COMMIT_RULE}}. Refine it from the suggested commit messages found in the specs and summaries you read. Keep it brief—usually 3-5 words for the description part. Shorter is better when it's clear.
|
|
33
33
|
|
|
34
34
|
## Guidelines for the Description Body
|
|
35
35
|
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
You need:
|
|
6
6
|
|
|
7
|
-
- the TASK_DIR — run `{{
|
|
8
|
-
- the CYCLE_LETTER and FILE_NUMBER — continue the current cycle: `{{
|
|
7
|
+
- the TASK_DIR — run `{{TICKET_CMD}}` (`{{CMD}} ticket --side` when there is no ticket)
|
|
8
|
+
- the CYCLE_LETTER and FILE_NUMBER — continue the current cycle: `{{TICKET_CMD}} --next merge.summary.md` prints the file to create
|
|
9
9
|
|
|
10
10
|
Identify and state these values before starting the protocol.
|
|
11
11
|
|
|
@@ -17,7 +17,7 @@ This protocol applies when a merge or rebase has produced conflicts, or when the
|
|
|
17
17
|
|
|
18
18
|
Run `git status` to check for conflicts.
|
|
19
19
|
|
|
20
|
-
**If there are no conflicts:** start the merge — use the incoming branch if the user provided one,
|
|
20
|
+
**If there are no conflicts:** start the merge — use the incoming branch if the user provided one, {{BASE_BRANCH_RULE}} If the merge completes cleanly, you are done — no summary file needed. Otherwise, continue with the steps below.
|
|
21
21
|
|
|
22
22
|
## 2. Investigate
|
|
23
23
|
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
You need:
|
|
8
8
|
|
|
9
|
-
- the TASK_DIR — run `{{
|
|
10
|
-
- the CYCLE_LETTER and FILE_NUMBER — continue the current cycle: `{{
|
|
9
|
+
- the TASK_DIR — run `{{TICKET_CMD}}` (`{{CMD}} ticket --side` when there is no ticket)
|
|
10
|
+
- the CYCLE_LETTER and FILE_NUMBER — continue the current cycle: `{{TICKET_CMD}} --next plan.md` or `{{TICKET_CMD}} --next main-plan.md` prints the file to create
|
|
11
11
|
- a **spec file** in the TASK_DIR
|
|
12
12
|
|
|
13
13
|
Identify and state these values before starting the protocol. If any of these pieces of information is missing, STOP AND ASK THE USER.
|
|
@@ -210,7 +210,7 @@ Note:
|
|
|
210
210
|
|
|
211
211
|
Write the plan file(s) according to the determined structure:
|
|
212
212
|
|
|
213
|
-
Use `{{
|
|
213
|
+
Use `{{TICKET_CMD}} --next plan.md`, `{{TICKET_CMD}} --next main-plan.md`, or `{{TICKET_CMD}} --next plan-<descriptor>.md` to get the next number. Run one command per file.
|
|
214
214
|
|
|
215
215
|
**Single Plan**:
|
|
216
216
|
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
You need:
|
|
6
6
|
|
|
7
|
-
- the TASK_DIR — run `{{
|
|
8
|
-
- the CYCLE_LETTER and FILE_NUMBER — start a new cycle: `{{
|
|
9
|
-
-
|
|
7
|
+
- the TASK_DIR — run `{{TICKET_CMD}}` (`{{CMD}} ticket --side` when there is no ticket)
|
|
8
|
+
- the CYCLE_LETTER and FILE_NUMBER — start a new cycle: `{{TICKET_CMD}} --next review.md --new-cycle` prints the file to create
|
|
9
|
+
- {{BASE_BRANCH_RULE}}
|
|
10
10
|
|
|
11
11
|
Identify and state these values before starting the protocol.
|
|
12
12
|
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
You need:
|
|
6
6
|
|
|
7
|
-
- the TASK_DIR — run `{{
|
|
8
|
-
- the CYCLE_LETTER and FILE_NUMBER — start a new cycle: `{{
|
|
7
|
+
- the TASK_DIR — run `{{TICKET_CMD}}` (`{{CMD}} ticket --side` when there is no ticket)
|
|
8
|
+
- the CYCLE_LETTER and FILE_NUMBER — start a new cycle: `{{TICKET_CMD}} --next spec.md --new-cycle` prints the file to create
|
|
9
9
|
|
|
10
10
|
Identify and state these values before starting the protocol.
|
|
11
11
|
|
|
@@ -51,7 +51,7 @@ Do not use your question tool. Always ask in plain text. Your questions will be
|
|
|
51
51
|
|
|
52
52
|
After the user approves your proposal, write the specification in a markdown file in TASK_DIR. Compose the filename with the current CYCLE_LETTER and the next FILE_NUMBER, e.g. `A1-spec.md`. Do not overwrite an existing file.
|
|
53
53
|
|
|
54
|
-
- After the title, include a suggested commit message
|
|
54
|
+
- After the title, include a suggested commit message {{COMMIT_RULE}}. The shorter the better. Then list the required documentation and skills. List each doc file individually — never a folder. Always exclude `alignfirst` from skills. Omit any field with nothing to list. Example:
|
|
55
55
|
|
|
56
56
|
```text
|
|
57
57
|
# [{TICKET_ID}] Short Title
|
|
@@ -1,36 +0,0 @@
|
|
|
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
|
-
}
|
package/dist/commands/setup.d.ts
DELETED
package/dist/commands/setup.js
DELETED
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmdirSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
2
|
-
import { basename, join } from "node:path";
|
|
3
|
-
import { parseArgs } from "node:util";
|
|
4
|
-
import { CliError } from "../cli-error.js";
|
|
5
|
-
import { assertMainWorktreeRoot, gitOutputOrUndefined, gitSucceeds } from "../git.js";
|
|
6
|
-
import { normalizeRemoteUrl } from "../overlay.js";
|
|
7
|
-
import { parseCommandArgs } from "../parse-args.js";
|
|
8
|
-
import { linkPlans } from "../plans/link.js";
|
|
9
|
-
import { PROJECT_CONFIG_FILENAME, readProjectConfig, validateProjectConfig, } from "../project-config.js";
|
|
10
|
-
import { installStubSkills } from "../skills.js";
|
|
11
|
-
import { defaultCliRange } from "../version-guard.js";
|
|
12
|
-
const ADOPT_FILES = ["AGENTS.md", "DEVELOPERS.md", "docs"];
|
|
13
|
-
export function runSetup(ctx, args) {
|
|
14
|
-
const usage = setupUsage(ctx);
|
|
15
|
-
const options = parseSetupArgs(ctx, args, usage);
|
|
16
|
-
if (options === undefined)
|
|
17
|
-
return 0;
|
|
18
|
-
if (options.overlay)
|
|
19
|
-
return runOverlaySetup(ctx, options);
|
|
20
|
-
if (options.adopt)
|
|
21
|
-
return runAdopt(ctx);
|
|
22
|
-
return runDefaultSetup(ctx, options);
|
|
23
|
-
}
|
|
24
|
-
function setupUsage(ctx) {
|
|
25
|
-
return `Usage:
|
|
26
|
-
${ctx.form} setup [--ticket-pattern <regex>] [--plans-folder <name>] [--port-range <first>-<last>] [--agent <name>]...
|
|
27
|
-
${ctx.form} setup --overlay [--plans-folder <name>] [--ticket-pattern <regex>] [--port-range <first>-<last>]
|
|
28
|
-
${ctx.form} setup --adopt
|
|
29
|
-
`;
|
|
30
|
-
}
|
|
31
|
-
function parseSetupArgs(ctx, args, usage) {
|
|
32
|
-
const { values } = parseCommandArgs(usage, () => parseArgs({
|
|
33
|
-
args,
|
|
34
|
-
options: {
|
|
35
|
-
"ticket-pattern": { type: "string" },
|
|
36
|
-
"plans-folder": { type: "string" },
|
|
37
|
-
"port-range": { type: "string" },
|
|
38
|
-
agent: { type: "string", multiple: true, default: [] },
|
|
39
|
-
overlay: { type: "boolean", default: false },
|
|
40
|
-
adopt: { type: "boolean", default: false },
|
|
41
|
-
help: { type: "boolean", short: "h", default: false },
|
|
42
|
-
},
|
|
43
|
-
strict: true,
|
|
44
|
-
}));
|
|
45
|
-
if (values.help) {
|
|
46
|
-
ctx.stdout.write(usage);
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
if (values.overlay && values.adopt)
|
|
50
|
-
throw new CliError(`--overlay and --adopt are mutually exclusive.\n\n${usage}`);
|
|
51
|
-
if ((values.overlay || values.adopt) && values.agent.length > 0)
|
|
52
|
-
throw new CliError(`--agent is available only in the default setup mode.\n\n${usage}`);
|
|
53
|
-
const portRange = values["port-range"] === undefined ? undefined : parsePortRange(values["port-range"], usage);
|
|
54
|
-
const options = {
|
|
55
|
-
ticketPattern: values["ticket-pattern"],
|
|
56
|
-
plansFolder: values["plans-folder"],
|
|
57
|
-
portRange,
|
|
58
|
-
agents: values.agent,
|
|
59
|
-
overlay: values.overlay,
|
|
60
|
-
adopt: values.adopt,
|
|
61
|
-
};
|
|
62
|
-
validateSetupOptions(options);
|
|
63
|
-
return options;
|
|
64
|
-
}
|
|
65
|
-
function parsePortRange(value, usage) {
|
|
66
|
-
const match = /^(\d+)-(\d+)$/.exec(value);
|
|
67
|
-
if (!match)
|
|
68
|
-
throw new CliError(`--port-range must be <first>-<last>.\n\n${usage}`);
|
|
69
|
-
return { first: Number(match[1]), last: Number(match[2]) };
|
|
70
|
-
}
|
|
71
|
-
function validateSetupOptions(options) {
|
|
72
|
-
validateProjectConfig({
|
|
73
|
-
schemaVersion: 1,
|
|
74
|
-
...(options.ticketPattern === undefined ? {} : { ticketPattern: options.ticketPattern }),
|
|
75
|
-
...(options.plansFolder === undefined ? {} : { plans: { folder: options.plansFolder } }),
|
|
76
|
-
...(options.portRange === undefined ? {} : { portRange: options.portRange }),
|
|
77
|
-
}, PROJECT_CONFIG_FILENAME);
|
|
78
|
-
}
|
|
79
|
-
function runDefaultSetup(ctx, options) {
|
|
80
|
-
assertMainWorktreeRoot(ctx.cwd);
|
|
81
|
-
setupProjectConfig(ctx, options);
|
|
82
|
-
setupPlansDirectory(ctx);
|
|
83
|
-
installStubSkills(ctx, options.agents);
|
|
84
|
-
ctx.stdout.write("Installed the AlignFirst skills globally.\n");
|
|
85
|
-
setupReadme(ctx);
|
|
86
|
-
return 0;
|
|
87
|
-
}
|
|
88
|
-
function setupProjectConfig(ctx, options) {
|
|
89
|
-
const path = join(ctx.cwd, PROJECT_CONFIG_FILENAME);
|
|
90
|
-
if (existsSync(path)) {
|
|
91
|
-
readProjectConfig(ctx.cwd);
|
|
92
|
-
ctx.stdout.write(`${PROJECT_CONFIG_FILENAME} is valid.\n`);
|
|
93
|
-
if (hasProjectOptions(options))
|
|
94
|
-
throw new CliError(`${PROJECT_CONFIG_FILENAME} exists; edit it instead of passing options.`);
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
const cli = defaultCliRange(ctx.version);
|
|
98
|
-
const config = validateProjectConfig({
|
|
99
|
-
schemaVersion: 1,
|
|
100
|
-
cli,
|
|
101
|
-
...(options.ticketPattern === undefined ? {} : { ticketPattern: options.ticketPattern }),
|
|
102
|
-
...(options.plansFolder === undefined ? {} : { plans: { folder: options.plansFolder } }),
|
|
103
|
-
...(options.portRange === undefined ? {} : { portRange: options.portRange }),
|
|
104
|
-
}, PROJECT_CONFIG_FILENAME);
|
|
105
|
-
writeJson(path, config);
|
|
106
|
-
ctx.stdout.write(`Created ${PROJECT_CONFIG_FILENAME} (cli ${cli})\n`);
|
|
107
|
-
}
|
|
108
|
-
function hasProjectOptions(options) {
|
|
109
|
-
return (options.ticketPattern !== undefined ||
|
|
110
|
-
options.plansFolder !== undefined ||
|
|
111
|
-
options.portRange !== undefined);
|
|
112
|
-
}
|
|
113
|
-
function setupPlansDirectory(ctx) {
|
|
114
|
-
const plansPath = join(ctx.cwd, ".plans");
|
|
115
|
-
if (!existsSync(plansPath)) {
|
|
116
|
-
mkdirSync(plansPath);
|
|
117
|
-
ctx.stdout.write("Created .plans/\n");
|
|
118
|
-
}
|
|
119
|
-
if (gitSucceeds(ctx.cwd, "check-ignore", "-q", ".plans"))
|
|
120
|
-
return;
|
|
121
|
-
appendLine(join(ctx.cwd, ".gitignore"), ".plans");
|
|
122
|
-
ctx.stdout.write("Added .plans to .gitignore.\n");
|
|
123
|
-
}
|
|
124
|
-
function appendLine(path, line) {
|
|
125
|
-
const content = existsSync(path) ? readFileSync(path, "utf-8") : "";
|
|
126
|
-
const separator = content === "" || content.endsWith("\n") ? "" : "\n";
|
|
127
|
-
appendFileSync(path, `${separator}${line}\n`);
|
|
128
|
-
}
|
|
129
|
-
function setupReadme(ctx) {
|
|
130
|
-
const path = join(ctx.cwd, "README.md");
|
|
131
|
-
if (!existsSync(path))
|
|
132
|
-
return;
|
|
133
|
-
const content = readFileSync(path, "utf-8");
|
|
134
|
-
if (/alignfirst/i.test(content))
|
|
135
|
-
return;
|
|
136
|
-
appendFileSync(path, "\n## Prerequisites\n\nInstall the AlignFirst CLI: `npm install -g alignfirst`.\n");
|
|
137
|
-
ctx.stdout.write("Added the CLI prerequisite to README.md.\n");
|
|
138
|
-
}
|
|
139
|
-
function runOverlaySetup(ctx, options) {
|
|
140
|
-
const overlaysDir = resolveOverlaysDir(ctx);
|
|
141
|
-
assertMainWorktreeRoot(ctx.cwd);
|
|
142
|
-
const projectPath = realpathSync(ctx.cwd);
|
|
143
|
-
const name = options.plansFolder ?? basename(projectPath);
|
|
144
|
-
const overlayDir = join(overlaysDir, name, "_project");
|
|
145
|
-
if (existsSync(overlayDir))
|
|
146
|
-
throw new CliError(`${overlayDir} already exists.`);
|
|
147
|
-
const config = buildOverlayConfig(ctx, options, projectPath);
|
|
148
|
-
mkdirSync(overlayDir, { recursive: true });
|
|
149
|
-
writeJson(join(overlayDir, PROJECT_CONFIG_FILENAME), config);
|
|
150
|
-
ctx.stdout.write(`Created overlay: ${overlayDir}\n`);
|
|
151
|
-
setupOverlayPlans(ctx, overlaysDir, name);
|
|
152
|
-
setupGitExclude(ctx);
|
|
153
|
-
return 0;
|
|
154
|
-
}
|
|
155
|
-
function resolveOverlaysDir(ctx) {
|
|
156
|
-
const value = ctx.env.ALIGNFIRST_OVERLAYS;
|
|
157
|
-
if (value === undefined || value === "")
|
|
158
|
-
throw new CliError("ALIGNFIRST_OVERLAYS is not set.");
|
|
159
|
-
return value.startsWith("~/") ? join(ctx.home, value.slice(2)) : value;
|
|
160
|
-
}
|
|
161
|
-
function buildOverlayConfig(ctx, options, projectPath) {
|
|
162
|
-
const origin = gitOutputOrUndefined(ctx.cwd, "remote", "get-url", "origin");
|
|
163
|
-
return validateProjectConfig({
|
|
164
|
-
schemaVersion: 1,
|
|
165
|
-
project: {
|
|
166
|
-
...(origin === undefined || origin === "" ? {} : { remote: normalizeRemoteUrl(origin) }),
|
|
167
|
-
paths: [projectPath],
|
|
168
|
-
},
|
|
169
|
-
...(options.ticketPattern === undefined ? {} : { ticketPattern: options.ticketPattern }),
|
|
170
|
-
...(options.plansFolder === undefined ? {} : { plans: { folder: options.plansFolder } }),
|
|
171
|
-
...(options.portRange === undefined ? {} : { portRange: options.portRange }),
|
|
172
|
-
}, PROJECT_CONFIG_FILENAME);
|
|
173
|
-
}
|
|
174
|
-
function setupOverlayPlans(ctx, overlaysDir, name) {
|
|
175
|
-
if (gitSucceeds(overlaysDir, "rev-parse", "--git-dir")) {
|
|
176
|
-
linkPlans(ctx, join(overlaysDir, name));
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
const plansPath = join(ctx.cwd, ".plans");
|
|
180
|
-
if (!existsSync(plansPath))
|
|
181
|
-
mkdirSync(plansPath);
|
|
182
|
-
ctx.stdout.write("Using a local .plans directory.\n");
|
|
183
|
-
}
|
|
184
|
-
function setupGitExclude(ctx) {
|
|
185
|
-
if (gitSucceeds(ctx.cwd, "check-ignore", "-q", ".plans"))
|
|
186
|
-
return;
|
|
187
|
-
appendLine(join(ctx.cwd, ".git", "info", "exclude"), ".plans");
|
|
188
|
-
ctx.stdout.write("Added .plans to .git/info/exclude.\n");
|
|
189
|
-
}
|
|
190
|
-
function runAdopt(ctx) {
|
|
191
|
-
const overlay = ctx.overlay;
|
|
192
|
-
if (overlay === undefined) {
|
|
193
|
-
const value = ctx.env.ALIGNFIRST_OVERLAYS;
|
|
194
|
-
throw new CliError(`No overlay matches this repository (ALIGNFIRST_OVERLAYS=${value === undefined || value === "" ? "unset" : value}).`);
|
|
195
|
-
}
|
|
196
|
-
adoptConfig(ctx, overlay.dir, overlay.config);
|
|
197
|
-
const agentsConflict = adoptFiles(ctx, overlay.dir);
|
|
198
|
-
removePlansExclude(ctx);
|
|
199
|
-
if (readdirSync(overlay.dir).length === 0)
|
|
200
|
-
rmdirSync(overlay.dir);
|
|
201
|
-
else
|
|
202
|
-
ctx.stdout.write(`Overlay remains: ${overlay.dir}\n`);
|
|
203
|
-
ctx.stdout.write("Next: add .plans to .gitignore.\n");
|
|
204
|
-
if (agentsConflict)
|
|
205
|
-
ctx.stdout.write("Next: merge the overlay AGENTS.md conventions by hand.\n");
|
|
206
|
-
return 0;
|
|
207
|
-
}
|
|
208
|
-
function adoptConfig(ctx, overlayDir, config) {
|
|
209
|
-
const name = PROJECT_CONFIG_FILENAME;
|
|
210
|
-
const source = join(overlayDir, name);
|
|
211
|
-
const target = join(ctx.cwd, name);
|
|
212
|
-
if (existsSync(target)) {
|
|
213
|
-
ctx.stdout.write(`kept in the overlay: ${name} (the root has its own)\n`);
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
const rootConfig = structuredClone(config);
|
|
217
|
-
delete rootConfig.project;
|
|
218
|
-
writeJson(target, validateProjectConfig(rootConfig, name));
|
|
219
|
-
unlinkSync(source);
|
|
220
|
-
ctx.stdout.write(`Adopted ${name}.\n`);
|
|
221
|
-
}
|
|
222
|
-
function adoptFiles(ctx, overlayDir) {
|
|
223
|
-
let agentsConflict = false;
|
|
224
|
-
for (const name of ADOPT_FILES) {
|
|
225
|
-
const source = join(overlayDir, name);
|
|
226
|
-
if (!existsSync(source))
|
|
227
|
-
continue;
|
|
228
|
-
const target = join(ctx.cwd, name);
|
|
229
|
-
if (existsSync(target)) {
|
|
230
|
-
ctx.stdout.write(`kept in the overlay: ${name} (the root has its own)\n`);
|
|
231
|
-
if (name === "AGENTS.md")
|
|
232
|
-
agentsConflict = true;
|
|
233
|
-
continue;
|
|
234
|
-
}
|
|
235
|
-
renameSync(source, target);
|
|
236
|
-
ctx.stdout.write(`Adopted ${name}.\n`);
|
|
237
|
-
}
|
|
238
|
-
return agentsConflict;
|
|
239
|
-
}
|
|
240
|
-
function removePlansExclude(ctx) {
|
|
241
|
-
const path = join(ctx.cwd, ".git", "info", "exclude");
|
|
242
|
-
if (!existsSync(path))
|
|
243
|
-
return;
|
|
244
|
-
const content = readFileSync(path, "utf-8");
|
|
245
|
-
const lines = content.split(/\r?\n/);
|
|
246
|
-
const filtered = lines.filter((line) => line.trim() !== ".plans");
|
|
247
|
-
if (filtered.length === lines.length)
|
|
248
|
-
return;
|
|
249
|
-
writeFileSync(path, filtered.join("\n"));
|
|
250
|
-
ctx.stdout.write("Removed .plans from .git/info/exclude.\n");
|
|
251
|
-
}
|
|
252
|
-
function writeJson(path, value) {
|
|
253
|
-
writeFileSync(path, `${JSON.stringify(value, undefined, 2)}\n`);
|
|
254
|
-
}
|
package/dist/overlay.d.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { type ProjectConfig } from "./project-config.js";
|
|
2
|
-
export interface Overlay {
|
|
3
|
-
dir: string;
|
|
4
|
-
config: ProjectConfig;
|
|
5
|
-
matchedBy: "remote" | "paths";
|
|
6
|
-
}
|
|
7
|
-
export interface ProjectFile {
|
|
8
|
-
path: string;
|
|
9
|
-
source: "root" | "overlay";
|
|
10
|
-
}
|
|
11
|
-
export interface ResolvedProjectConfig {
|
|
12
|
-
config: ProjectConfig;
|
|
13
|
-
source: "root" | "overlay";
|
|
14
|
-
overlay?: Overlay;
|
|
15
|
-
}
|
|
16
|
-
export declare function findOverlay(cwd: string, env: NodeJS.ProcessEnv, home: string): Overlay | undefined;
|
|
17
|
-
export declare function normalizeRemoteUrl(url: string): string;
|
|
18
|
-
export declare function resolveProjectFile(cwd: string, overlay: Overlay | undefined, name: string): ProjectFile | undefined;
|
|
19
|
-
export declare function resolveProjectConfig(cwd: string, env: NodeJS.ProcessEnv, home: string): ResolvedProjectConfig | undefined;
|
package/dist/overlay.js
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { existsSync, readdirSync, realpathSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { CliError } from "./cli-error.js";
|
|
4
|
-
import { gitOutputOrUndefined } from "./git.js";
|
|
5
|
-
import { readProjectConfig } from "./project-config.js";
|
|
6
|
-
export function findOverlay(cwd, env, home) {
|
|
7
|
-
const configuredDir = env.ALIGNFIRST_OVERLAYS;
|
|
8
|
-
if (configuredDir === undefined || configuredDir === "")
|
|
9
|
-
return;
|
|
10
|
-
const overlaysDir = expandHome(configuredDir, home);
|
|
11
|
-
const candidates = readOverlayCandidates(overlaysDir);
|
|
12
|
-
const origin = gitOutputOrUndefined(cwd, "remote", "get-url", "origin");
|
|
13
|
-
const normalizedOrigin = origin === undefined || origin === "" ? undefined : normalizeRemoteUrl(origin);
|
|
14
|
-
const realCwd = realpathSync(cwd);
|
|
15
|
-
const remoteMatches = candidates.filter(({ config }) => normalizedOrigin !== undefined && config.project?.remote === normalizedOrigin);
|
|
16
|
-
if (remoteMatches.length > 0)
|
|
17
|
-
return selectOverlay(remoteMatches, "remote");
|
|
18
|
-
const pathMatches = candidates.filter(({ config }) => config.project?.paths?.includes(realCwd));
|
|
19
|
-
if (pathMatches.length > 0)
|
|
20
|
-
return selectOverlay(pathMatches, "paths");
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
function expandHome(path, home) {
|
|
24
|
-
return path.startsWith("~/") ? join(home, path.slice(2)) : path;
|
|
25
|
-
}
|
|
26
|
-
function readOverlayCandidates(overlaysDir) {
|
|
27
|
-
if (!existsSync(overlaysDir))
|
|
28
|
-
return [];
|
|
29
|
-
return readdirSync(overlaysDir, { withFileTypes: true })
|
|
30
|
-
.filter((entry) => entry.isDirectory())
|
|
31
|
-
.flatMap((entry) => {
|
|
32
|
-
const dir = join(overlaysDir, entry.name, "_project");
|
|
33
|
-
const config = readProjectConfig(dir);
|
|
34
|
-
return config === undefined ? [] : [{ dir, config }];
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
function selectOverlay(candidates, matchedBy) {
|
|
38
|
-
if (candidates.length > 1)
|
|
39
|
-
throw new CliError(`Multiple AlignFirst overlays match this project: ${candidates
|
|
40
|
-
.map(({ dir }) => dir)
|
|
41
|
-
.join(", ")}`);
|
|
42
|
-
const candidate = candidates[0];
|
|
43
|
-
return { ...candidate, matchedBy };
|
|
44
|
-
}
|
|
45
|
-
export function normalizeRemoteUrl(url) {
|
|
46
|
-
const value = url
|
|
47
|
-
.trim()
|
|
48
|
-
.replace(/\/+$/, "")
|
|
49
|
-
.replace(/\.git$/, "");
|
|
50
|
-
const scpMatch = value.includes("://") ? null : /^(?:[^@]+@)?([^:/]+):(.+)$/.exec(value);
|
|
51
|
-
if (scpMatch)
|
|
52
|
-
return `${scpMatch[1].toLowerCase()}/${scpMatch[2].replace(/^\/+/, "")}`;
|
|
53
|
-
try {
|
|
54
|
-
const parsed = new URL(value.includes("://") ? value : `https://${value}`);
|
|
55
|
-
return `${parsed.hostname.toLowerCase()}${parsed.pathname}`
|
|
56
|
-
.replace(/\/+$/, "")
|
|
57
|
-
.replace(/\.git$/, "")
|
|
58
|
-
.replace(/^\/+/, "");
|
|
59
|
-
}
|
|
60
|
-
catch {
|
|
61
|
-
return value;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
export function resolveProjectFile(cwd, overlay, name) {
|
|
65
|
-
const rootPath = join(cwd, name);
|
|
66
|
-
if (existsSync(rootPath))
|
|
67
|
-
return { path: rootPath, source: "root" };
|
|
68
|
-
if (overlay === undefined)
|
|
69
|
-
return;
|
|
70
|
-
const overlayPath = join(overlay.dir, name);
|
|
71
|
-
if (existsSync(overlayPath))
|
|
72
|
-
return { path: overlayPath, source: "overlay" };
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
export function resolveProjectConfig(cwd, env, home) {
|
|
76
|
-
const overlay = findOverlay(cwd, env, home);
|
|
77
|
-
const rootConfig = readProjectConfig(cwd);
|
|
78
|
-
if (rootConfig !== undefined)
|
|
79
|
-
return { config: rootConfig, source: "root", overlay };
|
|
80
|
-
if (overlay !== undefined)
|
|
81
|
-
return { config: overlay.config, source: "overlay", overlay };
|
|
82
|
-
return;
|
|
83
|
-
}
|