aligndev 0.0.0 → 0.19.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 +166 -1
- package/bin/aligndev.mjs +3 -0
- package/dist/alignfirst-cli.d.ts +10 -0
- package/dist/alignfirst-cli.js +56 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.js +95 -0
- package/dist/code/claude-agent.d.ts +14 -0
- package/dist/code/claude-agent.js +168 -0
- package/dist/code/code-cli.d.ts +72 -0
- package/dist/code/code-cli.js +630 -0
- package/dist/code/codex-agent.d.ts +15 -0
- package/dist/code/codex-agent.js +168 -0
- package/dist/code/codex-rollout.d.ts +18 -0
- package/dist/code/codex-rollout.js +114 -0
- package/dist/code/coding-agent.d.ts +4 -0
- package/dist/code/coding-agent.js +6 -0
- package/dist/code/models.d.ts +14 -0
- package/dist/code/models.js +89 -0
- package/dist/code/prompt.d.ts +11 -0
- package/dist/code/prompt.js +26 -0
- package/dist/code/quota.d.ts +25 -0
- package/dist/code/quota.js +248 -0
- package/dist/code/run-agent.d.ts +60 -0
- package/dist/code/run-agent.js +212 -0
- package/dist/code/session-file.d.ts +50 -0
- package/dist/code/session-file.js +263 -0
- package/dist/command-form.d.ts +6 -0
- package/dist/command-form.js +7 -0
- package/dist/config.d.ts +22 -0
- package/dist/config.js +72 -0
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +6 -0
- package/dist/guide/code-guide.d.ts +4 -0
- package/dist/guide/code-guide.js +17 -0
- package/dist/guide/guide-cli.d.ts +3 -0
- package/dist/guide/guide-cli.js +81 -0
- package/dist/guide/render-template.d.ts +4 -0
- package/dist/guide/render-template.js +62 -0
- package/dist/guide/topics.d.ts +3 -0
- package/dist/guide/topics.js +16 -0
- package/dist/output.d.ts +3 -0
- package/dist/output.js +1 -0
- package/dist/project/discovery.d.ts +42 -0
- package/dist/project/discovery.js +287 -0
- package/dist/project/format.d.ts +6 -0
- package/dist/project/format.js +31 -0
- package/dist/project/guide.d.ts +3 -0
- package/dist/project/guide.js +69 -0
- package/dist/project/layout.d.ts +36 -0
- package/dist/project/layout.js +128 -0
- package/dist/project/markers.d.ts +18 -0
- package/dist/project/markers.js +90 -0
- package/dist/project/ports.d.ts +3 -0
- package/dist/project/ports.js +55 -0
- package/dist/project/project-cli.d.ts +17 -0
- package/dist/project/project-cli.js +227 -0
- package/dist/project/render.d.ts +10 -0
- package/dist/project/render.js +144 -0
- package/dist/project/status.d.ts +24 -0
- package/dist/project/status.js +110 -0
- package/dist/templates.d.ts +1 -0
- package/dist/templates.js +5 -0
- package/package.json +38 -3
- package/templates/guide/code.md +252 -0
- package/templates/guide/playbook/channel-handling.md +110 -0
- package/templates/guide/playbook/consultation.md +71 -0
- package/templates/guide/playbook/discord-message-tool.md +37 -0
- package/templates/guide/playbook/playbook.md +139 -0
- package/templates/guide/playbook/project-lifecycle.md +100 -0
- package/templates/guide/playbook/project-workspace-setup.md +186 -0
- package/templates/guide/playbook/slack-message-tool.md +23 -0
- package/templates/guide/playbook/working-session.md +588 -0
- package/templates/guide/project.md +33 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
export const RESULT_MARKER = "\n---- Result ----\n";
|
|
4
|
+
// `sessionsDir` is the `.plans`-shaped directory holding the session directories: the resolved
|
|
5
|
+
// `_aligndev` location, which is the resolved `.plans` unless the companion holds a separate tree.
|
|
6
|
+
export function resolveSessionFilePath(sessionsDir, ticket, now, fileExists = existsSync) {
|
|
7
|
+
const dir = join(sessionsDir, ...(ticket === undefined ? [] : [ticket]), "_aligndev");
|
|
8
|
+
const stamp = formatStamp(now);
|
|
9
|
+
let candidate = join(dir, `${stamp}.md`);
|
|
10
|
+
let suffix = 2;
|
|
11
|
+
while (fileExists(candidate)) {
|
|
12
|
+
candidate = join(dir, `${stamp}-${suffix}.md`);
|
|
13
|
+
++suffix;
|
|
14
|
+
}
|
|
15
|
+
return candidate;
|
|
16
|
+
}
|
|
17
|
+
// Writes the initial `running` header. Present before the run starts so an interrupted run still
|
|
18
|
+
// leaves an auditable record; the terminal frontmatter rewrite happens on completion.
|
|
19
|
+
export function writeInitialSessionFile(sessionFilePath, frontmatter) {
|
|
20
|
+
const header = `${serializeFrontmatter(frontmatter)}\n`;
|
|
21
|
+
mkdirSync(dirname(sessionFilePath), { recursive: true });
|
|
22
|
+
writeFileSync(sessionFilePath, header);
|
|
23
|
+
}
|
|
24
|
+
// Appends the result block, then rewrites the frontmatter in place. Append-first keeps the tailed
|
|
25
|
+
// transcript intact until the single terminal rewrite.
|
|
26
|
+
export function applyCompletion(sessionFilePath, update) {
|
|
27
|
+
const content = readFileSync(sessionFilePath, "utf-8");
|
|
28
|
+
const { frontmatter, body } = splitSessionFile(content);
|
|
29
|
+
const updated = {
|
|
30
|
+
...frontmatter,
|
|
31
|
+
status: update.status,
|
|
32
|
+
endedAt: update.endedAt,
|
|
33
|
+
exitReason: update.exitReason,
|
|
34
|
+
sessionId: update.sessionId ?? frontmatter.sessionId,
|
|
35
|
+
contextTokens: update.contextTokens ?? frontmatter.contextTokens,
|
|
36
|
+
contextCompacted: update.contextCompacted ?? frontmatter.contextCompacted,
|
|
37
|
+
contextTokensError: update.contextTokensError ?? frontmatter.contextTokensError,
|
|
38
|
+
};
|
|
39
|
+
const resultBlock = `${RESULT_MARKER}\n${update.result}\n`;
|
|
40
|
+
writeFileSync(sessionFilePath, `${serializeFrontmatter(updated)}\n${body}${resultBlock}`);
|
|
41
|
+
}
|
|
42
|
+
export function appendTranscript(sessionFilePath, text) {
|
|
43
|
+
writeFileSync(sessionFilePath, text, { flag: "a" });
|
|
44
|
+
}
|
|
45
|
+
// Reads back a completed (or in-progress) session file — the durable result handoff a waking
|
|
46
|
+
// OpenClaw agent or a human reads: frontmatter status/sessionId plus the `---- Result ----` block.
|
|
47
|
+
export function readCompletion(sessionFilePath) {
|
|
48
|
+
const content = readFileSync(sessionFilePath, "utf-8");
|
|
49
|
+
const { frontmatter } = splitSessionFile(content);
|
|
50
|
+
// `lastIndexOf`, not `indexOf`: `applyCompletion` always appends the real result block last, so a
|
|
51
|
+
// `---- Result ----` echoed earlier in the transcript body (e.g. the agent printing a session
|
|
52
|
+
// file) must not truncate the actual result.
|
|
53
|
+
const markerIndex = content.lastIndexOf(RESULT_MARKER);
|
|
54
|
+
const result = markerIndex === -1 ? undefined : content.slice(markerIndex + RESULT_MARKER.length).trim();
|
|
55
|
+
return { frontmatter, result };
|
|
56
|
+
}
|
|
57
|
+
export function reconcileSessionFile(sessionFilePath, now = new Date()) {
|
|
58
|
+
const completion = readCompletion(sessionFilePath);
|
|
59
|
+
const update = interruptedCompletion(completion.frontmatter, now);
|
|
60
|
+
if (!update)
|
|
61
|
+
return completion;
|
|
62
|
+
applyCompletion(sessionFilePath, update);
|
|
63
|
+
return {
|
|
64
|
+
frontmatter: {
|
|
65
|
+
...completion.frontmatter,
|
|
66
|
+
status: update.status,
|
|
67
|
+
endedAt: update.endedAt,
|
|
68
|
+
exitReason: update.exitReason,
|
|
69
|
+
},
|
|
70
|
+
result: update.result,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function interruptedCompletion(frontmatter, now) {
|
|
74
|
+
if (frontmatter.status !== "running" || isRecordedProcessAlive(frontmatter))
|
|
75
|
+
return;
|
|
76
|
+
return {
|
|
77
|
+
status: "failed",
|
|
78
|
+
endedAt: now.toISOString(),
|
|
79
|
+
exitReason: "terminated",
|
|
80
|
+
sessionId: frontmatter.sessionId,
|
|
81
|
+
result: `Sealed as interrupted: process ${frontmatter.pid ?? "(unknown)"} is gone.`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function isRecordedProcessAlive(frontmatter) {
|
|
85
|
+
if (frontmatter.pid === null || !isPidAlive(frontmatter.pid))
|
|
86
|
+
return false;
|
|
87
|
+
if (frontmatter.pidStartTime === null)
|
|
88
|
+
return true;
|
|
89
|
+
const currentStartTime = readPidStartTime(frontmatter.pid);
|
|
90
|
+
return currentStartTime === null || currentStartTime === frontmatter.pidStartTime;
|
|
91
|
+
}
|
|
92
|
+
function isPidAlive(pid) {
|
|
93
|
+
try {
|
|
94
|
+
process.kill(pid, 0);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
return err.code !== "ESRCH";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export function readPidStartTime(pid) {
|
|
102
|
+
if (process.platform !== "linux")
|
|
103
|
+
return null;
|
|
104
|
+
try {
|
|
105
|
+
const stat = readFileSync(`/proc/${pid}/stat`, "utf-8");
|
|
106
|
+
const fieldsAfterCommand = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
|
|
107
|
+
return fieldsAfterCommand[19] ?? null;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Lists every active session record: `<sessionsDir>/_aligndev/*.md` plus
|
|
114
|
+
// `<sessionsDir>/<ticket>/_aligndev/*.md`. Archived tickets move under `_archives/` and leave the
|
|
115
|
+
// registry. The session files are the registry — no separate registry file.
|
|
116
|
+
// Self-healing: a `running` record whose pid is gone is a stale leftover from an interrupted run;
|
|
117
|
+
// it gets sealed in passing so the launch guards never block on dead state. The returned records
|
|
118
|
+
// reflect the post-healing state.
|
|
119
|
+
export function listSessionRecords(sessionsDir) {
|
|
120
|
+
const sessionDirs = [join(sessionsDir, "_aligndev")];
|
|
121
|
+
for (const entry of readEntries(sessionsDir)) {
|
|
122
|
+
if (entry.isDirectory() && !entry.name.startsWith("_")) {
|
|
123
|
+
sessionDirs.push(join(sessionsDir, entry.name, "_aligndev"));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const records = [];
|
|
127
|
+
for (const dir of sessionDirs) {
|
|
128
|
+
for (const entry of readEntries(dir)) {
|
|
129
|
+
if (!entry.isFile() || !entry.name.endsWith(".md"))
|
|
130
|
+
continue;
|
|
131
|
+
const path = join(dir, entry.name);
|
|
132
|
+
const completion = reconcileSessionFileOrSkip(path);
|
|
133
|
+
if (completion)
|
|
134
|
+
records.push({ path, frontmatter: completion.frontmatter });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return records;
|
|
138
|
+
}
|
|
139
|
+
export function findNewestSessionFile(dir) {
|
|
140
|
+
let newestName;
|
|
141
|
+
let newestStamp;
|
|
142
|
+
let newestSuffix = -1;
|
|
143
|
+
for (const entry of readEntries(dir)) {
|
|
144
|
+
if (!entry.isFile())
|
|
145
|
+
continue;
|
|
146
|
+
const match = /^(\d{8}-\d{6})(?:-(\d+))?\.md$/.exec(entry.name);
|
|
147
|
+
if (!match)
|
|
148
|
+
continue;
|
|
149
|
+
const stamp = match[1];
|
|
150
|
+
const suffix = Number(match[2] ?? 0);
|
|
151
|
+
if (newestStamp !== undefined && stamp < newestStamp)
|
|
152
|
+
continue;
|
|
153
|
+
if (stamp === newestStamp && suffix <= newestSuffix)
|
|
154
|
+
continue;
|
|
155
|
+
newestName = entry.name;
|
|
156
|
+
newestStamp = stamp;
|
|
157
|
+
newestSuffix = suffix;
|
|
158
|
+
}
|
|
159
|
+
return newestName === undefined ? undefined : join(dir, newestName);
|
|
160
|
+
}
|
|
161
|
+
function readEntries(dir) {
|
|
162
|
+
try {
|
|
163
|
+
return readdirSync(dir, { withFileTypes: true });
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// A malformed file (e.g. missing its frontmatter block) must not block every future launch.
|
|
170
|
+
function reconcileSessionFileOrSkip(path) {
|
|
171
|
+
try {
|
|
172
|
+
return reconcileSessionFile(path);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// --- Frontmatter serialization (dependency-free, round-trips with parseFrontmatter) ---
|
|
179
|
+
export function serializeFrontmatter(frontmatter) {
|
|
180
|
+
const lines = Object.entries(frontmatter).map(([key, value]) => `${key}: ${serializeValue(value)}`);
|
|
181
|
+
return `---\n${lines.join("\n")}\n---\n`;
|
|
182
|
+
}
|
|
183
|
+
function serializeValue(value) {
|
|
184
|
+
if (value === null)
|
|
185
|
+
return "";
|
|
186
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
187
|
+
return String(value);
|
|
188
|
+
return needsQuote(value) ? JSON.stringify(value) : value;
|
|
189
|
+
}
|
|
190
|
+
function needsQuote(value) {
|
|
191
|
+
return value === "" || /[:"#]/.test(value) || value !== value.trim();
|
|
192
|
+
}
|
|
193
|
+
function splitSessionFile(content) {
|
|
194
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n/);
|
|
195
|
+
if (!match)
|
|
196
|
+
throw new Error("Session file is missing its frontmatter block.");
|
|
197
|
+
return { frontmatter: parseFrontmatter(match[1]), body: content.slice(match[0].length) };
|
|
198
|
+
}
|
|
199
|
+
export function parseFrontmatter(block) {
|
|
200
|
+
const map = {};
|
|
201
|
+
for (const line of block.split("\n")) {
|
|
202
|
+
const index = line.indexOf(":");
|
|
203
|
+
if (index === -1)
|
|
204
|
+
continue;
|
|
205
|
+
const key = line.slice(0, index).trim();
|
|
206
|
+
map[key] = parseValue(line.slice(index + 1).trim());
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
status: map.status ?? "running",
|
|
210
|
+
agent: parseAgent(map.agent),
|
|
211
|
+
protocol: map.protocol ?? null,
|
|
212
|
+
ticket: map.ticket ?? null,
|
|
213
|
+
model: map.model ?? null,
|
|
214
|
+
sessionId: map.sessionId ?? null,
|
|
215
|
+
command: map.command ?? "",
|
|
216
|
+
meta: map.meta ?? null,
|
|
217
|
+
pid: parsePid(map.pid),
|
|
218
|
+
pidStartTime: map.pidStartTime ?? null,
|
|
219
|
+
cwd: map.cwd ?? null,
|
|
220
|
+
startedAt: map.startedAt ?? "",
|
|
221
|
+
endedAt: map.endedAt ?? null,
|
|
222
|
+
exitReason: map.exitReason ?? null,
|
|
223
|
+
contextTokens: parseCount(map.contextTokens),
|
|
224
|
+
contextCompacted: map.contextCompacted === "true",
|
|
225
|
+
contextTokensError: map.contextTokensError ?? null,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function parseAgent(raw) {
|
|
229
|
+
return raw === "claude" || raw === "codex" ? raw : null;
|
|
230
|
+
}
|
|
231
|
+
function parsePid(raw) {
|
|
232
|
+
if (raw === null || raw === undefined)
|
|
233
|
+
return null;
|
|
234
|
+
const pid = Number(raw);
|
|
235
|
+
return Number.isSafeInteger(pid) && pid > 0 ? pid : null;
|
|
236
|
+
}
|
|
237
|
+
function parseCount(raw) {
|
|
238
|
+
if (raw === null || raw === undefined)
|
|
239
|
+
return null;
|
|
240
|
+
const count = Number(raw);
|
|
241
|
+
return Number.isSafeInteger(count) && count >= 0 ? count : null;
|
|
242
|
+
}
|
|
243
|
+
function parseValue(raw) {
|
|
244
|
+
if (raw === "")
|
|
245
|
+
return null;
|
|
246
|
+
if (!raw.startsWith('"'))
|
|
247
|
+
return raw;
|
|
248
|
+
try {
|
|
249
|
+
return JSON.parse(raw);
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
// A partially-written or hand-edited file may hold an unterminated/invalid quoted value; fall
|
|
253
|
+
// back to the raw text so the session file stays readable and sealable instead of crashing.
|
|
254
|
+
return raw;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// Local `YYYYMMDD-HHMMSS`.
|
|
258
|
+
function formatStamp(now) {
|
|
259
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
260
|
+
const date = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`;
|
|
261
|
+
const time = `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
262
|
+
return `${date}-${time}`;
|
|
263
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export function resolveCommandForms(env) {
|
|
2
|
+
const userAgent = env.npm_config_user_agent;
|
|
3
|
+
const viaNpx = userAgent !== undefined && userAgent !== "";
|
|
4
|
+
return viaNpx
|
|
5
|
+
? { viaNpx, aligndev: "npx -y aligndev", alignfirst: "npx -y alignfirst" }
|
|
6
|
+
: { viaNpx, aligndev: "aligndev", alignfirst: "alignfirst" };
|
|
7
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type CodingAgent } from "./code/coding-agent.js";
|
|
2
|
+
export declare const PLATFORMS: readonly ["openclaw", "codingAgent"];
|
|
3
|
+
export type Platform = (typeof PLATFORMS)[number];
|
|
4
|
+
export interface AligndevConfig {
|
|
5
|
+
path: string;
|
|
6
|
+
platform: Platform;
|
|
7
|
+
projectsRoot?: ProjectsRoot;
|
|
8
|
+
code: CodeConfig;
|
|
9
|
+
}
|
|
10
|
+
export interface ProjectsRoot {
|
|
11
|
+
path: string;
|
|
12
|
+
written: string;
|
|
13
|
+
}
|
|
14
|
+
export interface CodeConfig {
|
|
15
|
+
agent: CodingAgent;
|
|
16
|
+
models?: string[];
|
|
17
|
+
skipPermissions: boolean;
|
|
18
|
+
unset: string[];
|
|
19
|
+
}
|
|
20
|
+
export declare function loadConfig(home: string): AligndevConfig | undefined;
|
|
21
|
+
export declare function missingConfigMessage(home: string): string;
|
|
22
|
+
export declare function requireProjectsRoot(config: AligndevConfig): ProjectsRoot;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
|
+
import { type } from "arktype";
|
|
4
|
+
import { CODING_AGENTS } from "./code/coding-agent.js";
|
|
5
|
+
import { errorMessage } from "./errors.js";
|
|
6
|
+
export const PLATFORMS = ["openclaw", "codingAgent"];
|
|
7
|
+
const codeSchema = type({
|
|
8
|
+
"+": "reject",
|
|
9
|
+
agent: type.enumerated(...CODING_AGENTS),
|
|
10
|
+
"models?": "string[]",
|
|
11
|
+
"skipPermissions?": "boolean",
|
|
12
|
+
"unset?": "string[]",
|
|
13
|
+
});
|
|
14
|
+
const configSchema = type({
|
|
15
|
+
"+": "reject",
|
|
16
|
+
platform: type.enumerated(...PLATFORMS),
|
|
17
|
+
"projectsRoot?": "string > 0",
|
|
18
|
+
code: codeSchema,
|
|
19
|
+
});
|
|
20
|
+
// An absent file is a normal state: a machine where aligndev is not configured.
|
|
21
|
+
export function loadConfig(home) {
|
|
22
|
+
const path = configPath(home);
|
|
23
|
+
if (!existsSync(path))
|
|
24
|
+
return;
|
|
25
|
+
const value = parseConfigFile(path);
|
|
26
|
+
return {
|
|
27
|
+
path,
|
|
28
|
+
platform: value.platform,
|
|
29
|
+
...(value.projectsRoot === undefined
|
|
30
|
+
? {}
|
|
31
|
+
: { projectsRoot: resolveProjectsRoot(value.projectsRoot, home, path) }),
|
|
32
|
+
code: {
|
|
33
|
+
agent: value.code.agent,
|
|
34
|
+
...(value.code.models === undefined ? {} : { models: value.code.models }),
|
|
35
|
+
skipPermissions: value.code.skipPermissions ?? false,
|
|
36
|
+
unset: value.code.unset ?? [],
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function configPath(home) {
|
|
41
|
+
return join(home, ".config", "alignfirst", "aligndev.config.json");
|
|
42
|
+
}
|
|
43
|
+
function parseConfigFile(path) {
|
|
44
|
+
let value;
|
|
45
|
+
try {
|
|
46
|
+
value = JSON.parse(readFileSync(path, "utf8"));
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
throw invalidConfig(path, errorMessage(error));
|
|
50
|
+
}
|
|
51
|
+
const config = configSchema(value);
|
|
52
|
+
if (config instanceof type.errors)
|
|
53
|
+
throw invalidConfig(path, config.summary.split("\n", 1)[0]);
|
|
54
|
+
return config;
|
|
55
|
+
}
|
|
56
|
+
function invalidConfig(path, detail) {
|
|
57
|
+
return new Error(`Error: invalid aligndev config ${path}: ${detail}`);
|
|
58
|
+
}
|
|
59
|
+
function resolveProjectsRoot(written, home, path) {
|
|
60
|
+
if (written.startsWith("~/"))
|
|
61
|
+
return { path: join(home, written.slice(2)), written };
|
|
62
|
+
return { path: isAbsolute(written) ? written : resolve(dirname(path), written), written };
|
|
63
|
+
}
|
|
64
|
+
export function missingConfigMessage(home) {
|
|
65
|
+
return (`Error: no aligndev config at ${configPath(home)}. Create it with "platform" ` +
|
|
66
|
+
`(${PLATFORMS.join(" or ")}) and "code.agent" (${CODING_AGENTS.join(" or ")}).`);
|
|
67
|
+
}
|
|
68
|
+
export function requireProjectsRoot(config) {
|
|
69
|
+
if (config.projectsRoot !== undefined)
|
|
70
|
+
return config.projectsRoot;
|
|
71
|
+
throw new Error(`Error: projectsRoot is missing from the aligndev config ${config.path}.`);
|
|
72
|
+
}
|
package/dist/errors.d.ts
ADDED
package/dist/errors.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CodingAgent } from "../code/coding-agent.js";
|
|
2
|
+
import type { CommandForms } from "../command-form.js";
|
|
3
|
+
import type { Platform } from "../config.js";
|
|
4
|
+
export declare function renderCodeGuide(platform: Platform, agent: CodingAgent, models: readonly string[], forms: CommandForms): string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { renderGuideTemplate } from "./render-template.js";
|
|
2
|
+
export function renderCodeGuide(platform, agent, models, forms) {
|
|
3
|
+
return renderGuideTemplate("code.md", platform, forms, {
|
|
4
|
+
AGENT: agent,
|
|
5
|
+
AUTH_COMMAND: agent === "claude" ? "`claude`, then `/login`" : "`codex login`",
|
|
6
|
+
PERMISSIONS: agent === "claude"
|
|
7
|
+
? "Normal runs use `--permission-mode auto`, plus `--add-dir <companion>` when the project's AlignFirst files live in its companion directory. `code.skipPermissions: true` in the aligndev config selects `--dangerously-skip-permissions`"
|
|
8
|
+
: "Normal runs use `--sandbox workspace-write`, plus `--add-dir <companion>` when the project's AlignFirst files live in its companion directory. `code.skipPermissions: true` in the aligndev config selects `--dangerously-bypass-approvals-and-sandbox`",
|
|
9
|
+
MODELS: models.map((model) => `\`${model}\``).join(", "),
|
|
10
|
+
ALIGNFIRST_SETUP: forms.viaNpx
|
|
11
|
+
? "The project must be prepared for AlignFirst. `npx -y aligndev` runs the `alignfirst` CLI through `npx`."
|
|
12
|
+
: "The project must be prepared for AlignFirst, with the `alignfirst` CLI installed (`npm install -g alignfirst`).",
|
|
13
|
+
ALIGNFIRST_USE: forms.viaNpx
|
|
14
|
+
? "`npx -y aligndev code` runs the `alignfirst` CLI through `npx`, and so does the coder: it runs `npx -y alignfirst guide <protocol>` in the project."
|
|
15
|
+
: "`aligndev code` requires the `alignfirst` CLI on `PATH`. The coder runs `alignfirst guide <protocol>` in the project, so the protocols come from the installed CLI.",
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { resolveModels } from "../code/models.js";
|
|
3
|
+
import { PLATFORMS, requireProjectsRoot } from "../config.js";
|
|
4
|
+
import { errorMessage } from "../errors.js";
|
|
5
|
+
import { renderProjectsGuideForRoot } from "../project/project-cli.js";
|
|
6
|
+
import { renderCodeGuide } from "./code-guide.js";
|
|
7
|
+
import { renderGuideTemplate } from "./render-template.js";
|
|
8
|
+
import { PLAYBOOK_DISPATCHER, PLAYBOOK_TOPICS } from "./topics.js";
|
|
9
|
+
export function runGuide(tokens, config, ctx) {
|
|
10
|
+
try {
|
|
11
|
+
const args = parseGuideArgs(tokens);
|
|
12
|
+
ctx.stdout.write(args.help ? renderUsage(ctx.forms) : `${renderTopic(args, config, ctx)}\n`);
|
|
13
|
+
return 0;
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
ctx.stderr.write(`${errorMessage(error)}\n`);
|
|
17
|
+
return 1;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function parseGuideArgs(tokens) {
|
|
21
|
+
const { values, positionals } = parseArgs({
|
|
22
|
+
args: tokens,
|
|
23
|
+
options: {
|
|
24
|
+
root: { type: "string" },
|
|
25
|
+
help: { type: "boolean", short: "h", default: false },
|
|
26
|
+
},
|
|
27
|
+
strict: true,
|
|
28
|
+
allowPositionals: true,
|
|
29
|
+
});
|
|
30
|
+
if (positionals.length > 1)
|
|
31
|
+
throw new Error("Error: `aligndev guide` takes at most one topic.");
|
|
32
|
+
const [topic] = positionals;
|
|
33
|
+
if (values.root !== undefined && topic !== "project") {
|
|
34
|
+
throw new Error("Error: --root is valid only with `aligndev guide project`.");
|
|
35
|
+
}
|
|
36
|
+
return { topic, root: values.root, help: values.help };
|
|
37
|
+
}
|
|
38
|
+
function renderUsage(forms) {
|
|
39
|
+
const playbookTopics = PLATFORMS.map((platform) => ` ${platform}: ${PLAYBOOK_TOPICS[platform].join(", ")}`);
|
|
40
|
+
return `Usage:
|
|
41
|
+
${forms.aligndev} guide [<topic>] [--root <path>]
|
|
42
|
+
${forms.aligndev} guide --help
|
|
43
|
+
|
|
44
|
+
Without a topic, prints the playbook of the platform set in the aligndev config.
|
|
45
|
+
|
|
46
|
+
Topics:
|
|
47
|
+
code The delegation guide for \`${forms.aligndev} code\`.
|
|
48
|
+
project The projects guide, openclaw only. --root overrides projectsRoot in the aligndev config.
|
|
49
|
+
|
|
50
|
+
Playbook topics, by platform:
|
|
51
|
+
${playbookTopics.join("\n")}
|
|
52
|
+
`;
|
|
53
|
+
}
|
|
54
|
+
function renderTopic(args, config, ctx) {
|
|
55
|
+
if (args.topic === "code")
|
|
56
|
+
return renderCodeTopic(config, ctx.forms);
|
|
57
|
+
if (args.topic === "project" && config.platform === "openclaw") {
|
|
58
|
+
return renderProjectsGuideForRoot({ ...ctx, projectsRoot: config.projectsRoot }, args.root);
|
|
59
|
+
}
|
|
60
|
+
return renderPlaybookTopic(args.topic, config, ctx.forms);
|
|
61
|
+
}
|
|
62
|
+
function renderCodeTopic(config, forms) {
|
|
63
|
+
const { code } = config;
|
|
64
|
+
const models = resolveModels(code.agent, code.models);
|
|
65
|
+
return renderCodeGuide(config.platform, code.agent, models, forms);
|
|
66
|
+
}
|
|
67
|
+
function renderPlaybookTopic(topic, config, forms) {
|
|
68
|
+
if (topic !== undefined)
|
|
69
|
+
assertPlaybookTopic(topic, config.platform);
|
|
70
|
+
const values = config.platform === "openclaw"
|
|
71
|
+
? { PROJECTS_ROOT: requireProjectsRoot(config).written }
|
|
72
|
+
: undefined;
|
|
73
|
+
return renderGuideTemplate(`playbook/${topic ?? PLAYBOOK_DISPATCHER}.md`, config.platform, forms, values);
|
|
74
|
+
}
|
|
75
|
+
function assertPlaybookTopic(topic, platform) {
|
|
76
|
+
const topics = PLAYBOOK_TOPICS[platform];
|
|
77
|
+
if (topics.includes(topic))
|
|
78
|
+
return;
|
|
79
|
+
const specialTopics = platform === "openclaw" ? ["code", "project"] : ["code"];
|
|
80
|
+
throw new Error(`Error: unknown guide topic "${topic}". Topics: ${[...specialTopics, ...topics].join(", ")}.`);
|
|
81
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CommandForms } from "../command-form.js";
|
|
2
|
+
import { type Platform } from "../config.js";
|
|
3
|
+
export declare function renderGuideTemplate(name: string, platform: Platform, forms: CommandForms, values?: Readonly<Record<string, string>>): string;
|
|
4
|
+
export declare function renderPlatformBlocks(text: string, platform: Platform, templateName: string): string;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { PLATFORMS } from "../config.js";
|
|
2
|
+
import { readTemplate } from "../templates.js";
|
|
3
|
+
const MARKER = /^\{\{([#/])([A-Za-z]+)\}\}$/;
|
|
4
|
+
// Renders `templates/guide/<name>`: the platform blocks first, then the placeholders — the command
|
|
5
|
+
// forms and every key of `values`.
|
|
6
|
+
export function renderGuideTemplate(name, platform, forms, values = {}) {
|
|
7
|
+
const placeholders = {
|
|
8
|
+
ALIGNDEV: forms.aligndev,
|
|
9
|
+
ALIGNFIRST: forms.alignfirst,
|
|
10
|
+
...values,
|
|
11
|
+
};
|
|
12
|
+
let text = renderPlatformBlocks(readTemplate(`guide/${name}`), platform, name);
|
|
13
|
+
for (const [key, value] of Object.entries(placeholders)) {
|
|
14
|
+
text = text.replaceAll(`{{${key}}}`, value);
|
|
15
|
+
}
|
|
16
|
+
return text.trimEnd();
|
|
17
|
+
}
|
|
18
|
+
// A block is `{{#<platform>}}` … `{{/<platform>}}`, each marker alone on its line. The active
|
|
19
|
+
// platform's blocks keep their content; the others are removed. Blocks do not nest. The runs of
|
|
20
|
+
// empty lines that removed blocks leave collapse to one.
|
|
21
|
+
export function renderPlatformBlocks(text, platform, templateName) {
|
|
22
|
+
const kept = [];
|
|
23
|
+
let open;
|
|
24
|
+
for (const [index, line] of text.split("\n").entries()) {
|
|
25
|
+
const marker = MARKER.exec(line);
|
|
26
|
+
if (marker === null) {
|
|
27
|
+
if (open === undefined || open.platform === platform)
|
|
28
|
+
kept.push(line);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const [, kind, name] = marker;
|
|
32
|
+
const lineNumber = index + 1;
|
|
33
|
+
if (!isPlatform(name)) {
|
|
34
|
+
throw blockError(templateName, lineNumber, `unknown platform "${name}"`);
|
|
35
|
+
}
|
|
36
|
+
if (kind === "#") {
|
|
37
|
+
if (open !== undefined) {
|
|
38
|
+
throw blockError(templateName, lineNumber, `block "${name}" nested in "${open.platform}"`);
|
|
39
|
+
}
|
|
40
|
+
open = { platform: name, line: lineNumber };
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
if (open?.platform !== name) {
|
|
44
|
+
throw blockError(templateName, lineNumber, `closing marker "${name}" without its block`);
|
|
45
|
+
}
|
|
46
|
+
open = undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (open !== undefined) {
|
|
50
|
+
throw blockError(templateName, open.line, `block "${open.platform}" is not closed`);
|
|
51
|
+
}
|
|
52
|
+
return collapseEmptyLines(kept).join("\n");
|
|
53
|
+
}
|
|
54
|
+
function isPlatform(name) {
|
|
55
|
+
return PLATFORMS.includes(name);
|
|
56
|
+
}
|
|
57
|
+
function blockError(templateName, line, detail) {
|
|
58
|
+
return new Error(`Error: template ${templateName}, line ${line}: ${detail}.`);
|
|
59
|
+
}
|
|
60
|
+
function collapseEmptyLines(lines) {
|
|
61
|
+
return lines.filter((line, index) => line !== "" || lines[index - 1] !== "");
|
|
62
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// The template rendered by `aligndev guide` without a topic.
|
|
2
|
+
export const PLAYBOOK_DISPATCHER = "playbook";
|
|
3
|
+
// The playbook topics each platform serves. Every topic is one file of `templates/guide/playbook/`,
|
|
4
|
+
// shared by the platforms through platform blocks.
|
|
5
|
+
export const PLAYBOOK_TOPICS = {
|
|
6
|
+
openclaw: [
|
|
7
|
+
"channel-handling",
|
|
8
|
+
"working-session",
|
|
9
|
+
"project-workspace-setup",
|
|
10
|
+
"project-lifecycle",
|
|
11
|
+
"consultation",
|
|
12
|
+
"slack-message-tool",
|
|
13
|
+
"discord-message-tool",
|
|
14
|
+
],
|
|
15
|
+
codingAgent: ["working-session", "project-workspace-setup", "consultation"],
|
|
16
|
+
};
|
package/dist/output.d.ts
ADDED
package/dist/output.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type ProjectReport } from "./layout.js";
|
|
2
|
+
import { type MarkerPortRange, type PortRange, type ProjectsMarker } from "./markers.js";
|
|
3
|
+
export interface ProjectInventory {
|
|
4
|
+
root: string;
|
|
5
|
+
directories: ProjectsDirectory[];
|
|
6
|
+
projects: DiscoveredProject[];
|
|
7
|
+
issues: InventoryIssue[];
|
|
8
|
+
}
|
|
9
|
+
export interface ProjectsDirectory {
|
|
10
|
+
path: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
portRanges?: MarkerPortRange[];
|
|
13
|
+
others: string[];
|
|
14
|
+
}
|
|
15
|
+
export interface DiscoveredProject {
|
|
16
|
+
name: string;
|
|
17
|
+
path: string;
|
|
18
|
+
directory: string;
|
|
19
|
+
description: ProjectReport;
|
|
20
|
+
portRange?: PortRange;
|
|
21
|
+
portRangeCode?: string;
|
|
22
|
+
workspaces: string[];
|
|
23
|
+
}
|
|
24
|
+
export interface InventoryIssue {
|
|
25
|
+
path: string;
|
|
26
|
+
message: string;
|
|
27
|
+
conflict?: PortConflict;
|
|
28
|
+
}
|
|
29
|
+
export interface PortConflict {
|
|
30
|
+
left: ProjectPortClaim;
|
|
31
|
+
right: ProjectPortClaim;
|
|
32
|
+
}
|
|
33
|
+
interface ProjectPortClaim {
|
|
34
|
+
path: string;
|
|
35
|
+
portRange: PortRange;
|
|
36
|
+
}
|
|
37
|
+
export interface InventoryContext {
|
|
38
|
+
env: NodeJS.ProcessEnv;
|
|
39
|
+
alignfirstCommand: string[];
|
|
40
|
+
}
|
|
41
|
+
export declare function buildInventory(root: string, marker: ProjectsMarker, ctx: InventoryContext): ProjectInventory;
|
|
42
|
+
export {};
|