aligndev 0.0.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +166 -1
  2. package/bin/aligndev.mjs +3 -0
  3. package/dist/alignfirst-cli.d.ts +10 -0
  4. package/dist/alignfirst-cli.js +56 -0
  5. package/dist/cli.d.ts +15 -0
  6. package/dist/cli.js +95 -0
  7. package/dist/code/claude-agent.d.ts +14 -0
  8. package/dist/code/claude-agent.js +168 -0
  9. package/dist/code/code-cli.d.ts +72 -0
  10. package/dist/code/code-cli.js +630 -0
  11. package/dist/code/codex-agent.d.ts +15 -0
  12. package/dist/code/codex-agent.js +168 -0
  13. package/dist/code/codex-rollout.d.ts +18 -0
  14. package/dist/code/codex-rollout.js +114 -0
  15. package/dist/code/coding-agent.d.ts +4 -0
  16. package/dist/code/coding-agent.js +6 -0
  17. package/dist/code/models.d.ts +14 -0
  18. package/dist/code/models.js +89 -0
  19. package/dist/code/prompt.d.ts +11 -0
  20. package/dist/code/prompt.js +26 -0
  21. package/dist/code/quota.d.ts +25 -0
  22. package/dist/code/quota.js +248 -0
  23. package/dist/code/run-agent.d.ts +60 -0
  24. package/dist/code/run-agent.js +212 -0
  25. package/dist/code/session-file.d.ts +50 -0
  26. package/dist/code/session-file.js +263 -0
  27. package/dist/command-form.d.ts +6 -0
  28. package/dist/command-form.js +7 -0
  29. package/dist/config.d.ts +22 -0
  30. package/dist/config.js +72 -0
  31. package/dist/errors.d.ts +2 -0
  32. package/dist/errors.js +6 -0
  33. package/dist/guide/code-guide.d.ts +4 -0
  34. package/dist/guide/code-guide.js +17 -0
  35. package/dist/guide/guide-cli.d.ts +3 -0
  36. package/dist/guide/guide-cli.js +81 -0
  37. package/dist/guide/render-template.d.ts +4 -0
  38. package/dist/guide/render-template.js +62 -0
  39. package/dist/guide/topics.d.ts +3 -0
  40. package/dist/guide/topics.js +16 -0
  41. package/dist/output.d.ts +3 -0
  42. package/dist/output.js +1 -0
  43. package/dist/project/discovery.d.ts +42 -0
  44. package/dist/project/discovery.js +287 -0
  45. package/dist/project/format.d.ts +6 -0
  46. package/dist/project/format.js +31 -0
  47. package/dist/project/guide.d.ts +3 -0
  48. package/dist/project/guide.js +69 -0
  49. package/dist/project/layout.d.ts +36 -0
  50. package/dist/project/layout.js +128 -0
  51. package/dist/project/markers.d.ts +18 -0
  52. package/dist/project/markers.js +90 -0
  53. package/dist/project/ports.d.ts +3 -0
  54. package/dist/project/ports.js +55 -0
  55. package/dist/project/project-cli.d.ts +17 -0
  56. package/dist/project/project-cli.js +227 -0
  57. package/dist/project/render.d.ts +10 -0
  58. package/dist/project/render.js +144 -0
  59. package/dist/project/status.d.ts +24 -0
  60. package/dist/project/status.js +110 -0
  61. package/dist/templates.d.ts +1 -0
  62. package/dist/templates.js +5 -0
  63. package/package.json +38 -3
  64. package/templates/guide/code.md +252 -0
  65. package/templates/guide/playbook/channel-handling.md +110 -0
  66. package/templates/guide/playbook/consultation.md +71 -0
  67. package/templates/guide/playbook/discord-message-tool.md +37 -0
  68. package/templates/guide/playbook/playbook.md +139 -0
  69. package/templates/guide/playbook/project-lifecycle.md +100 -0
  70. package/templates/guide/playbook/project-workspace-setup.md +186 -0
  71. package/templates/guide/playbook/slack-message-tool.md +23 -0
  72. package/templates/guide/playbook/working-session.md +588 -0
  73. package/templates/guide/project.md +33 -0
