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/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";
|
package/dist/commands/ticket.js
CHANGED
|
@@ -15,7 +15,9 @@ export function runTicket(ctx, args) {
|
|
|
15
15
|
if (parsed === undefined)
|
|
16
16
|
return 0;
|
|
17
17
|
const result = resolveTicket(ctx, parsed);
|
|
18
|
-
const next = parsed.next === undefined
|
|
18
|
+
const next = parsed.next === undefined
|
|
19
|
+
? undefined
|
|
20
|
+
: nextFileName(result.entries, parsed.next, parsed.newCycle);
|
|
19
21
|
if (parsed.json)
|
|
20
22
|
ctx.stdout.write(`${JSON.stringify(jsonReport(ctx, parsed, result, next), undefined, 2)}\n`);
|
|
21
23
|
else
|
|
@@ -49,6 +51,8 @@ function parseTicketArgs(ctx, args, usage) {
|
|
|
49
51
|
throw new CliError(`A ticket id cannot be combined with --side.\n\n${usage}`);
|
|
50
52
|
if (values["new-cycle"] && values.next === undefined)
|
|
51
53
|
throw new CliError(`--new-cycle requires --next.\n\n${usage}`);
|
|
54
|
+
if (values.next !== undefined)
|
|
55
|
+
validateNextFilename(values.next);
|
|
52
56
|
const resolution = resolveTicketId(ctx, positionals[0], values.side, values["dry-run"]);
|
|
53
57
|
return {
|
|
54
58
|
...resolution,
|
|
@@ -59,8 +63,13 @@ function parseTicketArgs(ctx, args, usage) {
|
|
|
59
63
|
side: values.side,
|
|
60
64
|
};
|
|
61
65
|
}
|
|
66
|
+
function validateNextFilename(filename) {
|
|
67
|
+
if (filename.length === 0 || filename === "." || filename === ".." || /[\\/]/u.test(filename)) {
|
|
68
|
+
throw new CliError("--next must be a non-empty single path segment.");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
62
71
|
function resolveTicketId(ctx, positional, side, dryRun) {
|
|
63
|
-
const pattern = ctx.projectConfig?.config.
|
|
72
|
+
const pattern = ctx.projectConfig?.config.ticketIdPattern;
|
|
64
73
|
if (positional !== undefined) {
|
|
65
74
|
validateTicketId(positional, pattern);
|
|
66
75
|
return { id: positional };
|
|
@@ -68,7 +77,7 @@ function resolveTicketId(ctx, positional, side, dryRun) {
|
|
|
68
77
|
if (side)
|
|
69
78
|
return { id: dryRun ? peekSideTicket(ctx.cwd) : reserveSideTicket(ctx.cwd) };
|
|
70
79
|
if (pattern === undefined)
|
|
71
|
-
throw new CliError("No ticket id given
|
|
80
|
+
throw new CliError("No ticket id given. Pass it.\nSetting ticketIdPattern in .alignfirst.json enables deduction from the current branch.");
|
|
72
81
|
const deduced = deduceTicketFromBranch(ctx.cwd, pattern);
|
|
73
82
|
validateTicketId(deduced.id, pattern);
|
|
74
83
|
return deduced;
|
package/dist/context.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ResolvedProjectConfig } from "./project-config.js";
|
|
2
2
|
export interface Output {
|
|
3
3
|
write(text: string): void;
|
|
4
4
|
}
|
|
@@ -11,5 +11,4 @@ export interface CommandContext {
|
|
|
11
11
|
form: string;
|
|
12
12
|
version: string;
|
|
13
13
|
projectConfig?: ResolvedProjectConfig;
|
|
14
|
-
overlay?: Overlay;
|
|
15
14
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CommandContext } from "./context.js";
|
|
2
|
+
import type { CommitConfig } from "./project-config.js";
|
|
3
|
+
export interface CommitSubject {
|
|
4
|
+
subject: string;
|
|
5
|
+
side?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function renderConventions(ctx: CommandContext): string;
|
|
8
|
+
export declare function commitSubject(commit: CommitConfig): CommitSubject;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { existsSync, lstatSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { renderDefaultBranchLine, resolveDefaultBranch } from "./default-branch.js";
|
|
4
|
+
import { gitOutputOrUndefined, gitSucceeds } from "./git.js";
|
|
5
|
+
import { resolvePlansMode } from "./plans/mode.js";
|
|
6
|
+
export function renderConventions(ctx) {
|
|
7
|
+
const lines = [
|
|
8
|
+
renderTicketIds(ctx),
|
|
9
|
+
renderBranchNames(ctx),
|
|
10
|
+
renderCommits(ctx),
|
|
11
|
+
renderDefaultBranchLine(resolveDefaultBranch(ctx.cwd, ctx.projectConfig?.config)),
|
|
12
|
+
renderPlans(ctx),
|
|
13
|
+
renderSearches(ctx),
|
|
14
|
+
].filter((line) => line !== undefined);
|
|
15
|
+
return `${lines.join("\n")}\n`;
|
|
16
|
+
}
|
|
17
|
+
function renderTicketIds(ctx) {
|
|
18
|
+
const pattern = ctx.projectConfig?.config.ticketIdPattern;
|
|
19
|
+
if (pattern === undefined)
|
|
20
|
+
return "Ticket IDs: no configured format; ask the user for the ID. Without an external ticket, use the next `side-N`.";
|
|
21
|
+
return `Ticket IDs: \`${pattern}\`; infer a matching ID from the branch. Without an external ticket, use the next \`side-N\`.`;
|
|
22
|
+
}
|
|
23
|
+
function renderBranchNames(ctx) {
|
|
24
|
+
const template = ctx.projectConfig?.config.git?.branchNameTemplate;
|
|
25
|
+
return template === undefined ? undefined : `Branch names: \`${template}\`.`;
|
|
26
|
+
}
|
|
27
|
+
function renderCommits(ctx) {
|
|
28
|
+
const git = ctx.projectConfig?.config.git;
|
|
29
|
+
if (git?.commit === undefined) {
|
|
30
|
+
return git?.agentCoauthoring === false
|
|
31
|
+
? "Commits: do not add an agent co-author trailer."
|
|
32
|
+
: undefined;
|
|
33
|
+
}
|
|
34
|
+
const { subject, side } = commitSubject(git.commit);
|
|
35
|
+
const rule = side === undefined ? `${subject}.` : `${subject}; use ${side}.`;
|
|
36
|
+
const coauthor = git.agentCoauthoring === false ? " Do not add an agent co-author trailer." : "";
|
|
37
|
+
return `Commits: ${rule}${coauthor}`;
|
|
38
|
+
}
|
|
39
|
+
export function commitSubject(commit) {
|
|
40
|
+
if (commit.ticketReference === "bracketed")
|
|
41
|
+
return { subject: "`type: [ticketId] summary`", side: "`type: summary` for `side-N`" };
|
|
42
|
+
if (commit.ticketReference === "bracketedHash")
|
|
43
|
+
return { subject: "`type: [#ticketId] summary`", side: "`type: summary` for `side-N`" };
|
|
44
|
+
return { subject: "`type: summary`" };
|
|
45
|
+
}
|
|
46
|
+
function renderPlans(ctx) {
|
|
47
|
+
if (!plansEntryExists(ctx.cwd))
|
|
48
|
+
return;
|
|
49
|
+
try {
|
|
50
|
+
const mode = resolvePlansMode(ctx.cwd, ctx.form);
|
|
51
|
+
const folder = ctx.projectConfig?.config.plans?.folder;
|
|
52
|
+
const sharedFolder = folder === undefined ? "" : ` (shared folder \`${folder}\`)`;
|
|
53
|
+
const base = mode.kind === "shared"
|
|
54
|
+
? `Plans: use \`.plans\`${sharedFolder}; keep it out of product commits, and run \`${ctx.form} sync\` after changes.`
|
|
55
|
+
: "Plans: use `.plans`; keep it out of product commits.";
|
|
56
|
+
const archival = ctx.projectConfig?.config.plans?.autoArchive === true
|
|
57
|
+
? " Automatic archival is enabled."
|
|
58
|
+
: "";
|
|
59
|
+
return `${base}${archival}`;
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
63
|
+
return `Plans: ${message.split("\n", 1)[0]}`;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function plansEntryExists(cwd) {
|
|
67
|
+
return lstatSync(join(cwd, ".plans"), { throwIfNoEntry: false }) !== undefined;
|
|
68
|
+
}
|
|
69
|
+
function renderSearches(ctx) {
|
|
70
|
+
const directories = [
|
|
71
|
+
...(plansEntryExists(ctx.cwd) ? [".plans"] : []),
|
|
72
|
+
...[".local", ".local-wt"].filter((name) => qualifiesIgnoredDirectory(ctx.cwd, name)),
|
|
73
|
+
];
|
|
74
|
+
if (directories.length === 0)
|
|
75
|
+
return;
|
|
76
|
+
const rendered = directories.map((name) => `\`${name}\``);
|
|
77
|
+
const joined = rendered.length === 1
|
|
78
|
+
? rendered[0]
|
|
79
|
+
: `${rendered.slice(0, -1).join(", ")} and ${rendered.at(-1)}`;
|
|
80
|
+
return `Searches: exclude ${joined} from broad codebase searches.`;
|
|
81
|
+
}
|
|
82
|
+
function qualifiesIgnoredDirectory(cwd, name) {
|
|
83
|
+
return (existsSync(join(cwd, name)) &&
|
|
84
|
+
gitSucceeds(cwd, "check-ignore", "-q", name) &&
|
|
85
|
+
gitOutputOrUndefined(cwd, "ls-files", "--", name) === "");
|
|
86
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ProjectConfig } from "./project-config.js";
|
|
2
|
+
export interface DefaultBranch {
|
|
3
|
+
name: string;
|
|
4
|
+
source: "config" | "cached";
|
|
5
|
+
remote?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function resolveDefaultBranch(cwd: string, config: ProjectConfig | undefined): DefaultBranch | undefined;
|
|
8
|
+
export declare function renderDefaultBranchLine(branch: DefaultBranch | undefined): string;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { gitOutputOrUndefined, gitSucceeds } from "./git.js";
|
|
2
|
+
export function resolveDefaultBranch(cwd, config) {
|
|
3
|
+
const configured = config?.git?.defaultBranch;
|
|
4
|
+
if (configured !== undefined)
|
|
5
|
+
return { name: configured, source: "config" };
|
|
6
|
+
const remote = resolveRemote(cwd);
|
|
7
|
+
if (remote === undefined)
|
|
8
|
+
return;
|
|
9
|
+
const prefix = `refs/remotes/${remote}/`;
|
|
10
|
+
const target = gitOutputOrUndefined(cwd, "symbolic-ref", "-q", `${prefix}HEAD`);
|
|
11
|
+
if (target === undefined || target === "" || !target.startsWith(prefix))
|
|
12
|
+
return;
|
|
13
|
+
if (!gitSucceeds(cwd, "rev-parse", "--verify", "-q", target))
|
|
14
|
+
return;
|
|
15
|
+
return { name: target.slice(prefix.length), source: "cached", remote };
|
|
16
|
+
}
|
|
17
|
+
function resolveRemote(cwd) {
|
|
18
|
+
const output = gitOutputOrUndefined(cwd, "remote");
|
|
19
|
+
if (output === undefined || output === "")
|
|
20
|
+
return;
|
|
21
|
+
const remotes = output.split("\n").filter((remote) => remote !== "");
|
|
22
|
+
if (remotes.includes("origin"))
|
|
23
|
+
return "origin";
|
|
24
|
+
return remotes.length === 1 ? remotes[0] : undefined;
|
|
25
|
+
}
|
|
26
|
+
export function renderDefaultBranchLine(branch) {
|
|
27
|
+
if (branch === undefined)
|
|
28
|
+
return "Default branch: unresolved; ask before default-branch operations.";
|
|
29
|
+
return branch.source === "cached"
|
|
30
|
+
? `Default branch: ${branch.name} (cached).`
|
|
31
|
+
: `Default branch: ${branch.name}.`;
|
|
32
|
+
}
|
package/dist/parse-args.d.ts
CHANGED
package/dist/parse-args.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CliError } from "./cli-error.js";
|
|
2
2
|
import { errorMessage } from "./errors.js";
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
3
4
|
export function parseCommandArgs(usage, parse) {
|
|
4
5
|
try {
|
|
5
6
|
return parse();
|
|
@@ -9,3 +10,17 @@ export function parseCommandArgs(usage, parse) {
|
|
|
9
10
|
throw new CliError(`${detail}\n\n${usage}`);
|
|
10
11
|
}
|
|
11
12
|
}
|
|
13
|
+
export function parseBareCommandArgs(ctx, args, usage) {
|
|
14
|
+
const { values, positionals } = parseCommandArgs(usage, () => parseArgs({
|
|
15
|
+
args,
|
|
16
|
+
options: { help: { type: "boolean", short: "h", default: false } },
|
|
17
|
+
strict: true,
|
|
18
|
+
allowPositionals: true,
|
|
19
|
+
}));
|
|
20
|
+
if (positionals.length > 0)
|
|
21
|
+
throw new CliError(`Unexpected argument: ${positionals[0]}\n\n${usage}`);
|
|
22
|
+
if (!values.help)
|
|
23
|
+
return false;
|
|
24
|
+
ctx.stdout.write(usage);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
package/dist/plans/layout.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { CliError } from "../cli-error.js";
|
|
1
2
|
export declare const PLANS_DIR = ".plans";
|
|
2
3
|
export declare const ARCHIVES_DIR = "_archives";
|
|
3
4
|
export declare function isTicketName(name: string): boolean;
|
|
4
5
|
export declare function plansDir(cwd: string): string;
|
|
5
6
|
export declare function archivesDir(cwd: string): string;
|
|
6
7
|
export declare function assertPlansGate(cwd: string, form: string): void;
|
|
8
|
+
export declare function missingPlansMessage(form: string): string;
|
|
9
|
+
export declare function missingPlansError(form: string): CliError;
|
package/dist/plans/layout.js
CHANGED
|
@@ -15,5 +15,11 @@ export function archivesDir(cwd) {
|
|
|
15
15
|
export function assertPlansGate(cwd, form) {
|
|
16
16
|
if (existsSync(plansDir(cwd)))
|
|
17
17
|
return;
|
|
18
|
-
throw
|
|
18
|
+
throw missingPlansError(form);
|
|
19
|
+
}
|
|
20
|
+
export function missingPlansMessage(form) {
|
|
21
|
+
return `No .plans/ directory in the current directory.\nLocal plans: mkdir .plans && echo .plans >> .gitignore\nTeam plans: ${form} plans setup <clone-dir>`;
|
|
22
|
+
}
|
|
23
|
+
export function missingPlansError(form) {
|
|
24
|
+
return new CliError(missingPlansMessage(form));
|
|
19
25
|
}
|
package/dist/plans/mode.js
CHANGED
|
@@ -2,12 +2,12 @@ import { existsSync, lstatSync, realpathSync, statSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { CliError } from "../cli-error.js";
|
|
4
4
|
import { gitOutput } from "../git.js";
|
|
5
|
+
import { missingPlansError } from "./layout.js";
|
|
5
6
|
export function resolvePlansMode(cwd, form) {
|
|
6
7
|
const plansPath = join(cwd, ".plans");
|
|
7
8
|
const stats = lstatSync(plansPath, { throwIfNoEntry: false });
|
|
8
9
|
if (!stats)
|
|
9
|
-
throw
|
|
10
|
-
"(see the project documentation) — or create a plain .plans directory to keep plans local.");
|
|
10
|
+
throw missingPlansError(form);
|
|
11
11
|
if (stats.isSymbolicLink() && !existsSync(plansPath))
|
|
12
12
|
throw new CliError(`The .plans symlink is broken. Re-run ${form} plans setup with the clone location.`);
|
|
13
13
|
if (!statSync(plansPath).isDirectory())
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { gitOutputOrUndefined } from "../git.js";
|
|
4
|
+
export function findStoppedRebase(repoDir) {
|
|
5
|
+
const inProgress = ["rebase-merge", "rebase-apply"].some((name) => {
|
|
6
|
+
const path = gitOutputOrUndefined(repoDir, "rev-parse", "--git-path", name);
|
|
7
|
+
return path !== undefined && path !== "" && existsSync(resolve(repoDir, path));
|
|
8
|
+
});
|
|
9
|
+
if (!inProgress)
|
|
10
|
+
return;
|
|
11
|
+
const conflicts = gitOutputOrUndefined(repoDir, "diff", "--name-only", "--diff-filter=U");
|
|
12
|
+
return {
|
|
13
|
+
repoDir,
|
|
14
|
+
conflictedFiles: conflicts?.split("\n").filter((path) => path !== "") ?? [],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function renderStoppedRebase(stopped, form) {
|
|
18
|
+
const files = stopped.conflictedFiles.map((path) => ` ${path}`);
|
|
19
|
+
return [
|
|
20
|
+
`Plans synchronization stopped on a conflict in ${stopped.repoDir}:`,
|
|
21
|
+
...files,
|
|
22
|
+
"Resolve the markers in these files, then run:",
|
|
23
|
+
` git -C ${stopped.repoDir} add -A && git -C ${stopped.repoDir} rebase --continue`,
|
|
24
|
+
` ${form} sync`,
|
|
25
|
+
`To discard the local side instead: git -C ${stopped.repoDir} rebase --abort`,
|
|
26
|
+
].join("\n");
|
|
27
|
+
}
|
package/dist/plans/ticket.d.ts
CHANGED
|
@@ -11,11 +11,22 @@ export interface DeducedTicket {
|
|
|
11
11
|
id: string;
|
|
12
12
|
branch: string;
|
|
13
13
|
}
|
|
14
|
+
export type TicketDetection = {
|
|
15
|
+
kind: "detected";
|
|
16
|
+
id: string;
|
|
17
|
+
branch: string;
|
|
18
|
+
} | {
|
|
19
|
+
kind: "noMatch";
|
|
20
|
+
branch: string;
|
|
21
|
+
} | {
|
|
22
|
+
kind: "noBranch";
|
|
23
|
+
};
|
|
14
24
|
export declare function resolveTicketDir(cwd: string, id: string, { dryRun }: ResolveTicketOptions): ResolvedTicketDir;
|
|
15
25
|
export declare function reserveSideTicket(cwd: string): string;
|
|
16
26
|
export declare function peekSideTicket(cwd: string): string;
|
|
17
|
-
export declare function nextFileName(
|
|
27
|
+
export declare function nextFileName(entries: readonly string[], filename: string, newCycle: boolean): string;
|
|
18
28
|
export declare function listEntries(dir: string): string[];
|
|
19
29
|
export declare function isPathSafeTicketId(id: string): boolean;
|
|
20
30
|
export declare function validateTicketId(id: string, pattern?: string): void;
|
|
31
|
+
export declare function detectTicketFromBranch(cwd: string, pattern: string): TicketDetection;
|
|
21
32
|
export declare function deduceTicketFromBranch(cwd: string, pattern: string): DeducedTicket;
|
package/dist/plans/ticket.js
CHANGED
|
@@ -63,9 +63,9 @@ function readEntries(dir) {
|
|
|
63
63
|
return [];
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
-
export function nextFileName(
|
|
67
|
-
const prefixes =
|
|
68
|
-
const match = FILE_PREFIX.exec(entry
|
|
66
|
+
export function nextFileName(entries, filename, newCycle) {
|
|
67
|
+
const prefixes = entries.flatMap((entry) => {
|
|
68
|
+
const match = FILE_PREFIX.exec(entry);
|
|
69
69
|
return match ? [{ cycle: match[1], number: Number(match[2]) }] : [];
|
|
70
70
|
});
|
|
71
71
|
if (prefixes.length === 0)
|
|
@@ -90,15 +90,23 @@ export function validateTicketId(id, pattern) {
|
|
|
90
90
|
if (!isPathSafeTicketId(id))
|
|
91
91
|
throw new CliError(`Invalid ticket id: ${id}`);
|
|
92
92
|
if (pattern !== undefined && !new RegExp(pattern).test(id) && !SIDE_TICKET.test(id))
|
|
93
|
-
throw new CliError(`Ticket id "${id}" does not match
|
|
93
|
+
throw new CliError(`Ticket id "${id}" does not match ticketIdPattern "${pattern}".`);
|
|
94
94
|
}
|
|
95
|
-
export function
|
|
95
|
+
export function detectTicketFromBranch(cwd, pattern) {
|
|
96
96
|
const branch = gitOutputOrUndefined(cwd, "branch", "--show-current");
|
|
97
97
|
if (branch === undefined || branch === "")
|
|
98
|
-
|
|
98
|
+
return { kind: "noBranch" };
|
|
99
99
|
const unanchored = pattern.replace(/^\^/, "").replace(/\$$/, "");
|
|
100
100
|
const match = new RegExp(unanchored).exec(branch);
|
|
101
101
|
if (!match)
|
|
102
|
-
|
|
103
|
-
return { id: match[0], branch };
|
|
102
|
+
return { kind: "noMatch", branch };
|
|
103
|
+
return { kind: "detected", id: match[0], branch };
|
|
104
|
+
}
|
|
105
|
+
export function deduceTicketFromBranch(cwd, pattern) {
|
|
106
|
+
const result = detectTicketFromBranch(cwd, pattern);
|
|
107
|
+
if (result.kind === "noBranch")
|
|
108
|
+
throw new CliError("Cannot deduce a ticket id from a detached HEAD.");
|
|
109
|
+
if (result.kind === "noMatch")
|
|
110
|
+
throw new CliError(`Cannot deduce a ticket id from branch "${result.branch}" with pattern "${pattern}".`);
|
|
111
|
+
return result;
|
|
104
112
|
}
|
package/dist/project-config.d.ts
CHANGED
|
@@ -1,27 +1,34 @@
|
|
|
1
1
|
export declare const PROJECT_CONFIG_FILENAME = ".alignfirst.json";
|
|
2
|
-
export declare const portRangeSchema: import("arktype/internal/variants/object.ts").ObjectType<{
|
|
3
|
-
first: number;
|
|
4
|
-
last: number;
|
|
5
|
-
}, {}>;
|
|
6
2
|
export interface ProjectConfig {
|
|
7
3
|
schemaVersion: 1;
|
|
8
4
|
cli?: string;
|
|
9
|
-
|
|
5
|
+
ticketIdPattern?: string;
|
|
10
6
|
plans?: PlansConfig;
|
|
11
7
|
portRange?: PortRange;
|
|
12
|
-
|
|
8
|
+
git?: GitConfig;
|
|
13
9
|
}
|
|
14
10
|
export interface PlansConfig {
|
|
15
|
-
folder
|
|
11
|
+
folder?: string;
|
|
12
|
+
autoArchive?: boolean;
|
|
16
13
|
}
|
|
17
14
|
export interface PortRange {
|
|
18
15
|
first: number;
|
|
19
16
|
last: number;
|
|
20
17
|
}
|
|
21
|
-
export interface
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
export interface GitConfig {
|
|
19
|
+
defaultBranch?: string;
|
|
20
|
+
branchNameTemplate?: string;
|
|
21
|
+
commit?: CommitConfig;
|
|
22
|
+
agentCoauthoring?: boolean;
|
|
24
23
|
}
|
|
25
|
-
export
|
|
26
|
-
|
|
24
|
+
export interface CommitConfig {
|
|
25
|
+
style: "conventionalCommit";
|
|
26
|
+
ticketReference?: "bracketed" | "bracketedHash";
|
|
27
|
+
}
|
|
28
|
+
export interface ResolvedProjectConfig {
|
|
29
|
+
config: ProjectConfig;
|
|
30
|
+
source: "root";
|
|
31
|
+
}
|
|
32
|
+
export declare function resolveProjectConfig(cwd: string): ResolvedProjectConfig | undefined;
|
|
27
33
|
export declare function readProjectConfig(dir: string): ProjectConfig | undefined;
|
|
34
|
+
export declare function validateProjectConfig(value: unknown, label: string): ProjectConfig;
|
package/dist/project-config.js
CHANGED
|
@@ -1,45 +1,68 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { join } from "node:path";
|
|
3
3
|
import { type } from "arktype";
|
|
4
4
|
import semver from "semver";
|
|
5
5
|
import { CliError } from "./cli-error.js";
|
|
6
6
|
import { errorMessage } from "./errors.js";
|
|
7
7
|
export const PROJECT_CONFIG_FILENAME = ".alignfirst.json";
|
|
8
|
-
|
|
8
|
+
const portRangeSchema = type({
|
|
9
9
|
"+": "reject",
|
|
10
10
|
first: "1 <= number.integer <= 65535",
|
|
11
11
|
last: "1 <= number.integer <= 65535",
|
|
12
12
|
});
|
|
13
13
|
const plansSchema = type({
|
|
14
14
|
"+": "reject",
|
|
15
|
-
folder: "string > 0",
|
|
15
|
+
"folder?": "string > 0",
|
|
16
|
+
"autoArchive?": "boolean",
|
|
16
17
|
});
|
|
17
|
-
const
|
|
18
|
+
const commitSchema = type({
|
|
18
19
|
"+": "reject",
|
|
19
|
-
|
|
20
|
-
"
|
|
20
|
+
style: "'conventionalCommit'",
|
|
21
|
+
"ticketReference?": "'bracketed' | 'bracketedHash'",
|
|
22
|
+
});
|
|
23
|
+
const gitSchema = type({
|
|
24
|
+
"+": "reject",
|
|
25
|
+
"defaultBranch?": "string > 0",
|
|
26
|
+
"branchNameTemplate?": "string > 0",
|
|
27
|
+
"commit?": commitSchema,
|
|
28
|
+
"agentCoauthoring?": "boolean",
|
|
21
29
|
});
|
|
22
30
|
const projectConfigSchema = type({
|
|
23
31
|
"+": "reject",
|
|
24
32
|
schemaVersion: "1",
|
|
25
33
|
"cli?": "string > 0",
|
|
26
|
-
"
|
|
34
|
+
"ticketIdPattern?": "string > 0",
|
|
27
35
|
"plans?": plansSchema,
|
|
28
36
|
"portRange?": portRangeSchema,
|
|
29
|
-
"
|
|
37
|
+
"git?": gitSchema,
|
|
30
38
|
});
|
|
39
|
+
export function resolveProjectConfig(cwd) {
|
|
40
|
+
const config = readProjectConfig(cwd);
|
|
41
|
+
return config === undefined ? undefined : { config, source: "root" };
|
|
42
|
+
}
|
|
43
|
+
export function readProjectConfig(dir) {
|
|
44
|
+
const path = join(dir, PROJECT_CONFIG_FILENAME);
|
|
45
|
+
if (!existsSync(path))
|
|
46
|
+
return;
|
|
47
|
+
let value;
|
|
48
|
+
try {
|
|
49
|
+
value = JSON.parse(readFileSync(path, "utf-8"));
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
throw invalidConfig(path, errorMessage(error));
|
|
53
|
+
}
|
|
54
|
+
return validateProjectConfig(value, path);
|
|
55
|
+
}
|
|
31
56
|
export function validateProjectConfig(value, label) {
|
|
32
57
|
const config = projectConfigSchema(value);
|
|
33
58
|
if (config instanceof type.errors)
|
|
34
59
|
throw invalidConfig(label, config.summary.split("\n", 1)[0]);
|
|
35
60
|
if (config.cli !== undefined && semver.validRange(config.cli) === null)
|
|
36
61
|
throw invalidConfig(label, `cli is not a valid semver range: ${config.cli}`);
|
|
37
|
-
if (config.
|
|
38
|
-
assertValidPattern(config.
|
|
62
|
+
if (config.ticketIdPattern !== undefined)
|
|
63
|
+
assertValidPattern(config.ticketIdPattern, label);
|
|
39
64
|
if (config.portRange !== undefined)
|
|
40
65
|
assertValidPortRange(config.portRange, label);
|
|
41
|
-
if (config.project !== undefined)
|
|
42
|
-
assertValidProjectIdentity(config.project, label);
|
|
43
66
|
return config;
|
|
44
67
|
}
|
|
45
68
|
function invalidConfig(label, detail) {
|
|
@@ -50,30 +73,10 @@ function assertValidPattern(pattern, label) {
|
|
|
50
73
|
new RegExp(pattern);
|
|
51
74
|
}
|
|
52
75
|
catch (error) {
|
|
53
|
-
throw invalidConfig(label, `
|
|
76
|
+
throw invalidConfig(label, `ticketIdPattern is not a valid regular expression: ${errorMessage(error)}`);
|
|
54
77
|
}
|
|
55
78
|
}
|
|
56
|
-
|
|
79
|
+
function assertValidPortRange(range, label) {
|
|
57
80
|
if (range.first > range.last)
|
|
58
81
|
throw invalidConfig(label, "portRange.first must not exceed portRange.last");
|
|
59
82
|
}
|
|
60
|
-
function assertValidProjectIdentity(project, label) {
|
|
61
|
-
if (project.remote === undefined && project.paths === undefined)
|
|
62
|
-
throw invalidConfig(label, "project must contain remote or paths");
|
|
63
|
-
const relativePath = project.paths?.find((path) => !isAbsolute(path));
|
|
64
|
-
if (relativePath !== undefined)
|
|
65
|
-
throw invalidConfig(label, `project.paths must contain only absolute paths: ${relativePath}`);
|
|
66
|
-
}
|
|
67
|
-
export function readProjectConfig(dir) {
|
|
68
|
-
const path = join(dir, PROJECT_CONFIG_FILENAME);
|
|
69
|
-
if (!existsSync(path))
|
|
70
|
-
return;
|
|
71
|
-
let value;
|
|
72
|
-
try {
|
|
73
|
-
value = JSON.parse(readFileSync(path, "utf-8"));
|
|
74
|
-
}
|
|
75
|
-
catch (error) {
|
|
76
|
-
throw invalidConfig(path, errorMessage(error));
|
|
77
|
-
}
|
|
78
|
-
return validateProjectConfig(value, path);
|
|
79
|
-
}
|
package/dist/skills.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { CommandContext } from "./context.js";
|
|
2
1
|
export declare const STUB_SKILLS: readonly ["alignfirst", "alspec", "alplan", "al", "alcatchup", "almerge", "alreview", "aldescription"];
|
|
3
2
|
export declare const SKILL_ROOTS: readonly [".agents/skills", ".claude/skills", ".codex/skills"];
|
|
4
3
|
export interface InstalledSkill {
|
|
@@ -6,4 +5,3 @@ export interface InstalledSkill {
|
|
|
6
5
|
version: string | undefined;
|
|
7
6
|
}
|
|
8
7
|
export declare function findInstalledSkill(home: string, name: string): InstalledSkill | undefined;
|
|
9
|
-
export declare function installStubSkills(ctx: CommandContext, agents: string[]): void;
|