alignfirst 0.6.0 → 0.7.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.
- package/README.md +6 -3
- package/dist/cli.js +4 -0
- package/dist/commands/companion.d.ts +2 -0
- package/dist/commands/companion.js +36 -0
- package/dist/commands/doctor.js +3 -3
- package/dist/project-layout.d.ts +14 -1
- package/dist/project-layout.js +55 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -96,6 +96,7 @@ Use your alignfirst-setup-guide skill. Set up AlignFirst in this project without
|
|
|
96
96
|
- `docmap` — Browse project documentation.
|
|
97
97
|
- `conventions` — Print the effective project conventions.
|
|
98
98
|
- `context` — Print the conventions, the project instructions from `.alignfirst.md`, the documentation map when `docs/` exists, and the protocol aliases.
|
|
99
|
+
- `companion add` — Register the current project in the companion registry and create its companion directory.
|
|
99
100
|
- `config` — Report the effective project configuration, the companion directory and the location of each AlignFirst file.
|
|
100
101
|
- `doctor` — Diagnose an AlignFirst setup.
|
|
101
102
|
|
|
@@ -107,7 +108,7 @@ Run `alignfirst --help` for command usage or `alignfirst guide` to choose a prot
|
|
|
107
108
|
|
|
108
109
|
## Companion directories
|
|
109
110
|
|
|
110
|
-
A companion directory holds a project's AlignFirst files outside its repository, so the repository stays untouched. `~/.alignfirst/companions.json
|
|
111
|
+
A companion directory holds a project's AlignFirst files outside its repository, so the repository stays untouched. The companion registry, `~/.alignfirst/companions/registry.json`, declares which projects have one:
|
|
111
112
|
|
|
112
113
|
```json
|
|
113
114
|
{
|
|
@@ -121,13 +122,15 @@ A companion directory holds a project's AlignFirst files outside its repository,
|
|
|
121
122
|
|
|
122
123
|
- `paths` — the projects, by absolute or `~/` path. Each value sets flags for the items a companion can hold: `.alignfirst.json`, `.alignfirst.md`, `DEVELOPERS.md`, `docs`, `.plans` and `_aligndev`. A flag is `true`, `false` or `"auto"`.
|
|
123
124
|
|
|
124
|
-
An absent
|
|
125
|
+
An absent registry means no project has a companion. An invalid registry makes every command fail; `doctor` reports it and continues.
|
|
126
|
+
|
|
127
|
+
Run `alignfirst companion add` in a project to register it. It adds the project's main worktree path with every item on `"auto"`, unless a key already matches it, and creates the companion directory. Set the flags by editing the registry.
|
|
125
128
|
|
|
126
129
|
### Matching
|
|
127
130
|
|
|
128
131
|
A key matches a project when it names the project's main worktree or one of its ancestors, so every worktree of a project shares one companion. `"~": {}` matches every project under the home directory. For each item, the longest matching key that sets the flag wins, and an unset flag is `"auto"`. A bare repository or a directory outside git has no companion.
|
|
129
132
|
|
|
130
|
-
The companion directory is `~/.alignfirst/companions/<name>`. The name is the main worktree path relative to the home directory, or the absolute path without its leading `/` outside it, with every `/` replaced by `_`. For example, `~/projects/client-api` gets `~/.alignfirst/companions/projects_client-api/`. To keep the companions elsewhere, make `~/.alignfirst/companions` a symlink.
|
|
133
|
+
The companion directory is `~/.alignfirst/companions/<name>`. The name is the main worktree path relative to the home directory, or the absolute path without its leading `/` outside it, with every `/` replaced by `_`. For example, `~/projects/client-api` gets `~/.alignfirst/companions/projects_client-api/`. To keep the companions and their registry elsewhere, make `~/.alignfirst/companions` a symlink.
|
|
131
134
|
|
|
132
135
|
### Resolution
|
|
133
136
|
|
package/dist/cli.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { CliError } from "./cli-error.js";
|
|
4
4
|
import { resolveCommandForm } from "./command-form.js";
|
|
5
|
+
import { runCompanion } from "./commands/companion.js";
|
|
5
6
|
import { runConfig } from "./commands/config.js";
|
|
6
7
|
import { runContext } from "./commands/context.js";
|
|
7
8
|
import { runConventions } from "./commands/conventions.js";
|
|
@@ -67,6 +68,7 @@ Usage:
|
|
|
67
68
|
${ctx.form} docmap [<arguments>]
|
|
68
69
|
${ctx.form} conventions
|
|
69
70
|
${ctx.form} context
|
|
71
|
+
${ctx.form} companion add
|
|
70
72
|
${ctx.form} config [--json]
|
|
71
73
|
${ctx.form} doctor
|
|
72
74
|
${ctx.form} --help
|
|
@@ -89,6 +91,8 @@ function dispatch(ctx, command, args) {
|
|
|
89
91
|
return runConventions(ctx, args);
|
|
90
92
|
case "context":
|
|
91
93
|
return runContext(ctx, args);
|
|
94
|
+
case "companion":
|
|
95
|
+
return runCompanion(ctx, args);
|
|
92
96
|
case "config":
|
|
93
97
|
return runConfig(ctx, args);
|
|
94
98
|
case "doctor":
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { CliError } from "../cli-error.js";
|
|
2
|
+
import { parseBareCommandArgs } from "../parse-args.js";
|
|
3
|
+
import { addCompanion } from "../project-layout.js";
|
|
4
|
+
export function runCompanion(ctx, args) {
|
|
5
|
+
const [command, ...rest] = args;
|
|
6
|
+
switch (command) {
|
|
7
|
+
case "add":
|
|
8
|
+
return runAdd(ctx, rest);
|
|
9
|
+
case "--help":
|
|
10
|
+
case "-h":
|
|
11
|
+
ctx.stdout.write(companionUsage(ctx));
|
|
12
|
+
return 0;
|
|
13
|
+
default:
|
|
14
|
+
throw new CliError(`Unknown or missing companion command.\n\n${companionUsage(ctx)}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function companionUsage(ctx) {
|
|
18
|
+
return `Usage:
|
|
19
|
+
${ctx.form} companion add
|
|
20
|
+
`;
|
|
21
|
+
}
|
|
22
|
+
function runAdd(ctx, args) {
|
|
23
|
+
const usage = `Usage: ${ctx.form} companion add
|
|
24
|
+
|
|
25
|
+
Registers the current project in ~/.alignfirst/companions/registry.json, with every item on
|
|
26
|
+
"auto", and creates its companion directory. A project that a key already matches stays as is.
|
|
27
|
+
`;
|
|
28
|
+
if (parseBareCommandArgs(ctx, args, usage))
|
|
29
|
+
return 0;
|
|
30
|
+
const registration = addCompanion(ctx.cwd, ctx.home);
|
|
31
|
+
ctx.stdout.write(registration.added
|
|
32
|
+
? `Registered ${registration.key} in ${registration.registry}.\n`
|
|
33
|
+
: `Already registered by ${registration.key} in ${registration.registry}.\n`);
|
|
34
|
+
ctx.stdout.write(`Companion: ${registration.dir}\n`);
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -8,7 +8,7 @@ import { parseBareCommandArgs } from "../parse-args.js";
|
|
|
8
8
|
import { resolvePlansMode } from "../plans/mode.js";
|
|
9
9
|
import { findStoppedRebase } from "../plans/rebase.js";
|
|
10
10
|
import { PROJECT_CONFIG_FILENAME, resolveProjectConfig, } from "../project-config.js";
|
|
11
|
-
import {
|
|
11
|
+
import { registryPath, ITEM_NAMES, layoutOf, renderItemLocation, } from "../project-layout.js";
|
|
12
12
|
import { COMMAND_SKILLS, findInstalledSkill } from "../skills.js";
|
|
13
13
|
import { cliRangeResult } from "../version-guard.js";
|
|
14
14
|
export function runDoctor(ctx, args) {
|
|
@@ -67,11 +67,11 @@ function inspectConfig(ctx, resolved) {
|
|
|
67
67
|
return lines;
|
|
68
68
|
}
|
|
69
69
|
function inspectCompanion(ctx) {
|
|
70
|
-
const path =
|
|
70
|
+
const path = registryPath(ctx.home);
|
|
71
71
|
const layout = layoutOf(ctx);
|
|
72
72
|
const file = {
|
|
73
73
|
level: "ok",
|
|
74
|
-
text: `
|
|
74
|
+
text: `registry ${existsSync(path) ? "valid" : "absent"} (${path})`,
|
|
75
75
|
};
|
|
76
76
|
if (layout.companion === null)
|
|
77
77
|
return [file, { level: "ok", text: "none" }];
|
package/dist/project-layout.d.ts
CHANGED
|
@@ -23,8 +23,21 @@ export interface ItemLocation {
|
|
|
23
23
|
}
|
|
24
24
|
export declare function layoutOf(ctx: CommandContext): ProjectLayout;
|
|
25
25
|
export declare function resolveProjectLayout(cwd: string, home: string): ProjectLayout;
|
|
26
|
-
export declare function
|
|
26
|
+
export declare function registryPath(home: string): string;
|
|
27
27
|
/** One line: `<name>: <path> (<in>)`, with `, missing` when absent. */
|
|
28
28
|
export declare function renderItemLocation(name: ItemName, location: ItemLocation): string;
|
|
29
29
|
/** The `_aligndev` tree when it is not the resolved `.plans` and exists. */
|
|
30
30
|
export declare function separateSessionTree(layout: ProjectLayout): string | undefined;
|
|
31
|
+
export interface CompanionRegistration {
|
|
32
|
+
registry: string;
|
|
33
|
+
/** The key added, or the most specific key that already matched. */
|
|
34
|
+
key: string;
|
|
35
|
+
added: boolean;
|
|
36
|
+
/** Absolute. */
|
|
37
|
+
dir: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Registers the main worktree of `cwd` with every item on `"auto"`, unless a key already matches
|
|
41
|
+
* it, and creates its companion directory. Creates the registry when it is missing.
|
|
42
|
+
*/
|
|
43
|
+
export declare function addCompanion(cwd: string, home: string): CompanionRegistration;
|
package/dist/project-layout.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, lstatSync, readFileSync, realpathSync } from "node:fs";
|
|
1
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync, } from "node:fs";
|
|
2
2
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { type } from "arktype";
|
|
4
4
|
import { CliError } from "./cli-error.js";
|
|
@@ -12,8 +12,9 @@ export const ITEM_NAMES = [
|
|
|
12
12
|
".plans",
|
|
13
13
|
"_aligndev",
|
|
14
14
|
];
|
|
15
|
-
// The directory holding the companions. A symlink there moves them elsewhere.
|
|
15
|
+
// The directory holding the companions and their registry. A symlink there moves them elsewhere.
|
|
16
16
|
const COMPANIONS_ROOT = "~/.alignfirst/companions";
|
|
17
|
+
const REGISTRY_FILE = "registry.json";
|
|
17
18
|
const FLAG = "boolean | 'auto'";
|
|
18
19
|
const flagsSchema = type({
|
|
19
20
|
"+": "reject",
|
|
@@ -24,7 +25,7 @@ const flagsSchema = type({
|
|
|
24
25
|
".plans?": FLAG,
|
|
25
26
|
"_aligndev?": FLAG,
|
|
26
27
|
});
|
|
27
|
-
const
|
|
28
|
+
const registrySchema = type({
|
|
28
29
|
"+": "reject",
|
|
29
30
|
paths: type.Record("string", flagsSchema),
|
|
30
31
|
});
|
|
@@ -37,23 +38,23 @@ export function resolveProjectLayout(cwd, home) {
|
|
|
37
38
|
return { companion, locations: resolveLocations(cwd, companion) };
|
|
38
39
|
}
|
|
39
40
|
function resolveCompanion(cwd, home) {
|
|
40
|
-
const
|
|
41
|
-
if (
|
|
41
|
+
const registry = readRegistry(home);
|
|
42
|
+
if (registry === undefined)
|
|
42
43
|
return null;
|
|
43
44
|
const mainWorktree = findMainWorktree(cwd);
|
|
44
45
|
if (mainWorktree === undefined)
|
|
45
46
|
return null;
|
|
46
47
|
const realHome = realOrResolved(home);
|
|
47
|
-
const matches = matchingEntries(
|
|
48
|
+
const matches = matchingEntries(registry, mainWorktree, realHome);
|
|
48
49
|
if (matches.length === 0)
|
|
49
50
|
return null;
|
|
50
51
|
const flags = mergeFlags(matches);
|
|
51
|
-
assertValidFlags(
|
|
52
|
-
const dir =
|
|
52
|
+
assertValidFlags(registry, flags, matches);
|
|
53
|
+
const dir = companionDir(mainWorktree, realHome);
|
|
53
54
|
return { dir, exists: pathExists(dir), entries: matches.map((match) => match.key), flags };
|
|
54
55
|
}
|
|
55
|
-
function
|
|
56
|
-
const path =
|
|
56
|
+
function readRegistry(home) {
|
|
57
|
+
const path = registryPath(home);
|
|
57
58
|
if (!pathExists(path))
|
|
58
59
|
return;
|
|
59
60
|
let value;
|
|
@@ -61,20 +62,20 @@ function readCompanionsFile(home) {
|
|
|
61
62
|
value = JSON.parse(readFileSync(path, "utf-8"));
|
|
62
63
|
}
|
|
63
64
|
catch (error) {
|
|
64
|
-
throw
|
|
65
|
+
throw invalidRegistry(path, errorMessage(error));
|
|
65
66
|
}
|
|
66
|
-
const file =
|
|
67
|
+
const file = registrySchema(value);
|
|
67
68
|
if (file instanceof type.errors)
|
|
68
|
-
throw
|
|
69
|
+
throw invalidRegistry(path, file.summary.split("\n", 1)[0]);
|
|
69
70
|
const badKey = Object.keys(file.paths).find((key) => !isUserPath(key));
|
|
70
71
|
if (badKey !== undefined)
|
|
71
|
-
throw
|
|
72
|
+
throw invalidRegistry(path, `paths key must be an absolute path or start with ~/: ${badKey}`);
|
|
72
73
|
return { path, paths: file.paths };
|
|
73
74
|
}
|
|
74
|
-
export function
|
|
75
|
-
return join(home, ".alignfirst", "companions
|
|
75
|
+
export function registryPath(home) {
|
|
76
|
+
return join(home, ".alignfirst", "companions", REGISTRY_FILE);
|
|
76
77
|
}
|
|
77
|
-
function
|
|
78
|
+
function invalidRegistry(path, detail) {
|
|
78
79
|
return new CliError(`Invalid ${path}: ${detail}`);
|
|
79
80
|
}
|
|
80
81
|
function isUserPath(value) {
|
|
@@ -95,8 +96,8 @@ function normalizePath(value, realHome) {
|
|
|
95
96
|
function realOrResolved(path) {
|
|
96
97
|
return existsSync(path) ? realpathSync(path) : resolve(path);
|
|
97
98
|
}
|
|
98
|
-
function matchingEntries(
|
|
99
|
-
return Object.entries(
|
|
99
|
+
function matchingEntries(registry, mainWorktree, realHome) {
|
|
100
|
+
return Object.entries(registry.paths)
|
|
100
101
|
.map(([key, flags]) => ({ key, path: normalizePath(key, realHome), flags }))
|
|
101
102
|
.filter((entry) => isSameOrInside(mainWorktree, entry.path))
|
|
102
103
|
.toSorted((left, right) => right.path.length - left.path.length);
|
|
@@ -115,11 +116,14 @@ function mergeFlags(matches) {
|
|
|
115
116
|
_aligndev: flagOf("_aligndev"),
|
|
116
117
|
};
|
|
117
118
|
}
|
|
118
|
-
function assertValidFlags(
|
|
119
|
+
function assertValidFlags(registry, flags, matches) {
|
|
119
120
|
if (flags._aligndev !== true || flags[".plans"] !== "auto")
|
|
120
121
|
return;
|
|
121
122
|
const keys = matches.map((match) => match.key).join(", ");
|
|
122
|
-
throw
|
|
123
|
+
throw invalidRegistry(registry.path, `"_aligndev": true requires ".plans" set to true or false (matching keys: ${keys})`);
|
|
124
|
+
}
|
|
125
|
+
function companionDir(mainWorktree, realHome) {
|
|
126
|
+
return join(normalizePath(COMPANIONS_ROOT, realHome), companionName(mainWorktree, realHome));
|
|
123
127
|
}
|
|
124
128
|
function companionName(mainWorktree, realHome) {
|
|
125
129
|
const name = mainWorktree !== realHome && isSameOrInside(mainWorktree, realHome)
|
|
@@ -171,3 +175,33 @@ export function separateSessionTree(layout) {
|
|
|
171
175
|
return;
|
|
172
176
|
return sessions.path;
|
|
173
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Registers the main worktree of `cwd` with every item on `"auto"`, unless a key already matches
|
|
180
|
+
* it, and creates its companion directory. Creates the registry when it is missing.
|
|
181
|
+
*/
|
|
182
|
+
export function addCompanion(cwd, home) {
|
|
183
|
+
const mainWorktree = findMainWorktree(cwd);
|
|
184
|
+
if (mainWorktree === undefined)
|
|
185
|
+
throw new CliError("A companion needs a git repository with a main worktree.");
|
|
186
|
+
const realHome = realOrResolved(home);
|
|
187
|
+
const registry = readRegistry(home) ?? { path: registryPath(home), paths: {} };
|
|
188
|
+
const match = matchingEntries(registry, mainWorktree, realHome)[0];
|
|
189
|
+
const key = match?.key ?? userPathOf(mainWorktree, realHome);
|
|
190
|
+
if (match === undefined)
|
|
191
|
+
writeRegistry(registry, key);
|
|
192
|
+
const dir = companionDir(mainWorktree, realHome);
|
|
193
|
+
mkdirSync(dir, { recursive: true });
|
|
194
|
+
return { registry: registry.path, key, added: match === undefined, dir };
|
|
195
|
+
}
|
|
196
|
+
function userPathOf(path, realHome) {
|
|
197
|
+
if (path === realHome)
|
|
198
|
+
return "~";
|
|
199
|
+
return isSameOrInside(path, realHome) ? `~/${relative(realHome, path)}` : path;
|
|
200
|
+
}
|
|
201
|
+
function writeRegistry(registry, key) {
|
|
202
|
+
mkdirSync(dirname(registry.path), { recursive: true });
|
|
203
|
+
const paths = { ...registry.paths, [key]: {} };
|
|
204
|
+
const tmpPath = `${registry.path}.${process.pid}.tmp`;
|
|
205
|
+
writeFileSync(tmpPath, `${JSON.stringify({ paths }, undefined, 2)}\n`);
|
|
206
|
+
renameSync(tmpPath, registry.path);
|
|
207
|
+
}
|