@@ -0,0 +1,55 @@
1
+ import { formatRange } from "./format.js";
2
+ import { MARKER_FILENAME } from "./markers.js";
3
+ export function findFreeBlock(inventory, size, code) {
4
+ const rootRanges = inventory.directories.find(({ path }) => path === inventory.root)?.portRanges;
5
+ const markerPath = `${inventory.root}/${MARKER_FILENAME}`;
6
+ if (rootRanges === undefined)
7
+ throw new Error(`${markerPath} has no portRanges.`);
8
+ const rootRange = selectPortRange(rootRanges, code, markerPath);
9
+ const occupied = [
10
+ ...inventory.projects.flatMap(({ portRange }) => portRange === undefined ? [] : [allocatedRange(portRange)]),
11
+ ...inventory.directories.flatMap(({ path, portRanges }) => path === inventory.root
12
+ ? []
13
+ : (portRanges ?? []).map((portRange) => allocatedRange(portRange))),
14
+ ];
15
+ const first = lowestFreeBase(occupied, size, rootRange.first, rootRange.last);
16
+ if (first === undefined) {
17
+ throw new Error(`No block of ${size} contiguous free ports in ${formatRange(rootRange)}.`);
18
+ }
19
+ return { first, last: first + size - 1 };
20
+ }
21
+ function selectPortRange(ranges, code, markerPath) {
22
+ const declaredCodes = ranges.flatMap((range) => (range.code === undefined ? [] : [range.code]));
23
+ if (code !== undefined) {
24
+ const selected = ranges.find((range) => range.code === code);
25
+ if (selected !== undefined)
26
+ return selected;
27
+ throw new Error(`Unknown port range code ${JSON.stringify(code)} in ${markerPath}; ` +
28
+ `declared codes: ${declaredCodes.join(", ")}`);
29
+ }
30
+ const defaultRange = ranges.find((range) => range.code === undefined);
31
+ if (defaultRange !== undefined)
32
+ return defaultRange;
33
+ throw new Error(`${markerPath} declares no default range: pass --range <code> ` +
34
+ `(codes: ${declaredCodes.join(", ")})`);
35
+ }
36
+ function lowestFreeBase(ranges, size, firstPort, lastPort) {
37
+ let candidate = firstPort;
38
+ for (const range of ranges.toSorted((left, right) => left.start - right.start)) {
39
+ if (range.end < candidate)
40
+ continue;
41
+ if (range.start > lastPort)
42
+ break;
43
+ if (fitsBefore(candidate, size, range.start - 1))
44
+ return candidate;
45
+ candidate = Math.max(candidate, range.end + 1);
46
+ }
47
+ return fitsBefore(candidate, size, lastPort) ? candidate : undefined;
48
+ }
49
+ function fitsBefore(basePort, size, lastPort) {
50
+ const end = basePort + size - 1;
51
+ return Number.isSafeInteger(end) && end <= lastPort;
52
+ }
53
+ function allocatedRange(range) {
54
+ return { start: range.first, end: range.last };
55
+ }
@@ -0,0 +1,17 @@
1
+ import type { CommandForms } from "../command-form.js";
2
+ import type { AligndevConfig, ProjectsRoot } from "../config.js";
3
+ import type { Output } from "../output.js";
4
+ export interface ProjectsCallerContext {
5
+ cwd: string;
6
+ env: NodeJS.ProcessEnv;
7
+ home: string;
8
+ stdout: Output;
9
+ stderr: Output;
10
+ forms: CommandForms;
11
+ alignfirstCommand: string[];
12
+ }
13
+ export interface ProjectsContext extends ProjectsCallerContext {
14
+ projectsRoot?: ProjectsRoot;
15
+ }
16
+ export declare function runProject(tokens: string[], config: AligndevConfig | undefined, caller: ProjectsCallerContext): number;
17
+ export declare function renderProjectsGuideForRoot(ctx: ProjectsContext, rootOption: string | undefined): string;
@@ -0,0 +1,227 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { isAbsolute, join, resolve } from "node:path";
3
+ import { parseArgs } from "node:util";
4
+ import { errorMessage } from "../errors.js";
5
+ import { buildInventory } from "./discovery.js";
6
+ import { formatRange } from "./format.js";
7
+ import { renderProjectsGuide } from "./guide.js";
8
+ import { assertValidPortRanges, MARKER_FILENAME, readMarker, writeMarker, } from "./markers.js";
9
+ import { findFreeBlock } from "./ports.js";
10
+ import { renderPortRangeJson, renderProjectDoctor, renderProjectDoctorFailure, renderProjectList, renderProjectListJson, renderProjectStatus, renderProjectStatusJson, } from "./render.js";
11
+ import { getProjectStatus } from "./status.js";
12
+ export function runProject(tokens, config, caller) {
13
+ const ctx = { ...caller, projectsRoot: config?.projectsRoot };
14
+ try {
15
+ return runProjectCommand(ctx, parseProjectsArgs(tokens));
16
+ }
17
+ catch (error) {
18
+ ctx.stderr.write(`${errorMessage(error)}\n`);
19
+ return 1;
20
+ }
21
+ }
22
+ function runProjectCommand(ctx, args) {
23
+ if (args.help || args.command === undefined) {
24
+ ctx.stdout.write(renderUsage(ctx.forms.aligndev));
25
+ return 0;
26
+ }
27
+ if (args.command === "doctor")
28
+ return inspectProjectInventory(ctx, args.root);
29
+ const root = resolveProjectsRoot(ctx, args.root);
30
+ if (args.command === "init")
31
+ return initializeProjectsDirectory(root, args, ctx.stdout);
32
+ const marker = requireMarker(root, ctx.forms.aligndev);
33
+ const inventory = inventoryFor(root, marker, ctx);
34
+ if (args.command === "list") {
35
+ ctx.stdout.write(args.json ? renderProjectListJson(inventory) : renderProjectList(inventory));
36
+ return 0;
37
+ }
38
+ if (args.command === "status" && args.path !== undefined) {
39
+ const details = getProjectStatus(inventory, args.path);
40
+ ctx.stdout.write(args.json ? renderProjectStatusJson(details) : renderProjectStatus(details));
41
+ return 0;
42
+ }
43
+ if (args.command === "free-ports" && args.size !== undefined) {
44
+ const range = findFreeBlock(inventory, args.size, args.range);
45
+ ctx.stdout.write(args.json ? renderPortRangeJson(range) : `${formatRange(range)}\n`);
46
+ return 0;
47
+ }
48
+ throw new Error("Invalid aligndev project command");
49
+ }
50
+ function renderUsage(aligndev) {
51
+ return `Usage:
52
+ ${aligndev} project list [--json] [--root <path>]
53
+ ${aligndev} project doctor [--root <path>]
54
+ ${aligndev} project status <path> [--json] [--root <path>]
55
+ ${aligndev} project init [--root <path>] [--description <text>] [--port-range [<code>=]<first>-<last>]...
56
+ ${aligndev} project free-ports --size <n> [--range <code>] [--json] [--root <path>]
57
+ ${aligndev} project --help
58
+
59
+ --root defaults to projectsRoot in the aligndev config, then to the working directory.
60
+ `;
61
+ }
62
+ // The projects guide, followed by the directory sections when the root carries a marker.
63
+ export function renderProjectsGuideForRoot(ctx, rootOption) {
64
+ const root = resolveProjectsRoot(ctx, rootOption);
65
+ const marker = readMarker(root);
66
+ const inventory = marker === undefined ? undefined : inventoryFor(root, marker, ctx);
67
+ return renderProjectsGuide(ctx.forms, inventory);
68
+ }
69
+ function inspectProjectInventory(ctx, rootOption) {
70
+ try {
71
+ const root = resolveProjectsRoot(ctx, rootOption);
72
+ const inventory = inventoryFor(root, requireMarker(root, ctx.forms.aligndev), ctx);
73
+ ctx.stdout.write(renderProjectDoctor(inventory));
74
+ return inventory.issues.length === 0 ? 0 : 1;
75
+ }
76
+ catch (error) {
77
+ ctx.stdout.write(renderProjectDoctorFailure(errorMessage(error)));
78
+ return 1;
79
+ }
80
+ }
81
+ function parseProjectsArgs(tokens) {
82
+ const { values, positionals } = parseArgs({
83
+ args: tokens,
84
+ options: {
85
+ root: { type: "string" },
86
+ json: { type: "boolean", default: false },
87
+ description: { type: "string" },
88
+ "port-range": { type: "string", multiple: true },
89
+ range: { type: "string" },
90
+ size: { type: "string" },
91
+ help: { type: "boolean", short: "h", default: false },
92
+ },
93
+ strict: true,
94
+ allowPositionals: true,
95
+ });
96
+ if (values.help)
97
+ return emptyModeArgs(values.root, true);
98
+ const [rawCommand, path, ...extra] = positionals;
99
+ if (rawCommand === undefined) {
100
+ assertNoCommandOptions(values);
101
+ return emptyModeArgs(values.root, false);
102
+ }
103
+ if (!isProjectsCommand(rawCommand))
104
+ throw new Error(`Unknown projects command: ${rawCommand}`);
105
+ validatePositionals(rawCommand, path, extra);
106
+ validateOptionPlacement(rawCommand, values);
107
+ return {
108
+ command: rawCommand,
109
+ ...(path === undefined ? {} : { path }),
110
+ ...(values.root === undefined ? {} : { root: values.root }),
111
+ json: values.json,
112
+ help: false,
113
+ ...(values.description === undefined ? {} : { description: values.description }),
114
+ ...(values["port-range"] === undefined
115
+ ? {}
116
+ : { portRanges: parsePortRanges(values["port-range"]) }),
117
+ ...(values.range === undefined ? {} : { range: values.range }),
118
+ ...(values.size === undefined ? {} : { size: parsePositiveInteger("--size", values.size) }),
119
+ };
120
+ }
121
+ function emptyModeArgs(root, help) {
122
+ return {
123
+ ...(root === undefined ? {} : { root }),
124
+ json: false,
125
+ help,
126
+ };
127
+ }
128
+ function assertNoCommandOptions(values) {
129
+ if (values.json ||
130
+ values.description !== undefined ||
131
+ values["port-range"] !== undefined ||
132
+ values.range !== undefined ||
133
+ values.size !== undefined) {
134
+ throw new Error("Command options require a projects command");
135
+ }
136
+ }
137
+ function isProjectsCommand(value) {
138
+ return (value === "list" ||
139
+ value === "doctor" ||
140
+ value === "status" ||
141
+ value === "init" ||
142
+ value === "free-ports");
143
+ }
144
+ function validatePositionals(command, path, extra) {
145
+ if (command === "status") {
146
+ if (path === undefined || extra.length > 0)
147
+ throw new Error("status requires exactly one path");
148
+ return;
149
+ }
150
+ if (path !== undefined)
151
+ throw new Error(`${command} does not accept a path`);
152
+ }
153
+ function validateOptionPlacement(command, values) {
154
+ if (values.json && command !== "list" && command !== "status" && command !== "free-ports") {
155
+ throw new Error("--json is valid only with list, status, or free-ports");
156
+ }
157
+ if (command !== "init" &&
158
+ (values.description !== undefined || values["port-range"] !== undefined)) {
159
+ throw new Error("--description and --port-range are valid only with init");
160
+ }
161
+ if (values.range !== undefined && command !== "free-ports") {
162
+ throw new Error("--range is valid only with free-ports");
163
+ }
164
+ if (command === "free-ports") {
165
+ if (values.size === undefined)
166
+ throw new Error("free-ports requires --size <n>");
167
+ }
168
+ else if (values.size !== undefined) {
169
+ throw new Error("--size is valid only with free-ports");
170
+ }
171
+ }
172
+ function parsePortRanges(values) {
173
+ const ranges = values.map(parsePortRange);
174
+ assertValidPortRanges(ranges, "--port-range");
175
+ return ranges;
176
+ }
177
+ function parsePortRange(value) {
178
+ const match = /^(?:([a-z][a-z0-9-]*)=)?(\d+)-(\d+)$/u.exec(value);
179
+ if (match === null)
180
+ throw new Error("--port-range must be [<code>=]<first>-<last>");
181
+ return {
182
+ ...(match[1] === undefined ? {} : { code: match[1] }),
183
+ first: Number(match[2]),
184
+ last: Number(match[3]),
185
+ };
186
+ }
187
+ function parsePositiveInteger(option, value) {
188
+ if (!/^[1-9]\d*$/u.test(value))
189
+ throw new Error(`${option} must be a positive integer`);
190
+ const parsed = Number(value);
191
+ if (!Number.isSafeInteger(parsed))
192
+ throw new Error(`${option} must be a positive integer`);
193
+ return parsed;
194
+ }
195
+ function resolveProjectsRoot(ctx, rootOption) {
196
+ if (rootOption !== undefined)
197
+ return realpathSync(resolveRootOption(ctx, rootOption));
198
+ return realpathSync(ctx.projectsRoot?.path ?? ctx.cwd);
199
+ }
200
+ function resolveRootOption(ctx, rootOption) {
201
+ const expanded = rootOption.startsWith("~/") ? join(ctx.home, rootOption.slice(2)) : rootOption;
202
+ return isAbsolute(expanded) ? expanded : resolve(ctx.cwd, expanded);
203
+ }
204
+ function initializeProjectsDirectory(root, args, stdout) {
205
+ const markerPath = join(root, MARKER_FILENAME);
206
+ if (readMarker(root) !== undefined)
207
+ throw new Error(`${markerPath} already exists.`);
208
+ writeMarker(root, {
209
+ ...(args.description === undefined ? {} : { description: args.description }),
210
+ ...(args.portRanges === undefined ? {} : { portRanges: args.portRanges }),
211
+ });
212
+ stdout.write(`Created ${markerPath}\n`);
213
+ return 0;
214
+ }
215
+ function requireMarker(root, aligndev) {
216
+ const marker = readMarker(root);
217
+ if (marker !== undefined)
218
+ return marker;
219
+ throw new Error(`${root} is not a projects directory: ${MARKER_FILENAME} is missing. ` +
220
+ `Run \`${aligndev} project init\` there, or pass --root <path>.`);
221
+ }
222
+ function inventoryFor(root, marker, ctx) {
223
+ return buildInventory(root, marker, {
224
+ env: ctx.env,
225
+ alignfirstCommand: ctx.alignfirstCommand,
226
+ });
227
+ }
@@ -0,0 +1,10 @@
1
+ import type { ProjectInventory } from "./discovery.js";
2
+ import type { PortRange } from "./markers.js";
3
+ import type { ProjectDetails } from "./status.js";
4
+ export declare function renderProjectList(inventory: ProjectInventory): string;
5
+ export declare function renderProjectListJson(inventory: ProjectInventory): string;
6
+ export declare function renderProjectDoctor(inventory: ProjectInventory): string;
7
+ export declare function renderProjectDoctorFailure(message: string): string;
8
+ export declare function renderProjectStatus(details: ProjectDetails): string;
9
+ export declare function renderProjectStatusJson(details: ProjectDetails): string;
10
+ export declare function renderPortRangeJson(range: PortRange): string;
@@ -0,0 +1,144 @@
1
+ import { escapeAdditionalJsonCharacters, escapeControlCharacters, formatRange } from "./format.js";
2
+ export function renderProjectList(inventory) {
3
+ const lines = ["Projects:"];
4
+ if (inventory.projects.length === 0)
5
+ lines.push(" (none)");
6
+ for (const project of inventory.projects) {
7
+ lines.push(`- Name: ${renderOutputValue(project.name)}`, ` Path: ${renderOutputValue(project.path)}`, ` Directory: ${renderOutputValue(project.directory)}`, ` Port range: ${renderProjectRange(project.portRange, project.portRangeCode)}`, ` Workspaces: ${renderValues(project.workspaces)}`);
8
+ }
9
+ lines.push("", "Directories:");
10
+ if (inventory.directories.length === 0)
11
+ lines.push(" (none)");
12
+ for (const directory of inventory.directories) {
13
+ lines.push(`- Path: ${renderOutputValue(directory.path)}`);
14
+ if (directory.description !== undefined) {
15
+ lines.push(` Description: ${renderOutputValue(directory.description)}`);
16
+ }
17
+ if (directory.portRanges === undefined)
18
+ lines.push(" Port ranges: (none)");
19
+ else {
20
+ lines.push(" Port ranges:");
21
+ for (const range of directory.portRanges)
22
+ lines.push(` ${renderMarkerRange(range)}`);
23
+ }
24
+ lines.push(` Others: ${renderValues(directory.others)}`);
25
+ }
26
+ lines.push("", "Issues:");
27
+ if (inventory.issues.length === 0)
28
+ lines.push(" (none)");
29
+ for (const issue of inventory.issues) {
30
+ lines.push(`- ${renderOutputValue(issue.path)}: ${escapeControlCharacters(issue.message)}`);
31
+ }
32
+ return `${lines.join("\n")}\n`;
33
+ }
34
+ export function renderProjectListJson(inventory) {
35
+ const report = {
36
+ root: inventory.root,
37
+ directories: inventory.directories.map((directory) => ({
38
+ path: directory.path,
39
+ description: directory.description ?? null,
40
+ portRanges: directory.portRanges ?? [],
41
+ others: directory.others,
42
+ })),
43
+ projects: inventory.projects.map((project) => ({
44
+ name: project.name,
45
+ path: project.path,
46
+ directory: project.directory,
47
+ portRange: project.portRange ?? null,
48
+ portRangeCode: project.portRangeCode ?? null,
49
+ companion: project.description.companion,
50
+ locations: project.description.locations,
51
+ workspaces: project.workspaces,
52
+ })),
53
+ issues: inventory.issues.map(({ path, message }) => ({ path, message })),
54
+ };
55
+ return renderJson(report);
56
+ }
57
+ export function renderProjectDoctor(inventory) {
58
+ if (inventory.issues.length === 0) {
59
+ return (`[ok] Project inventory: ${renderCount(inventory.projects.length, "project")}, ` +
60
+ `${renderCount(inventory.directories.length, "directory")}\n`);
61
+ }
62
+ return `${inventory.issues.map(renderInventoryIssue).join("\n")}\n`;
63
+ }
64
+ function renderCount(value, singular) {
65
+ return `${value} ${singular}${value === 1 ? "" : "s"}`;
66
+ }
67
+ function renderInventoryIssue(issue) {
68
+ if (issue.conflict !== undefined) {
69
+ const { left, right } = issue.conflict;
70
+ return (`[error] Port conflict: ${renderOutputValue(left.path)} (${renderRange(left.portRange)}) ` +
71
+ `overlaps ${renderOutputValue(right.path)} (${renderRange(right.portRange)})`);
72
+ }
73
+ return (`[error] Project inventory: ${renderOutputValue(issue.path)}: ` +
74
+ escapeControlCharacters(issue.message));
75
+ }
76
+ export function renderProjectDoctorFailure(message) {
77
+ return `[error] Project inventory: ${escapeControlCharacters(message)}\n`;
78
+ }
79
+ export function renderProjectStatus(details) {
80
+ const lines = [
81
+ "Project:",
82
+ ` Name: ${renderOutputValue(details.name)}`,
83
+ ` Path: ${renderOutputValue(details.path)}`,
84
+ ` Directory: ${renderOutputValue(details.directory)}`,
85
+ ` Remote host: ${renderNullableValue(details.remoteHost)}`,
86
+ ` Port range: ${renderProjectRange(details.portRange ?? undefined, details.portRangeCode ?? undefined)}`,
87
+ ` Config source: ${details.configSource ?? "(none)"}`,
88
+ ` Companion: ${renderCompanion(details.companion)}`,
89
+ ` DEVELOPERS.md: ${renderLocation(details.locations["DEVELOPERS.md"])}`,
90
+ ` Work files: ${renderLocation(details.locations[".plans"])}`,
91
+ ` Work-files folder: ${renderNullableValue(details.plansFolder)}`,
92
+ ` Ticket id pattern: ${renderNullableValue(details.ticketIdPattern)}`,
93
+ ` Workspaces: ${renderValues(details.workspaces)}`,
94
+ " Worktrees:",
95
+ ];
96
+ if (details.worktrees.length === 0)
97
+ lines.push(" (none)");
98
+ for (const worktree of details.worktrees) {
99
+ lines.push(` - Name: ${renderOutputValue(worktree.name)}`, ` Path: ${renderOutputValue(worktree.path)}`, ` Branch: ${worktree.branch === null ? "(detached)" : renderOutputValue(worktree.branch)}`);
100
+ }
101
+ return `${lines.join("\n")}\n`;
102
+ }
103
+ function renderCompanion(companion) {
104
+ if (companion === null)
105
+ return "(none)";
106
+ return renderPath(companion.dir, companion.exists);
107
+ }
108
+ function renderLocation(location) {
109
+ return renderPath(location.path, location.exists);
110
+ }
111
+ function renderPath(path, exists) {
112
+ return `${renderOutputValue(path)}${exists ? "" : " (missing)"}`;
113
+ }
114
+ export function renderProjectStatusJson(details) {
115
+ return renderJson(details);
116
+ }
117
+ export function renderPortRangeJson(range) {
118
+ return renderJson(range);
119
+ }
120
+ function renderJson(value) {
121
+ return `${escapeAdditionalJsonCharacters(JSON.stringify(value, undefined, 2))}\n`;
122
+ }
123
+ function renderRange(range) {
124
+ return range === undefined ? "(none)" : formatRange(range);
125
+ }
126
+ function renderProjectRange(range, code) {
127
+ if (range === undefined)
128
+ return "(none)";
129
+ return `${formatRange(range)}${code === undefined ? "" : ` (${code})`}`;
130
+ }
131
+ function renderMarkerRange(range) {
132
+ const code = range.code ?? "default";
133
+ const description = range.description === undefined ? "" : ` — ${escapeControlCharacters(range.description)}`;
134
+ return `${formatRange(range)} (${code})${description}`;
135
+ }
136
+ function renderValues(values) {
137
+ return values.length === 0 ? "(none)" : values.map(renderOutputValue).join(", ");
138
+ }
139
+ function renderNullableValue(value) {
140
+ return value === null ? "(none)" : renderOutputValue(value);
141
+ }
142
+ function renderOutputValue(value) {
143
+ return escapeAdditionalJsonCharacters(JSON.stringify(value));
144
+ }
@@ -0,0 +1,24 @@
1
+ import type { ProjectInventory } from "./discovery.js";
2
+ import type { CompanionReport, ConfigSource, ItemLocation, ItemName } from "./layout.js";
3
+ import type { PortRange } from "./markers.js";
4
+ export interface ProjectDetails {
5
+ name: string;
6
+ path: string;
7
+ directory: string;
8
+ remoteHost: string | null;
9
+ portRange: PortRange | null;
10
+ portRangeCode: string | null;
11
+ plansFolder: string | null;
12
+ ticketIdPattern: string | null;
13
+ configSource: ConfigSource | null;
14
+ companion: CompanionReport | null;
15
+ locations: Record<ItemName, ItemLocation>;
16
+ workspaces: string[];
17
+ worktrees: ProjectWorktree[];
18
+ }
19
+ export interface ProjectWorktree {
20
+ branch: string | null;
21
+ name: string;
22
+ path: string;
23
+ }
24
+ export declare function getProjectStatus(inventory: ProjectInventory, inputPath: string): ProjectDetails;
@@ -0,0 +1,110 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { realpathSync } from "node:fs";
3
+ import { basename, isAbsolute, resolve } from "node:path";
4
+ import { errorMessage, isNodeError } from "../errors.js";
5
+ const URL_WITH_AUTHORITY = /^[A-Za-z][A-Za-z\d+.-]*:\/\//u;
6
+ export function getProjectStatus(inventory, inputPath) {
7
+ const path = resolveProjectPath(inventory.root, inputPath);
8
+ const project = inventory.projects.find((candidate) => candidate.path === path);
9
+ if (project === undefined) {
10
+ throw new Error(`${path} is not a project of ${inventory.root}. Pass the main-worktree path of a git ` +
11
+ "repository in that projects directory.");
12
+ }
13
+ return buildProjectDetails(project);
14
+ }
15
+ function resolveProjectPath(root, inputPath) {
16
+ const path = isAbsolute(inputPath) ? inputPath : resolve(root, inputPath);
17
+ try {
18
+ return realpathSync(path);
19
+ }
20
+ catch (error) {
21
+ if (isNodeError(error) && (error.code === "ENOENT" || error.code === "ENOTDIR"))
22
+ return path;
23
+ throw error;
24
+ }
25
+ }
26
+ function buildProjectDetails(project) {
27
+ return {
28
+ name: project.name,
29
+ path: project.path,
30
+ directory: project.directory,
31
+ remoteHost: readRemoteHost(project.path),
32
+ portRange: project.portRange ?? null,
33
+ portRangeCode: project.portRangeCode ?? null,
34
+ plansFolder: project.description.config?.plans?.folder ?? null,
35
+ ticketIdPattern: project.description.config?.ticketIdPattern ?? null,
36
+ configSource: project.description.source,
37
+ companion: project.description.companion,
38
+ locations: project.description.locations,
39
+ workspaces: project.workspaces,
40
+ worktrees: readWorktrees(project.path),
41
+ };
42
+ }
43
+ function readRemoteHost(projectPath) {
44
+ const remotes = runGit(projectPath, "remote")
45
+ .trimEnd()
46
+ .split("\n")
47
+ .filter((remote) => remote.length > 0)
48
+ .toSorted();
49
+ const orderedRemotes = remotes.includes("origin")
50
+ ? ["origin", ...remotes.filter((remote) => remote !== "origin")]
51
+ : remotes;
52
+ for (const remote of orderedRemotes) {
53
+ const host = remoteHost(runGit(projectPath, "remote", "get-url", "--", remote).trim());
54
+ if (host !== null)
55
+ return host;
56
+ }
57
+ return null;
58
+ }
59
+ function remoteHost(remoteUrl) {
60
+ return URL_WITH_AUTHORITY.test(remoteUrl) ? urlRemoteHost(remoteUrl) : scpRemoteHost(remoteUrl);
61
+ }
62
+ function urlRemoteHost(remoteUrl) {
63
+ try {
64
+ const host = new URL(remoteUrl).hostname;
65
+ return host.length === 0 ? null : host;
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
71
+ function scpRemoteHost(remoteUrl) {
72
+ if (/^[A-Za-z]:[\\/]/u.test(remoteUrl))
73
+ return null;
74
+ const match = /^(?:[^@/:\s]+@)?(\[[^\]]+\]|[^/:\s]+):/u.exec(remoteUrl);
75
+ return match?.[1] ?? null;
76
+ }
77
+ function readWorktrees(projectPath) {
78
+ return runGit(projectPath, "worktree", "list", "--porcelain", "-z")
79
+ .split("\0\0")
80
+ .filter((record) => record.length > 0)
81
+ .map(parseWorktree);
82
+ }
83
+ function parseWorktree(record) {
84
+ const fields = record.split("\0");
85
+ const pathField = fields.find((field) => field.startsWith("worktree "));
86
+ if (pathField === undefined)
87
+ throw new Error("Git returned a worktree record without a path");
88
+ const path = realpathSync(pathField.slice("worktree ".length));
89
+ const branchField = fields.find((field) => field.startsWith("branch "));
90
+ return {
91
+ branch: branchField === undefined ? null : shortBranch(branchField.slice("branch ".length)),
92
+ name: basename(path),
93
+ path,
94
+ };
95
+ }
96
+ function shortBranch(branch) {
97
+ const prefix = "refs/heads/";
98
+ return branch.startsWith(prefix) ? branch.slice(prefix.length) : branch;
99
+ }
100
+ function runGit(projectPath, ...args) {
101
+ try {
102
+ return execFileSync("git", ["-C", projectPath, ...args], {
103
+ encoding: "utf8",
104
+ stdio: ["ignore", "pipe", "pipe"],
105
+ });
106
+ }
107
+ catch (error) {
108
+ throw new Error(`Cannot inspect Git project ${projectPath}: ${errorMessage(error)}`);
109
+ }
110
+ }
@@ -0,0 +1 @@
1
+ export declare function readTemplate(relativePath: string): string;
@@ -0,0 +1,5 @@
1
+ import { readFileSync } from "node:fs";
2
+ // Templates are read at runtime, so edits to a mounted checkout apply without a rebuild.
3
+ export function readTemplate(relativePath) {
4
+ return readFileSync(new URL(`../templates/${relativePath}`, import.meta.url), "utf-8");
5
+ }
package/package.json CHANGED
@@ -1,14 +1,49 @@
1
1
  {
2
2
  "name": "aligndev",
3
- "version": "0.0.0",
3
+ "version": "0.18.0",
4
4
  "license": "CC0-1.0",
5
5
  "author": "Thomas MUR",
6
- "description": "The AlignFirst Dev Kit CLI. Placeholder release: use a later version.",
6
+ "description": "The AlignFirst Dev Kit CLI: the assistant's playbook, coding-agent delegation, and project inventory.",
7
+ "keywords": [
8
+ "ai",
9
+ "agent",
10
+ "alignfirst",
11
+ "cli",
12
+ "claude",
13
+ "codex",
14
+ "git",
15
+ "projects",
16
+ "worktrees"
17
+ ],
7
18
  "homepage": "https://alignfirst.paroi.tech/openclaw-dev-kit",
8
19
  "repository": {
9
20
  "type": "git",
10
21
  "url": "git+https://github.com/paleo/alignfirst.git",
11
22
  "directory": "packages/aligndev"
12
23
  },
13
- "files": ["README.md"]
24
+ "engines": {
25
+ "node": ">=24.16.0"
26
+ },
27
+ "packageManager": "npm@11.19.0",
28
+ "type": "module",
29
+ "bin": {
30
+ "aligndev": "bin/aligndev.mjs"
31
+ },
32
+ "files": [
33
+ "bin",
34
+ "dist",
35
+ "templates"
36
+ ],
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "dependencies": {
41
+ "arktype": "^2.2.3"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "~24.13.3",
45
+ "rimraf": "~6.1.3",
46
+ "typescript": "~7.0.2",
47
+ "vitest": "~5.0.1"
48
+ }
14
49
  }