@archstone/cli 0.8.0 → 0.10.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/dist/index.js +412 -5
- package/dist/index.js.map +1 -1
- package/package.json +20 -5
package/dist/index.js
CHANGED
|
@@ -1,13 +1,414 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { writeFileSync } from "fs";
|
|
5
|
-
import { resolve } from "path";
|
|
4
|
+
import { writeFileSync as writeFileSync2 } from "fs";
|
|
5
|
+
import { resolve as resolve2 } from "path";
|
|
6
6
|
import { createServer } from "http";
|
|
7
7
|
import { load } from "@archstone/schema";
|
|
8
8
|
import { validateSemantics, compile } from "@archstone/compiler";
|
|
9
9
|
import { Registry, buildRegistry, serveStdio, runVerify } from "@archstone/runtime";
|
|
10
10
|
import { createHttpHandler } from "@archstone/runtime/http";
|
|
11
|
+
|
|
12
|
+
// src/init.ts
|
|
13
|
+
import { createInterface } from "readline/promises";
|
|
14
|
+
import { existsSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
15
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "path";
|
|
16
|
+
import {
|
|
17
|
+
CAPABILITY_ID_RE,
|
|
18
|
+
COMPANY_ID_RE,
|
|
19
|
+
formatReport,
|
|
20
|
+
isKnown,
|
|
21
|
+
locusCandidates,
|
|
22
|
+
openApiAdapter,
|
|
23
|
+
valueOrUndefined,
|
|
24
|
+
validateDecisionRecord
|
|
25
|
+
} from "@archstone/init";
|
|
26
|
+
import { runInit } from "@archstone/init/loop";
|
|
27
|
+
var MAX_REFERENCE_ROUNDS = 8;
|
|
28
|
+
var INIT_USAGE = [
|
|
29
|
+
"usage: archstone init <spec-file> --out <dir> [options]",
|
|
30
|
+
"",
|
|
31
|
+
" Read an API description, ask you the questions no tool can answer, and write a CDL",
|
|
32
|
+
" manifest the real compiler has already compiled. No LLM is involved, on any path.",
|
|
33
|
+
"",
|
|
34
|
+
" --out <dir> where the manifest goes (required)",
|
|
35
|
+
" --domain <name> the domain half of every capability id (e.g. 'framing')",
|
|
36
|
+
" --company <id> company id, lowercase kebab (e.g. 'acme')",
|
|
37
|
+
" --decisions <file> a Decision Record JSON file, instead of the interactive gate.",
|
|
38
|
+
" Each entry's `operation` is the CANDIDATE KEY, which is",
|
|
39
|
+
" `<METHOD> <path>` with the path INCLUDING the server base path",
|
|
40
|
+
" from `servers[0].url` \u2014 so a document whose `paths:` reads",
|
|
41
|
+
" `/catalog/frames` under a server of `https://api.x.test/api/v1`",
|
|
42
|
+
" has the key `GET /api/v1/catalog/frames`. Run without --decisions",
|
|
43
|
+
" once to see the real keys, or read them off a failed run's report.",
|
|
44
|
+
" Not combinable with --company or --domain, which it answers.",
|
|
45
|
+
" --report <file> also write the report here (default: <out>/INIT-REPORT.md)",
|
|
46
|
+
" --probe OPT-IN, READ-ONLY. Record a golden fixture by making ONE live",
|
|
47
|
+
" request per capability you consent to. Never issued for a",
|
|
48
|
+
" capability whose confirmed effect is not `read`; a non-GET/HEAD",
|
|
49
|
+
" method needs a second, separate confirmation, and is refused",
|
|
50
|
+
" outright when there is no terminal. Off by default.",
|
|
51
|
+
" --non-interactive no prompts. Requires --decisions: `init` never defaults an",
|
|
52
|
+
" `effect`, so with no human and no record there is nothing to do.",
|
|
53
|
+
" --force write into a non-empty directory"
|
|
54
|
+
].join("\n");
|
|
55
|
+
function resolveReference(specFile, key) {
|
|
56
|
+
if (isAbsolute(key) || key.split(/[\\/]/).includes("..")) return void 0;
|
|
57
|
+
const root = dirname(resolve(specFile));
|
|
58
|
+
const target = resolve(root, key);
|
|
59
|
+
if (target !== root && !target.startsWith(root + sep)) return void 0;
|
|
60
|
+
return existsSync(target) && statSync(target).isFile() ? target : void 0;
|
|
61
|
+
}
|
|
62
|
+
function loadSource(adapter, specFile) {
|
|
63
|
+
const input = { origin: relative(process.cwd(), specFile) || specFile, document: readFileSync(specFile, "utf8"), documents: {} };
|
|
64
|
+
const unresolved = [];
|
|
65
|
+
if (!adapter.references) return { input, unresolved };
|
|
66
|
+
for (let round = 0; round < MAX_REFERENCE_ROUNDS; round += 1) {
|
|
67
|
+
const wanted = adapter.references(input).filter((key) => input.documents[key] === void 0 && !unresolved.includes(key));
|
|
68
|
+
if (wanted.length === 0) break;
|
|
69
|
+
for (const key of wanted) {
|
|
70
|
+
const path = resolveReference(specFile, key);
|
|
71
|
+
if (path === void 0) unresolved.push(key);
|
|
72
|
+
else input.documents[key] = readFileSync(path, "utf8");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { input, unresolved };
|
|
76
|
+
}
|
|
77
|
+
function promptFailureKind(error) {
|
|
78
|
+
if (!(error instanceof Error)) return void 0;
|
|
79
|
+
const code = error.code;
|
|
80
|
+
if (error.name === "AbortError" || code === "ABORT_ERR") return "no-more-input";
|
|
81
|
+
if (code === "ERR_USE_AFTER_CLOSE") return "terminal-closed";
|
|
82
|
+
return void 0;
|
|
83
|
+
}
|
|
84
|
+
function terminalAsk(rl) {
|
|
85
|
+
const controller = new AbortController();
|
|
86
|
+
rl.once?.("close", () => controller.abort());
|
|
87
|
+
return {
|
|
88
|
+
async question(text, fallback) {
|
|
89
|
+
const suggestion = fallback !== void 0 && fallback !== "" ? fallback : void 0;
|
|
90
|
+
const prompt = suggestion === void 0 ? text : `${text.trimEnd()} [${suggestion}] `;
|
|
91
|
+
let answer;
|
|
92
|
+
try {
|
|
93
|
+
answer = await rl.question(prompt, { signal: controller.signal });
|
|
94
|
+
} catch (error) {
|
|
95
|
+
const kind = promptFailureKind(error);
|
|
96
|
+
if (kind === void 0) throw error;
|
|
97
|
+
throw new PromptUnavailable(kind);
|
|
98
|
+
}
|
|
99
|
+
return answer.trim() === "" && suggestion !== void 0 ? suggestion : answer;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
var PromptUnavailable = class extends Error {
|
|
104
|
+
constructor(kind) {
|
|
105
|
+
super(kind);
|
|
106
|
+
this.kind = kind;
|
|
107
|
+
this.name = "PromptUnavailable";
|
|
108
|
+
}
|
|
109
|
+
kind;
|
|
110
|
+
};
|
|
111
|
+
async function runGateOverTerminal(draft, rl, args) {
|
|
112
|
+
try {
|
|
113
|
+
return await runGate(draft, terminalAsk(rl), args);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (error instanceof PromptUnavailable) return error.kind;
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
var MAX_PROMPT_ATTEMPTS = 5;
|
|
120
|
+
async function askUntil(ask, question, parse, onInvalid, fallback) {
|
|
121
|
+
for (let attempt = 0; attempt < MAX_PROMPT_ATTEMPTS; attempt += 1) {
|
|
122
|
+
const parsed = parse((await ask.question(question, fallback)).trim());
|
|
123
|
+
if (parsed !== void 0) return parsed;
|
|
124
|
+
onInvalid();
|
|
125
|
+
}
|
|
126
|
+
return void 0;
|
|
127
|
+
}
|
|
128
|
+
async function confirm(ask, text, fallback) {
|
|
129
|
+
const answer = (await ask.question(`${text} [${fallback ? "Y/n" : "y/N"}] `)).trim().toLowerCase();
|
|
130
|
+
if (answer === "") return fallback;
|
|
131
|
+
return answer.startsWith("y");
|
|
132
|
+
}
|
|
133
|
+
var EFFECTS = /* @__PURE__ */ new Set(["read", "write", "irreversible"]);
|
|
134
|
+
async function runGate(draft, ask, args) {
|
|
135
|
+
const companyId = (await ask.question("Company id (lowercase, kebab-case) ", args.company)).trim();
|
|
136
|
+
if (!COMPANY_ID_RE.test(companyId)) {
|
|
137
|
+
console.error(`archstone init: '${companyId}' is not a valid company id (^[a-z][a-z0-9-]*$).`);
|
|
138
|
+
return void 0;
|
|
139
|
+
}
|
|
140
|
+
const companyName = (await ask.question("Company name (for the manifest header) ", valueOrUndefined(draft.company.name))).trim();
|
|
141
|
+
const domain = (await ask.question("Domain for these capabilities (the first half of every id) ", args.domain)).trim();
|
|
142
|
+
const envPrefix = companyId.replace(/-/g, "_").toUpperCase();
|
|
143
|
+
const baseUrlEnvVar = (await ask.question("Env var holding the backend base URL ", `${envPrefix}_API_URL`)).trim();
|
|
144
|
+
const declaresAuth = draft.auth !== void 0 || draft.operations.some((o) => o.auth?.kind === "header");
|
|
145
|
+
const authEnvVar = declaresAuth ? (await ask.question("Env var holding the API credential (never its value) ", `${envPrefix}_API_TOKEN`)).trim() : "";
|
|
146
|
+
const decisions = [];
|
|
147
|
+
let keepAll = false;
|
|
148
|
+
for (const [index, candidate] of draft.operations.entries()) {
|
|
149
|
+
const operation = candidate;
|
|
150
|
+
const summary = valueOrUndefined(operation.description) ?? "";
|
|
151
|
+
console.log("");
|
|
152
|
+
console.log(`[${index + 1}/${draft.operations.length}] ${operation.key}`);
|
|
153
|
+
if (summary) console.log(` ${summary}`);
|
|
154
|
+
const blocking = operation.notes.filter((n) => n.code.startsWith("unsupported") || n.code === "declined");
|
|
155
|
+
for (const n of blocking) console.log(` ! ${n.code}${n.detail ? `: ${n.detail}` : ""}`);
|
|
156
|
+
let keep = keepAll;
|
|
157
|
+
if (!keep) {
|
|
158
|
+
const answer = (await ask.question(" keep as a capability? [y/N/a=keep all remaining] ")).trim().toLowerCase();
|
|
159
|
+
if (answer === "a") {
|
|
160
|
+
keepAll = true;
|
|
161
|
+
keep = true;
|
|
162
|
+
} else keep = answer.startsWith("y");
|
|
163
|
+
}
|
|
164
|
+
if (!keep) {
|
|
165
|
+
decisions.push({ operation: operation.key, keep: false });
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const action = valueOrUndefined(operation.suggestedAction);
|
|
169
|
+
const suggestedId = domain !== "" && action !== void 0 ? `${domain}.${action}` : void 0;
|
|
170
|
+
const capabilityId = (await ask.question(" capability id (domain.action) ", suggestedId)).trim();
|
|
171
|
+
if (!CAPABILITY_ID_RE.test(capabilityId)) {
|
|
172
|
+
console.error(` '${capabilityId}' is not a valid capability id \u2014 skipping this candidate.`);
|
|
173
|
+
decisions.push({ operation: operation.key, keep: false, note: `invalid id '${capabilityId}' supplied at the gate` });
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const prefill = operation.method.toUpperCase() === "GET" && operation.effectHint ? operation.effectHint.value : void 0;
|
|
177
|
+
const effect = await askUntil(
|
|
178
|
+
ask,
|
|
179
|
+
" effect (read | write | irreversible) ",
|
|
180
|
+
(answer) => EFFECTS.has(answer) ? answer : void 0,
|
|
181
|
+
() => console.error(" must be one of: read, write, irreversible"),
|
|
182
|
+
prefill
|
|
183
|
+
);
|
|
184
|
+
if (effect === void 0) {
|
|
185
|
+
console.error("archstone init: no valid `effect` after several attempts \u2014 refusing rather than defaulting one.");
|
|
186
|
+
return void 0;
|
|
187
|
+
}
|
|
188
|
+
let responseLocus;
|
|
189
|
+
const census = locusCandidates(operation.response);
|
|
190
|
+
if (census.candidates.length > 1) {
|
|
191
|
+
console.log(` this response could be read ${census.candidates.length} ways \u2014 which one does this capability return?`);
|
|
192
|
+
for (const [index2, candidate2] of census.candidates.entries()) {
|
|
193
|
+
const shape = candidate2.kind === "root" ? "one object, with fields" : `a list, each with fields`;
|
|
194
|
+
console.log(` ${index2 + 1}. ${shape}: ${candidate2.fields.join(", ")}`);
|
|
195
|
+
console.log(` (${candidate2.id})`);
|
|
196
|
+
}
|
|
197
|
+
const collections = census.candidates.filter((c) => c.kind === "collection");
|
|
198
|
+
const prefill2 = collections.length === 1 ? String(census.candidates.indexOf(collections[0]) + 1) : void 0;
|
|
199
|
+
const picked = await askUntil(
|
|
200
|
+
ask,
|
|
201
|
+
` which one? [1-${census.candidates.length}] `,
|
|
202
|
+
(answer) => {
|
|
203
|
+
const index2 = Number(answer);
|
|
204
|
+
return Number.isInteger(index2) && index2 >= 1 && index2 <= census.candidates.length ? index2 : void 0;
|
|
205
|
+
},
|
|
206
|
+
() => console.error(` answer with a number from 1 to ${census.candidates.length}`),
|
|
207
|
+
prefill2
|
|
208
|
+
);
|
|
209
|
+
if (picked === void 0) {
|
|
210
|
+
console.error("archstone init: no response locus chosen after several attempts \u2014 refusing rather than guessing one.");
|
|
211
|
+
return void 0;
|
|
212
|
+
}
|
|
213
|
+
responseLocus = census.candidates[picked - 1].id;
|
|
214
|
+
}
|
|
215
|
+
const resourceName = (await ask.question(" resource name (blank = derive from the source) ")).trim();
|
|
216
|
+
const decision = {
|
|
217
|
+
operation: operation.key,
|
|
218
|
+
keep: true,
|
|
219
|
+
capabilityId,
|
|
220
|
+
effect,
|
|
221
|
+
...responseLocus !== void 0 ? { responseLocus } : {},
|
|
222
|
+
...resourceName !== "" ? { resourceName } : {}
|
|
223
|
+
};
|
|
224
|
+
if (args.probe && decision.effect === "read") {
|
|
225
|
+
decision.probe = await confirm(ask, ` record a golden fixture with ONE live ${operation.method} to the real backend?`, false);
|
|
226
|
+
if (decision.probe) {
|
|
227
|
+
const method = operation.method.toUpperCase();
|
|
228
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
229
|
+
decision.probeNonReadMethodConfirmed = await confirm(
|
|
230
|
+
ask,
|
|
231
|
+
` ${method} is not a GET. Confirm again that this request changes nothing on the backend:`,
|
|
232
|
+
false
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
const sample = {};
|
|
236
|
+
for (const field of operation.input) {
|
|
237
|
+
const suggested = isKnown(field.example) ? String(field.example.value) : void 0;
|
|
238
|
+
const origin = suggested === void 0 ? "" : " (from the API description)";
|
|
239
|
+
const required = valueOrUndefined(field.required) === true || field.in === "path";
|
|
240
|
+
const typed = (await ask.question(` sample value for ${field.name}${required ? "" : " (optional)"}${origin} `, suggested)).trim();
|
|
241
|
+
if (typed !== "") sample[field.name] = coerce(typed);
|
|
242
|
+
}
|
|
243
|
+
if (Object.keys(sample).length > 0) decision.sampleInput = sample;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
decisions.push(decision);
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
version: "0",
|
|
250
|
+
company: { id: companyId, ...companyName !== "" ? { name: companyName } : {} },
|
|
251
|
+
...baseUrlEnvVar !== "" ? { baseUrlEnvVar } : {},
|
|
252
|
+
...authEnvVar !== "" ? { authEnvVar } : {},
|
|
253
|
+
decisions
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
function coerce(text) {
|
|
257
|
+
try {
|
|
258
|
+
return JSON.parse(text);
|
|
259
|
+
} catch {
|
|
260
|
+
return text;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function parseInitArgs(argv) {
|
|
264
|
+
const flag = (name) => {
|
|
265
|
+
const idx = argv.indexOf(name);
|
|
266
|
+
return idx === -1 ? void 0 : argv[idx + 1];
|
|
267
|
+
};
|
|
268
|
+
const valued = ["--out", "--domain", "--company", "--decisions", "--report"];
|
|
269
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
270
|
+
for (const name of valued) {
|
|
271
|
+
const idx = argv.indexOf(name);
|
|
272
|
+
if (idx !== -1) {
|
|
273
|
+
consumed.add(idx);
|
|
274
|
+
consumed.add(idx + 1);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const positional = argv.filter((a, i) => !consumed.has(i) && !a.startsWith("--"));
|
|
278
|
+
const spec = positional[1];
|
|
279
|
+
if (spec === void 0) return { error: "a spec file is required" };
|
|
280
|
+
const out = flag("--out");
|
|
281
|
+
if (out === void 0) return { error: "--out <dir> is required" };
|
|
282
|
+
return {
|
|
283
|
+
spec,
|
|
284
|
+
out,
|
|
285
|
+
...flag("--domain") !== void 0 ? { domain: flag("--domain") } : {},
|
|
286
|
+
...flag("--company") !== void 0 ? { company: flag("--company") } : {},
|
|
287
|
+
probe: argv.includes("--probe"),
|
|
288
|
+
...flag("--decisions") !== void 0 ? { decisionsFile: flag("--decisions") } : {},
|
|
289
|
+
interactive: !argv.includes("--non-interactive"),
|
|
290
|
+
force: argv.includes("--force"),
|
|
291
|
+
...flag("--report") !== void 0 ? { reportFile: flag("--report") } : {}
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
async function runInitCmd(argv) {
|
|
295
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
296
|
+
console.log(INIT_USAGE);
|
|
297
|
+
return 0;
|
|
298
|
+
}
|
|
299
|
+
const parsed = parseInitArgs(argv);
|
|
300
|
+
if ("error" in parsed) {
|
|
301
|
+
console.error(`archstone init: ${parsed.error}
|
|
302
|
+
|
|
303
|
+
${INIT_USAGE}`);
|
|
304
|
+
return 2;
|
|
305
|
+
}
|
|
306
|
+
const args = parsed;
|
|
307
|
+
const specFile = resolve(process.cwd(), args.spec);
|
|
308
|
+
if (!existsSync(specFile)) {
|
|
309
|
+
console.error(`archstone init: no such file: ${specFile}`);
|
|
310
|
+
return 2;
|
|
311
|
+
}
|
|
312
|
+
const adapter = openApiAdapter;
|
|
313
|
+
const { input, unresolved } = loadSource(adapter, specFile);
|
|
314
|
+
for (const key of unresolved) {
|
|
315
|
+
console.error(`archstone init: referenced document '${key}' could not be read from the spec's own directory \u2014 operations that need it will be skipped.`);
|
|
316
|
+
}
|
|
317
|
+
const draft = adapter.adapt(input);
|
|
318
|
+
if (draft.operations.length === 0) {
|
|
319
|
+
console.error(`archstone init: ${adapter.id} found no candidate operations in ${args.spec}.`);
|
|
320
|
+
for (const n of draft.notes) console.error(` - ${n.code}${n.detail ? `: ${n.detail}` : ""}`);
|
|
321
|
+
return 1;
|
|
322
|
+
}
|
|
323
|
+
let record;
|
|
324
|
+
if (args.decisionsFile !== void 0) {
|
|
325
|
+
const ignored = [args.company !== void 0 ? "--company" : void 0, args.domain !== void 0 ? "--domain" : void 0].filter(
|
|
326
|
+
(f) => f !== void 0
|
|
327
|
+
);
|
|
328
|
+
if (ignored.length > 0) {
|
|
329
|
+
console.error(
|
|
330
|
+
`archstone init: ${ignored.join(" and ")} ${ignored.length === 1 ? "is" : "are"} answered by the Decision Record and cannot be combined with --decisions.
|
|
331
|
+
${ignored.includes("--company") ? "Set `company.id` in the record" : ""}${ignored.length === 2 ? "; " : ""}${ignored.includes("--domain") ? "the domain is the first half of each `capabilityId` in the record" : ""}.`
|
|
332
|
+
);
|
|
333
|
+
return 2;
|
|
334
|
+
}
|
|
335
|
+
let parsed2;
|
|
336
|
+
try {
|
|
337
|
+
parsed2 = JSON.parse(readFileSync(resolve(process.cwd(), args.decisionsFile), "utf8"));
|
|
338
|
+
} catch (err) {
|
|
339
|
+
console.error(`archstone init: cannot read the Decision Record: ${err.message}`);
|
|
340
|
+
return 2;
|
|
341
|
+
}
|
|
342
|
+
const validation = validateDecisionRecord(parsed2);
|
|
343
|
+
if (!validation.ok) {
|
|
344
|
+
console.error(`archstone init: the Decision Record at ${args.decisionsFile} is not valid:`);
|
|
345
|
+
for (const problem of validation.problems) console.error(` - ${problem}`);
|
|
346
|
+
return 2;
|
|
347
|
+
}
|
|
348
|
+
record = validation.record;
|
|
349
|
+
} else if (!args.interactive) {
|
|
350
|
+
console.error("archstone init: --non-interactive requires --decisions <file>. `init` never defaults an `effect`.");
|
|
351
|
+
return 2;
|
|
352
|
+
} else {
|
|
353
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
354
|
+
let outcome;
|
|
355
|
+
try {
|
|
356
|
+
outcome = await runGateOverTerminal(draft, rl, args);
|
|
357
|
+
} finally {
|
|
358
|
+
rl.close();
|
|
359
|
+
}
|
|
360
|
+
if (outcome === "no-more-input") {
|
|
361
|
+
console.error("\narchstone init: no more input (Ctrl+D, or stdin ended) \u2014 nothing was written.");
|
|
362
|
+
console.error(" To answer without a human, use --decisions <file> --non-interactive.");
|
|
363
|
+
return 2;
|
|
364
|
+
}
|
|
365
|
+
if (outcome === "terminal-closed") {
|
|
366
|
+
console.error("\narchstone init: the terminal closed before the gate finished \u2014 nothing was written.");
|
|
367
|
+
return 2;
|
|
368
|
+
}
|
|
369
|
+
record = outcome;
|
|
370
|
+
if (!record) return 2;
|
|
371
|
+
}
|
|
372
|
+
const result = await runInit(draft, record, {
|
|
373
|
+
targetDir: resolve(process.cwd(), args.out),
|
|
374
|
+
force: args.force,
|
|
375
|
+
probe: args.probe,
|
|
376
|
+
// "Interactive" for R-8's purposes means A HUMAN WAS ACTUALLY ASKED, not "the
|
|
377
|
+
// --non-interactive flag was absent". A Decision Record file supplies every answer up
|
|
378
|
+
// front, so `--decisions` without `--non-interactive` has no prompt either — and treating
|
|
379
|
+
// it as interactive would let a file-supplied `probeNonReadMethodConfirmed` authorize a
|
|
380
|
+
// non-GET probe against a production backend with nobody at the terminal. The second
|
|
381
|
+
// confirmation is a human act performed AT THE MOMENT OF THE CALL; that is the whole
|
|
382
|
+
// reason it is separate from `effect`, which a file may legitimately carry.
|
|
383
|
+
interactive: args.interactive && args.decisionsFile === void 0
|
|
384
|
+
});
|
|
385
|
+
const report = formatReport({
|
|
386
|
+
origin: draft.source.origin,
|
|
387
|
+
adapter: draft.source.adapter,
|
|
388
|
+
targetDir: resolve(process.cwd(), args.out),
|
|
389
|
+
emitted: result.emitted,
|
|
390
|
+
written: result.written,
|
|
391
|
+
failures: result.failures,
|
|
392
|
+
probes: result.probes.map((p) => ({ capabilityId: p.capabilityId, outcome: p.outcome, detail: p.detail })),
|
|
393
|
+
verifications: result.verifications,
|
|
394
|
+
candidates: draft.operations.length
|
|
395
|
+
});
|
|
396
|
+
console.log(`
|
|
397
|
+
${report}`);
|
|
398
|
+
if (result.ok) {
|
|
399
|
+
const reportFile = args.reportFile !== void 0 ? resolve(process.cwd(), args.reportFile) : join(resolve(process.cwd(), args.out), "INIT-REPORT.md");
|
|
400
|
+
try {
|
|
401
|
+
writeFileSync(reportFile, report);
|
|
402
|
+
console.log(`Report also written to ${reportFile}
|
|
403
|
+
`);
|
|
404
|
+
} catch (err) {
|
|
405
|
+
console.error(`archstone init: could not write the report file: ${err.message}`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return result.ok ? 0 : 1;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// src/index.ts
|
|
11
412
|
function runApply(dir) {
|
|
12
413
|
const res = load(dir);
|
|
13
414
|
console.log(`
|
|
@@ -85,8 +486,8 @@ function runBuild(dir, outPath) {
|
|
|
85
486
|
process.exit(1);
|
|
86
487
|
}
|
|
87
488
|
const stripped = { ...ir, tools: ir.tools.map(({ contract: _contract, ...t }) => t) };
|
|
88
|
-
const outFile =
|
|
89
|
-
|
|
489
|
+
const outFile = resolve2(process.cwd(), outPath ?? "archstone.ir.json");
|
|
490
|
+
writeFileSync2(outFile, `${JSON.stringify(stripped, null, 2)}
|
|
90
491
|
`);
|
|
91
492
|
console.log(`archstone build ${dir} \u2192 ${outFile} (${stripped.tools.length} tool(s))`);
|
|
92
493
|
process.exit(0);
|
|
@@ -249,8 +650,14 @@ async function main() {
|
|
|
249
650
|
runBuild(dir, out.value);
|
|
250
651
|
return;
|
|
251
652
|
}
|
|
653
|
+
if (cmd === "init") {
|
|
654
|
+
process.exit(await runInitCmd(argv));
|
|
655
|
+
}
|
|
252
656
|
console.error(
|
|
253
|
-
|
|
657
|
+
// `init` is named HERE, in the verb list, and not only in the block below it. It takes a
|
|
658
|
+
// spec file rather than a manifest directory, so it cannot share the first line's shape —
|
|
659
|
+
// which is exactly how it came to be missing from the one line a user actually scans.
|
|
660
|
+
"usage: archstone <apply|serve|verify|build|init>\n\n archstone <apply|serve|verify|build> <manifest-dir> [--json] [--out path]\n archstone serve --http <manifest-dir> [--port <n>] [--token <value>]\n bearer token: --token <value>, or the ARCHSTONE_HTTP_TOKEN env var (required \u2014 never serves open)\n archstone init <spec-file> --out <dir> \u2014 start here if you have no manifest yet\n\n" + INIT_USAGE
|
|
254
661
|
);
|
|
255
662
|
process.exit(2);
|
|
256
663
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\n// @archstone/cli — `archstone apply` (#1) + `archstone serve` (#7, + `--http` ADD-0008 #29)\n// + `archstone verify` (#18-20) + `archstone build` (ADD-0008 #27)\n//\n// apply: parse → shape-validate (#2) → semantic-validate (#3) → compile IR (#4)\n// → index Registry (#5), and REPORT (human output, exits).\n// serve: build the registry and expose it as an MCP server over stdio (#7),\n// so Claude/Cursor/ChatGPT can discover and invoke the tools. Blocks.\n// serve --http: same registry, served over real Streamable-HTTP instead of stdio —\n// `@archstone/runtime/http`'s createHttpHandler (Web-standard Request/Response,\n// bearer-token gated, shared with @archstone/agent/mcp's mcpHandler(), ADD-0008 D-3)\n// behind a thin Node-http adapter. Blocks.\n// verify: replay each bound capability's golden fixture against the LIVE backend\n// and report a per-binding health status (ADD-18). The only command that\n// makes a network call outside a real MCP invocation — on demand, never\n// scheduled by Archstone itself (wire it into your own CI/cron).\n// build: run the same compile pipeline as `apply`, strip each tool's `contract`\n// (D-8 — the fingerprint/golden-fixture path is meaningless once the fixture\n// file isn't shipping), and write the IR as a standalone JSON artifact —\n// the substrate `@archstone/agent`'s `fromIR()` will consume (RFC-0008).\n\nimport { writeFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { createServer, type IncomingMessage, type ServerResponse } from \"node:http\";\nimport { load } from \"@archstone/schema\";\nimport { validateSemantics, compile, type IR } from \"@archstone/compiler\";\nimport { Registry, buildRegistry, serveStdio, runVerify, type HealthStatus } from \"@archstone/runtime\";\nimport { createHttpHandler } from \"@archstone/runtime/http\";\n\nfunction runApply(dir: string): void {\n const res = load(dir);\n console.log(`\\narchstone apply ${dir}\\n`);\n\n if (res.capabilities) {\n const c = res.capabilities;\n console.log(` company ${c.company.name ?? c.company.id} (${c.company.id})`);\n console.log(` providers ${c.providers.join(\", \")}`);\n console.log(` declared ${c.capabilities.length} capabilities`);\n }\n console.log(` loaded ${res.capabilityDocs.length} capability docs, ${res.bindings.length} bindings`);\n for (const d of res.capabilityDocs) {\n console.log(` ✓ ${d.capability.id} [${d.capability.effect}] → ${d.capability.provider ?? \"?\"}`);\n }\n // #43: a policy the author believes is enforced must never be invisible here — the whole\n // point of the semantic pass's scope diagnostics is that \"attached to nothing\" is loud.\n if (res.policyDocs.length > 0) {\n console.log(` policies ${res.policyDocs.length} policy document(s)`);\n for (const p of res.policyDocs) {\n const target =\n p.metadata.scope === \"capability\"\n ? `capability ${p.metadata.capabilityId ?? \"?\"}`\n : p.metadata.scope === \"provider\"\n ? `provider ${p.metadata.provider ?? \"?\"}`\n : \"(no scope)\";\n console.log(` ✓ ${p.metadata.id} → ${target}`);\n }\n }\n\n // Shape (schema) issues from #2 — \"valid shapes\" is not \"deployable\".\n if (res.issues.length > 0) {\n console.log(`\\n ✗ ${res.issues.length} shape issue(s):`);\n for (const i of res.issues) console.log(` - ${i.file}: ${i.message}`);\n } else {\n console.log(`\\n ✓ shapes valid`);\n }\n\n // Semantic pass (#3) — cross-file resolution; errors block, warnings inform.\n const diags = validateSemantics(res);\n const errors = diags.filter((d) => d.severity === \"error\");\n const warnings = diags.filter((d) => d.severity === \"warning\");\n console.log(` semantic ${errors.length} error(s), ${warnings.length} warning(s)`);\n for (const d of errors) console.log(` ✗ ${d.message}`);\n for (const d of warnings) console.log(` ⚠ ${d.message}`);\n\n const shapesAndSemanticsOk = res.ok && errors.length === 0;\n\n // Compile to IR (#4) + index into the Registry (#5) — only when valid enough to emit.\n // ADD-30: a tool-name collision (two capability ids sanitizing to the same advertised\n // name) is checked here, before the final `ok`, alongside the semantic errors above —\n // 'apply' must refuse the same manifest 'build'/'serve' would refuse (D-2).\n const registry = shapesAndSemanticsOk ? new Registry(compile(res)) : undefined;\n const collisions = registry?.toolNameCollisions ?? [];\n if (collisions.length > 0) {\n console.log(`\\n ✗ ${collisions.length} tool-name collision(s):`);\n for (const c of collisions) {\n console.log(` - tool name '${c.name}' is ambiguous — capabilities ${c.ids.join(\", \")} all sanitize to it`);\n }\n }\n\n const ok = shapesAndSemanticsOk && collisions.length === 0;\n\n if (ok && registry) {\n const invocable = registry.listCapabilities().filter((t) => t.connector).length;\n console.log(` registry IR v${registry.ir.version} — ${registry.size} capabilities, ${invocable} invocable (bound)`);\n console.log(`\\n → run 'archstone serve ${dir}' to expose ${invocable} tool(s) to an AI agent over MCP`);\n }\n\n console.log(\"\");\n process.exit(ok ? 0 : 1);\n}\n\nfunction runBuild(dir: string, outPath: string | undefined): void {\n const res = load(dir);\n const diags = validateSemantics(res);\n const errors = diags.filter((d) => d.severity === \"error\");\n const ok = res.ok && errors.length === 0;\n\n if (!ok) {\n console.error(`archstone build ${dir}: manifest invalid — run 'archstone apply ${dir}' for details`);\n for (const i of res.issues) console.error(` - ${i.file}: ${i.message}`);\n for (const d of errors) console.error(` - ${d.message}`);\n process.exit(1);\n }\n\n const ir = compile(res);\n\n // ADD-30 R-2: `runBuild` didn't construct a Registry at all, so it could ship a broken\n // artifact whose ambiguous tool name only surfaces later, inside a third party's\n // `fromIR()` call. Refuse to write on a collision — fail at `build` time instead\n // (the same \"ambiguous is a compile-time error, never a guess\" pattern this repo already\n // applies to resource-name resolution, compiler/src/resolve.ts).\n const registry = new Registry(ir);\n if (registry.toolNameCollisions.length > 0) {\n console.error(`archstone build ${dir}: refusing to write artifact — tool-name collision(s):`);\n for (const c of registry.toolNameCollisions) {\n console.error(` - tool name '${c.name}' is ambiguous — capabilities ${c.ids.join(\", \")} all sanitize to it`);\n }\n process.exit(1);\n }\n\n // THE STRIP RULE, stated as a principle rather than a list (ADD-43 D-9), so the next field\n // added to `IRTool` is classified deliberately instead of by whichever example was copied:\n //\n // strip what the INVOCATION PATH cannot use.\n //\n // `contract` qualifies (ADD-0008 D-8): it is verify-time-only and carries an fs path that is\n // meaningless once the golden fixture is not shipping alongside the artifact.\n //\n // `policyRules` (#43) is the exact opposite and MUST survive: it is invocation-path data, read\n // by the evaluator on every `execute()` call. Stripping it would ship an unpoliced embedded\n // SDK beside a policed MCP surface — the precise cross-path drift #43 exists to prevent, and\n // silent, because `fromIR` validates only `version` and treats the rest as opaque.\n const stripped: IR = { ...ir, tools: ir.tools.map(({ contract: _contract, ...t }) => t) };\n\n const outFile = resolve(process.cwd(), outPath ?? \"archstone.ir.json\");\n writeFileSync(outFile, `${JSON.stringify(stripped, null, 2)}\\n`);\n console.log(`archstone build ${dir} → ${outFile} (${stripped.tools.length} tool(s))`);\n process.exit(0);\n}\n\nfunction runServeHttp(dir: string, port: number, token: string | undefined): void {\n // Rule #7 / ADD-0008 R-5: fail closed before touching the network — a missing token is a\n // startup error, never a silently-open endpoint. `--token` wins over the env var if both\n // are set; createHttpHandler itself would also throw on empty, but checking here first\n // gives a CLI-appropriate error message instead of an uncaught exception.\n if (!token) {\n console.error(\n \"archstone serve --http: bearer token required — set ARCHSTONE_HTTP_TOKEN or pass --token <value>\",\n );\n process.exit(1);\n }\n\n const built = buildRegistry(dir);\n if (!built.ok || !built.registry) {\n console.error(`archstone: cannot serve '${dir}' — manifest invalid:`);\n for (const i of built.issues) console.error(` - ${i.file}: ${i.message}`);\n for (const d of built.diagnostics.filter((x) => x.severity === \"error\")) console.error(` - ${d.message}`);\n process.exit(1);\n }\n\n const handler = createHttpHandler(built.registry, { bearerToken: token });\n const server = createServer((req, res) => {\n // #49 belt-and-braces: this used to be `void handleHttpRequest(...)`. Fire-and-forget\n // means nothing is attached to the returned promise, so ANY rejection escaping the\n // function became an unhandled rejection — fatal under Node's default\n // `--unhandled-rejections=throw`, killing the server on one aborted client connection.\n // handleHttpRequest now contains its own failures, but this `.catch` is the seam that\n // makes the fix independent of that catch staying exhaustive: a future throw added\n // outside its `try` cannot resurrect the process-death bug.\n handleHttpRequest(handler, req, res).catch((err: unknown) => {\n console.error(\"archstone serve --http: request handling failed —\", err);\n endResponseQuietly(res, 500);\n });\n });\n server.listen(port, () => {\n console.error(`archstone: serving MCP over HTTP on http://localhost:${port}/ (bearer-token gated)`);\n });\n}\n\n/**\n * Largest request body `archstone serve --http` will buffer, in bytes (#50).\n *\n * 4 MiB is not chosen by feel: it is the limit the MCP SDK itself applies to an MCP message\n * arriving over HTTP (`MAXIMUM_MESSAGE_SIZE = '4mb'` in the SDK's own Node SSE transport,\n * enforced via `raw-body`). Same protocol, same message class, same SDK version this package\n * already depends on — so the ceiling matches what an MCP client can reasonably expect to send\n * anywhere else in the ecosystem, rather than inventing an Archstone-specific number. The\n * Web-standard transport used here never reads the socket itself (this adapter hands it an\n * already-built `Request`), which is precisely why the SDK's limit does not apply on this path\n * and has to be reapplied here.\n *\n * For scale: an MCP `tools/call` body carries a capability's declared inputs as JSON. 4 MiB is\n * orders of magnitude above any manifest in `examples/`.\n */\nconst MAX_REQUEST_BODY_BYTES = 4 * 1024 * 1024;\n\n/**\n * Terminate a response without ever throwing (#49). Every exit path out of the adapter goes\n * through here, including the ones reached after the client is already gone: on an aborted\n * connection the socket is destroyed, and a naive `res.end()` there is at best pointless and\n * at worst a second error thrown out of an error path. Ending is still attempted whenever the\n * socket survives — a truncated body on a keep-alive connection has a live socket that would\n * otherwise hang until the client's own timeout.\n */\nfunction endResponseQuietly(\n res: ServerResponse,\n status: number,\n opts: { closeConnection?: boolean } = {},\n): void {\n try {\n if (res.writableEnded || res.destroyed) return;\n if (!res.headersSent) {\n res.statusCode = status;\n // #50: on a refused oversized body the connection must not be reused. The client is\n // mid-upload and the rest of its bytes are still in flight, so a keep-alive socket\n // would leave that remainder to be misparsed as the next request. `Connection: close`\n // lets Node flush the response first and then close — destroying the socket here\n // instead would race the 413 and the client would see nothing.\n if (opts.closeConnection) res.setHeader(\"connection\", \"close\");\n }\n res.end();\n } catch {\n // The socket went away between the checks above and the write. Nothing is left to\n // terminate and there is no one to tell — swallowing here is the whole point.\n }\n}\n\n// D-3's \"~20-line wrapper\": Node's http.IncomingMessage/ServerResponse <-> Web-standard\n// Request/Response, so createHttpHandler (already Web-standard, shared with\n// @archstone/agent/mcp's mcpHandler()) can serve real Node HTTP traffic without a second\n// transport implementation. CLI-level plumbing only — HTTP itself still lives in\n// providers/rest for business-backend calls; this adapter never touches a backend.\n//\n// #49 (P0, unauthenticated remote DoS): this function must never reject and must always\n// reach a terminal `res.end()`. It is invoked from a Node `request` listener, where an\n// escaping rejection is an unhandled rejection and therefore a fatal uncaught exception —\n// one client that declares a Content-Length and disconnects mid-body used to kill the\n// process, before any handler and therefore before any credential check ran.\nasync function handleHttpRequest(\n handler: (request: Request) => Promise<Response>,\n req: IncomingMessage,\n res: ServerResponse,\n): Promise<void> {\n // #50: the body is buffered BEFORE authentication (the bearer check lives inside\n // createHttpHandler, reached only once the Request is built), so an unauthenticated client\n // controls how much memory this allocates. Measured server-side: the body is held ~4x over\n // simultaneously — the chunk array, `Buffer.concat`'s copy, and undici's own copies inside\n // `new Request` — so a 256 MiB body peaked at 1,081 MiB RSS, essentially all of it in\n // `external`/`arrayBuffers`. Being external is what makes it nasty: `--max-old-space-size`\n // does not bound it, and the terminal symptom is an uncatchable OOM abort.\n //\n // A declared Content-Length over the cap is refused before a single byte is read; the\n // running total is then enforced during streaming as well, because Content-Length can lie\n // and chunked encoding omits it entirely. Like every other client fault in this adapter the\n // 413 is NOT logged — an unauthenticated caller must not be able to drive log volume (#49\n // BF-1).\n const declared = Number(req.headers[\"content-length\"]);\n if (Number.isFinite(declared) && declared > MAX_REQUEST_BODY_BYTES) {\n endResponseQuietly(res, 413, { closeConnection: true });\n return;\n }\n\n const chunks: Buffer[] = [];\n try {\n let received = 0;\n for await (const chunk of req) {\n const buf = chunk as Buffer;\n received += buf.length;\n if (received > MAX_REQUEST_BODY_BYTES) {\n // Returning from inside `for await` calls the iterator's `return()`, which tears the\n // request stream down — so the remaining bytes are never buffered, and the client is\n // not left streaming into a socket nobody drains.\n endResponseQuietly(res, 413, { closeConnection: true });\n return;\n }\n chunks.push(buf);\n }\n } catch {\n // The client went away mid-body (ECONNRESET / aborted), or delivered fewer bytes than\n // its declared Content-Length. On a public endpoint this is routine traffic — a closed\n // laptop, a cancelled fetch, a load-balancer health probe — NOT a server fault, so it is\n // deliberately not logged: turning an aborted-request flood into a log flood just trades\n // one denial of service for another.\n //\n // 400 is the deliberate status, not 500: the request was never completed, and nothing on\n // the server failed. In practice nobody reads it — this catch is reached only once the\n // socket is already dead. (Node does NOT surface a short body while the connection is\n // still open: it waits for the declared bytes until `server.requestTimeout`, 300 s by\n // default, and answers that itself.) The end is still attempted rather than skipped\n // because this code cannot tell from here whether `res` is writable — `req` erroring\n // does not by itself prove the response side is gone — and `endResponseQuietly` makes\n // the attempt free when it is.\n endResponseQuietly(res, 400);\n return;\n }\n\n // Translating the raw request into a Web `Request` is still CLIENT input handling, and it\n // runs BEFORE authentication (the bearer check lives inside createHttpHandler, reached only\n // at `handler(request)` below). `req.headers.host` and `req.url` are attacker-controlled and\n // a malformed value throws here — a bad `Host` was in fact a second unauthenticated kill\n // vector before #49's containment landed. So this gets its own client-fault arm, on exactly\n // the argument the body-read catch above makes: answering 500 and logging a stack trace per\n // request would hand an unauthenticated caller ~13x log amplification and trade the crash\n // for a disk-fill DoS. RFC 9112 §3.2 also makes 400 the required answer to an invalid Host.\n //\n // Classification is positional, not by error sniffing: what failed decides the class, so it\n // cannot drift when undici changes an error's shape between Node versions.\n let request: Request;\n try {\n const headers = new Headers();\n for (const [key, value] of Object.entries(req.headers)) {\n if (value !== undefined) headers.set(key, Array.isArray(value) ? value.join(\", \") : value);\n }\n const hasBody = req.method !== \"GET\" && req.method !== \"HEAD\" && chunks.length > 0;\n request = new Request(`http://${req.headers.host ?? \"localhost\"}${req.url ?? \"/\"}`, {\n method: req.method ?? \"GET\",\n headers,\n body: hasBody ? Buffer.concat(chunks) : undefined,\n });\n } catch {\n endResponseQuietly(res, 400);\n return;\n }\n\n try {\n const response = await handler(request);\n res.statusCode = response.status;\n response.headers.forEach((value, key) => res.setHeader(key, value));\n res.end(response.body ? Buffer.from(await response.arrayBuffer()) : undefined);\n } catch (err) {\n // A genuine server-side failure: the handler rejected, or serialising its Response threw.\n // Unlike a malformed or abandoned request this IS worth surfacing, so it is logged — and\n // answered with a 500 rather than left to hang the caller. Nothing attacker-controlled\n // reaches this arm without first passing through the handler, so it cannot be used as a\n // log-amplification primitive the way the pre-auth construction path above could.\n console.error(\"archstone serve --http: request handling failed —\", err);\n endResponseQuietly(res, 500);\n }\n}\n\nconst HEALTH_ICON: Record<HealthStatus, string> = { green: \"🟢\", yellow: \"🟡\", red: \"🔴\" };\n\nasync function runVerifyCmd(dir: string, json: boolean): Promise<void> {\n const res = load(dir);\n const diags = validateSemantics(res);\n const errors = diags.filter((d) => d.severity === \"error\");\n const ok = res.ok && errors.length === 0;\n if (!ok) {\n if (json) {\n // ADD-20 D-2: this shape is strictly disjoint from the `{results}` shape below —\n // never add a shared \"envelope\" field (e.g. `ok`) to either.\n console.log(JSON.stringify({ error: \"manifest_invalid\", issues: res.issues, errors }));\n } else {\n console.error(`archstone verify ${dir}: manifest invalid — run 'archstone apply ${dir}' for details`);\n }\n process.exit(2);\n }\n\n const registry = new Registry(compile(res));\n const reports = await runVerify(registry.listCapabilities(), dir, registry.ir.resources);\n\n if (json) {\n // ADD-20 D-2: strictly disjoint from the `{error, issues, errors}` shape above.\n console.log(JSON.stringify({ results: reports }));\n process.exit(reports.some((r) => r.status === \"red\") ? 1 : 0);\n }\n\n console.log(`\\narchstone verify ${dir}\\n`);\n if (reports.length === 0) {\n console.log(\" (no bindings declare a contract: — nothing to verify)\\n\");\n process.exit(0);\n }\n for (const r of reports) {\n console.log(` ${HEALTH_ICON[r.status]} ${r.capabilityId} — ${r.detail}`);\n }\n console.log(\"\");\n process.exit(reports.some((r) => r.status === \"red\") ? 1 : 0);\n}\n\n/** Value of a `--name value` flag pair, plus the index it was found at (-1 if absent) —\n * used both to read the value and to exclude both tokens from the positional args. */\nfunction flagArg(argv: string[], name: string): { value?: string; idx: number } {\n const idx = argv.indexOf(name);\n return { value: idx !== -1 ? argv[idx + 1] : undefined, idx };\n}\n\nasync function main(): Promise<void> {\n const argv = process.argv.slice(2);\n const json = argv.includes(\"--json\");\n const http = argv.includes(\"--http\");\n const out = flagArg(argv, \"--out\");\n const port = flagArg(argv, \"--port\");\n const token = flagArg(argv, \"--token\");\n\n const consumed = new Set<number>();\n for (const f of [out, port, token]) {\n if (f.idx !== -1) {\n consumed.add(f.idx);\n consumed.add(f.idx + 1);\n }\n }\n const positional = argv.filter((a, i) => !consumed.has(i) && a !== \"--json\" && a !== \"--http\");\n const [cmd, dir] = positional;\n\n if (cmd === \"apply\" && dir) {\n runApply(dir);\n return;\n }\n if (cmd === \"serve\" && dir && http) {\n // Bearer token: --token wins over ARCHSTONE_HTTP_TOKEN if both are set (Rule #7 —\n // required, never defaults open).\n runServeHttp(dir, Number(port.value ?? 8787), token.value ?? process.env.ARCHSTONE_HTTP_TOKEN);\n return; // blocks on the HTTP server\n }\n if (cmd === \"serve\" && dir) {\n await serveStdio(dir); // blocks on the stdio transport\n return;\n }\n if (cmd === \"verify\" && dir) {\n await runVerifyCmd(dir, json);\n return;\n }\n if (cmd === \"build\" && dir) {\n runBuild(dir, out.value);\n return;\n }\n\n console.error(\n \"usage: archstone <apply|serve|verify|build> <manifest-dir> [--json] [--out path]\\n\" +\n \" archstone serve --http <manifest-dir> [--port <n>] [--token <value>]\\n\" +\n \" bearer token: --token <value>, or the ARCHSTONE_HTTP_TOKEN env var (required — never serves open)\",\n );\n process.exit(2);\n}\n\nmain();\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/init.ts"],"sourcesContent":["#!/usr/bin/env node\n// @archstone/cli — `archstone apply` (#1) + `archstone serve` (#7, + `--http` ADD-0008 #29)\n// + `archstone verify` (#18-20) + `archstone build` (ADD-0008 #27)\n//\n// apply: parse → shape-validate (#2) → semantic-validate (#3) → compile IR (#4)\n// → index Registry (#5), and REPORT (human output, exits).\n// serve: build the registry and expose it as an MCP server over stdio (#7),\n// so Claude/Cursor/ChatGPT can discover and invoke the tools. Blocks.\n// serve --http: same registry, served over real Streamable-HTTP instead of stdio —\n// `@archstone/runtime/http`'s createHttpHandler (Web-standard Request/Response,\n// bearer-token gated, shared with @archstone/agent/mcp's mcpHandler(), ADD-0008 D-3)\n// behind a thin Node-http adapter. Blocks.\n// verify: replay each bound capability's golden fixture against the LIVE backend\n// and report a per-binding health status (ADD-18). The only command that\n// makes a network call outside a real MCP invocation — on demand, never\n// scheduled by Archstone itself (wire it into your own CI/cron).\n// build: run the same compile pipeline as `apply`, strip each tool's `contract`\n// (D-8 — the fingerprint/golden-fixture path is meaningless once the fixture\n// file isn't shipping), and write the IR as a standalone JSON artifact —\n// the substrate `@archstone/agent`'s `fromIR()` will consume (RFC-0008).\n// init: read an existing API description, ask the human the questions no tool can answer\n// (is this a capability? is it `read`? what is it called?), and write a CDL manifest\n// the real compiler has already compiled (ADD-37). Thin by design — argv, the terminal\n// gate and report rendering only; everything of substance is in @archstone/init.\n\nimport { writeFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { createServer, type IncomingMessage, type ServerResponse } from \"node:http\";\nimport { load } from \"@archstone/schema\";\nimport { validateSemantics, compile, type IR } from \"@archstone/compiler\";\nimport { Registry, buildRegistry, serveStdio, runVerify, type HealthStatus } from \"@archstone/runtime\";\nimport { createHttpHandler } from \"@archstone/runtime/http\";\nimport { INIT_USAGE, runInitCmd } from \"./init\";\n\nfunction runApply(dir: string): void {\n const res = load(dir);\n console.log(`\\narchstone apply ${dir}\\n`);\n\n if (res.capabilities) {\n const c = res.capabilities;\n console.log(` company ${c.company.name ?? c.company.id} (${c.company.id})`);\n console.log(` providers ${c.providers.join(\", \")}`);\n console.log(` declared ${c.capabilities.length} capabilities`);\n }\n console.log(` loaded ${res.capabilityDocs.length} capability docs, ${res.bindings.length} bindings`);\n for (const d of res.capabilityDocs) {\n console.log(` ✓ ${d.capability.id} [${d.capability.effect}] → ${d.capability.provider ?? \"?\"}`);\n }\n // #43: a policy the author believes is enforced must never be invisible here — the whole\n // point of the semantic pass's scope diagnostics is that \"attached to nothing\" is loud.\n if (res.policyDocs.length > 0) {\n console.log(` policies ${res.policyDocs.length} policy document(s)`);\n for (const p of res.policyDocs) {\n const target =\n p.metadata.scope === \"capability\"\n ? `capability ${p.metadata.capabilityId ?? \"?\"}`\n : p.metadata.scope === \"provider\"\n ? `provider ${p.metadata.provider ?? \"?\"}`\n : \"(no scope)\";\n console.log(` ✓ ${p.metadata.id} → ${target}`);\n }\n }\n\n // Shape (schema) issues from #2 — \"valid shapes\" is not \"deployable\".\n if (res.issues.length > 0) {\n console.log(`\\n ✗ ${res.issues.length} shape issue(s):`);\n for (const i of res.issues) console.log(` - ${i.file}: ${i.message}`);\n } else {\n console.log(`\\n ✓ shapes valid`);\n }\n\n // Semantic pass (#3) — cross-file resolution; errors block, warnings inform.\n const diags = validateSemantics(res);\n const errors = diags.filter((d) => d.severity === \"error\");\n const warnings = diags.filter((d) => d.severity === \"warning\");\n console.log(` semantic ${errors.length} error(s), ${warnings.length} warning(s)`);\n for (const d of errors) console.log(` ✗ ${d.message}`);\n for (const d of warnings) console.log(` ⚠ ${d.message}`);\n\n const shapesAndSemanticsOk = res.ok && errors.length === 0;\n\n // Compile to IR (#4) + index into the Registry (#5) — only when valid enough to emit.\n // ADD-30: a tool-name collision (two capability ids sanitizing to the same advertised\n // name) is checked here, before the final `ok`, alongside the semantic errors above —\n // 'apply' must refuse the same manifest 'build'/'serve' would refuse (D-2).\n const registry = shapesAndSemanticsOk ? new Registry(compile(res)) : undefined;\n const collisions = registry?.toolNameCollisions ?? [];\n if (collisions.length > 0) {\n console.log(`\\n ✗ ${collisions.length} tool-name collision(s):`);\n for (const c of collisions) {\n console.log(` - tool name '${c.name}' is ambiguous — capabilities ${c.ids.join(\", \")} all sanitize to it`);\n }\n }\n\n const ok = shapesAndSemanticsOk && collisions.length === 0;\n\n if (ok && registry) {\n const invocable = registry.listCapabilities().filter((t) => t.connector).length;\n console.log(` registry IR v${registry.ir.version} — ${registry.size} capabilities, ${invocable} invocable (bound)`);\n console.log(`\\n → run 'archstone serve ${dir}' to expose ${invocable} tool(s) to an AI agent over MCP`);\n }\n\n console.log(\"\");\n process.exit(ok ? 0 : 1);\n}\n\nfunction runBuild(dir: string, outPath: string | undefined): void {\n const res = load(dir);\n const diags = validateSemantics(res);\n const errors = diags.filter((d) => d.severity === \"error\");\n const ok = res.ok && errors.length === 0;\n\n if (!ok) {\n console.error(`archstone build ${dir}: manifest invalid — run 'archstone apply ${dir}' for details`);\n for (const i of res.issues) console.error(` - ${i.file}: ${i.message}`);\n for (const d of errors) console.error(` - ${d.message}`);\n process.exit(1);\n }\n\n const ir = compile(res);\n\n // ADD-30 R-2: `runBuild` didn't construct a Registry at all, so it could ship a broken\n // artifact whose ambiguous tool name only surfaces later, inside a third party's\n // `fromIR()` call. Refuse to write on a collision — fail at `build` time instead\n // (the same \"ambiguous is a compile-time error, never a guess\" pattern this repo already\n // applies to resource-name resolution, compiler/src/resolve.ts).\n const registry = new Registry(ir);\n if (registry.toolNameCollisions.length > 0) {\n console.error(`archstone build ${dir}: refusing to write artifact — tool-name collision(s):`);\n for (const c of registry.toolNameCollisions) {\n console.error(` - tool name '${c.name}' is ambiguous — capabilities ${c.ids.join(\", \")} all sanitize to it`);\n }\n process.exit(1);\n }\n\n // THE STRIP RULE, stated as a principle rather than a list (ADD-43 D-9), so the next field\n // added to `IRTool` is classified deliberately instead of by whichever example was copied:\n //\n // strip what the INVOCATION PATH cannot use.\n //\n // `contract` qualifies (ADD-0008 D-8): it is verify-time-only and carries an fs path that is\n // meaningless once the golden fixture is not shipping alongside the artifact.\n //\n // `policyRules` (#43) is the exact opposite and MUST survive: it is invocation-path data, read\n // by the evaluator on every `execute()` call. Stripping it would ship an unpoliced embedded\n // SDK beside a policed MCP surface — the precise cross-path drift #43 exists to prevent, and\n // silent, because `fromIR` validates only `version` and treats the rest as opaque.\n const stripped: IR = { ...ir, tools: ir.tools.map(({ contract: _contract, ...t }) => t) };\n\n const outFile = resolve(process.cwd(), outPath ?? \"archstone.ir.json\");\n writeFileSync(outFile, `${JSON.stringify(stripped, null, 2)}\\n`);\n console.log(`archstone build ${dir} → ${outFile} (${stripped.tools.length} tool(s))`);\n process.exit(0);\n}\n\nfunction runServeHttp(dir: string, port: number, token: string | undefined): void {\n // Rule #7 / ADD-0008 R-5: fail closed before touching the network — a missing token is a\n // startup error, never a silently-open endpoint. `--token` wins over the env var if both\n // are set; createHttpHandler itself would also throw on empty, but checking here first\n // gives a CLI-appropriate error message instead of an uncaught exception.\n if (!token) {\n console.error(\n \"archstone serve --http: bearer token required — set ARCHSTONE_HTTP_TOKEN or pass --token <value>\",\n );\n process.exit(1);\n }\n\n const built = buildRegistry(dir);\n if (!built.ok || !built.registry) {\n console.error(`archstone: cannot serve '${dir}' — manifest invalid:`);\n for (const i of built.issues) console.error(` - ${i.file}: ${i.message}`);\n for (const d of built.diagnostics.filter((x) => x.severity === \"error\")) console.error(` - ${d.message}`);\n process.exit(1);\n }\n\n const handler = createHttpHandler(built.registry, { bearerToken: token });\n const server = createServer((req, res) => {\n // #49 belt-and-braces: this used to be `void handleHttpRequest(...)`. Fire-and-forget\n // means nothing is attached to the returned promise, so ANY rejection escaping the\n // function became an unhandled rejection — fatal under Node's default\n // `--unhandled-rejections=throw`, killing the server on one aborted client connection.\n // handleHttpRequest now contains its own failures, but this `.catch` is the seam that\n // makes the fix independent of that catch staying exhaustive: a future throw added\n // outside its `try` cannot resurrect the process-death bug.\n handleHttpRequest(handler, req, res).catch((err: unknown) => {\n console.error(\"archstone serve --http: request handling failed —\", err);\n endResponseQuietly(res, 500);\n });\n });\n server.listen(port, () => {\n console.error(`archstone: serving MCP over HTTP on http://localhost:${port}/ (bearer-token gated)`);\n });\n}\n\n/**\n * Largest request body `archstone serve --http` will buffer, in bytes (#50).\n *\n * 4 MiB is not chosen by feel: it is the limit the MCP SDK itself applies to an MCP message\n * arriving over HTTP (`MAXIMUM_MESSAGE_SIZE = '4mb'` in the SDK's own Node SSE transport,\n * enforced via `raw-body`). Same protocol, same message class, same SDK version this package\n * already depends on — so the ceiling matches what an MCP client can reasonably expect to send\n * anywhere else in the ecosystem, rather than inventing an Archstone-specific number. The\n * Web-standard transport used here never reads the socket itself (this adapter hands it an\n * already-built `Request`), which is precisely why the SDK's limit does not apply on this path\n * and has to be reapplied here.\n *\n * For scale: an MCP `tools/call` body carries a capability's declared inputs as JSON. 4 MiB is\n * orders of magnitude above any manifest in `examples/`.\n */\nconst MAX_REQUEST_BODY_BYTES = 4 * 1024 * 1024;\n\n/**\n * Terminate a response without ever throwing (#49). Every exit path out of the adapter goes\n * through here, including the ones reached after the client is already gone: on an aborted\n * connection the socket is destroyed, and a naive `res.end()` there is at best pointless and\n * at worst a second error thrown out of an error path. Ending is still attempted whenever the\n * socket survives — a truncated body on a keep-alive connection has a live socket that would\n * otherwise hang until the client's own timeout.\n */\nfunction endResponseQuietly(\n res: ServerResponse,\n status: number,\n opts: { closeConnection?: boolean } = {},\n): void {\n try {\n if (res.writableEnded || res.destroyed) return;\n if (!res.headersSent) {\n res.statusCode = status;\n // #50: on a refused oversized body the connection must not be reused. The client is\n // mid-upload and the rest of its bytes are still in flight, so a keep-alive socket\n // would leave that remainder to be misparsed as the next request. `Connection: close`\n // lets Node flush the response first and then close — destroying the socket here\n // instead would race the 413 and the client would see nothing.\n if (opts.closeConnection) res.setHeader(\"connection\", \"close\");\n }\n res.end();\n } catch {\n // The socket went away between the checks above and the write. Nothing is left to\n // terminate and there is no one to tell — swallowing here is the whole point.\n }\n}\n\n// D-3's \"~20-line wrapper\": Node's http.IncomingMessage/ServerResponse <-> Web-standard\n// Request/Response, so createHttpHandler (already Web-standard, shared with\n// @archstone/agent/mcp's mcpHandler()) can serve real Node HTTP traffic without a second\n// transport implementation. CLI-level plumbing only — HTTP itself still lives in\n// providers/rest for business-backend calls; this adapter never touches a backend.\n//\n// #49 (P0, unauthenticated remote DoS): this function must never reject and must always\n// reach a terminal `res.end()`. It is invoked from a Node `request` listener, where an\n// escaping rejection is an unhandled rejection and therefore a fatal uncaught exception —\n// one client that declares a Content-Length and disconnects mid-body used to kill the\n// process, before any handler and therefore before any credential check ran.\nasync function handleHttpRequest(\n handler: (request: Request) => Promise<Response>,\n req: IncomingMessage,\n res: ServerResponse,\n): Promise<void> {\n // #50: the body is buffered BEFORE authentication (the bearer check lives inside\n // createHttpHandler, reached only once the Request is built), so an unauthenticated client\n // controls how much memory this allocates. Measured server-side: the body is held ~4x over\n // simultaneously — the chunk array, `Buffer.concat`'s copy, and undici's own copies inside\n // `new Request` — so a 256 MiB body peaked at 1,081 MiB RSS, essentially all of it in\n // `external`/`arrayBuffers`. Being external is what makes it nasty: `--max-old-space-size`\n // does not bound it, and the terminal symptom is an uncatchable OOM abort.\n //\n // A declared Content-Length over the cap is refused before a single byte is read; the\n // running total is then enforced during streaming as well, because Content-Length can lie\n // and chunked encoding omits it entirely. Like every other client fault in this adapter the\n // 413 is NOT logged — an unauthenticated caller must not be able to drive log volume (#49\n // BF-1).\n const declared = Number(req.headers[\"content-length\"]);\n if (Number.isFinite(declared) && declared > MAX_REQUEST_BODY_BYTES) {\n endResponseQuietly(res, 413, { closeConnection: true });\n return;\n }\n\n const chunks: Buffer[] = [];\n try {\n let received = 0;\n for await (const chunk of req) {\n const buf = chunk as Buffer;\n received += buf.length;\n if (received > MAX_REQUEST_BODY_BYTES) {\n // Returning from inside `for await` calls the iterator's `return()`, which tears the\n // request stream down — so the remaining bytes are never buffered, and the client is\n // not left streaming into a socket nobody drains.\n endResponseQuietly(res, 413, { closeConnection: true });\n return;\n }\n chunks.push(buf);\n }\n } catch {\n // The client went away mid-body (ECONNRESET / aborted), or delivered fewer bytes than\n // its declared Content-Length. On a public endpoint this is routine traffic — a closed\n // laptop, a cancelled fetch, a load-balancer health probe — NOT a server fault, so it is\n // deliberately not logged: turning an aborted-request flood into a log flood just trades\n // one denial of service for another.\n //\n // 400 is the deliberate status, not 500: the request was never completed, and nothing on\n // the server failed. In practice nobody reads it — this catch is reached only once the\n // socket is already dead. (Node does NOT surface a short body while the connection is\n // still open: it waits for the declared bytes until `server.requestTimeout`, 300 s by\n // default, and answers that itself.) The end is still attempted rather than skipped\n // because this code cannot tell from here whether `res` is writable — `req` erroring\n // does not by itself prove the response side is gone — and `endResponseQuietly` makes\n // the attempt free when it is.\n endResponseQuietly(res, 400);\n return;\n }\n\n // Translating the raw request into a Web `Request` is still CLIENT input handling, and it\n // runs BEFORE authentication (the bearer check lives inside createHttpHandler, reached only\n // at `handler(request)` below). `req.headers.host` and `req.url` are attacker-controlled and\n // a malformed value throws here — a bad `Host` was in fact a second unauthenticated kill\n // vector before #49's containment landed. So this gets its own client-fault arm, on exactly\n // the argument the body-read catch above makes: answering 500 and logging a stack trace per\n // request would hand an unauthenticated caller ~13x log amplification and trade the crash\n // for a disk-fill DoS. RFC 9112 §3.2 also makes 400 the required answer to an invalid Host.\n //\n // Classification is positional, not by error sniffing: what failed decides the class, so it\n // cannot drift when undici changes an error's shape between Node versions.\n let request: Request;\n try {\n const headers = new Headers();\n for (const [key, value] of Object.entries(req.headers)) {\n if (value !== undefined) headers.set(key, Array.isArray(value) ? value.join(\", \") : value);\n }\n const hasBody = req.method !== \"GET\" && req.method !== \"HEAD\" && chunks.length > 0;\n request = new Request(`http://${req.headers.host ?? \"localhost\"}${req.url ?? \"/\"}`, {\n method: req.method ?? \"GET\",\n headers,\n body: hasBody ? Buffer.concat(chunks) : undefined,\n });\n } catch {\n endResponseQuietly(res, 400);\n return;\n }\n\n try {\n const response = await handler(request);\n res.statusCode = response.status;\n response.headers.forEach((value, key) => res.setHeader(key, value));\n res.end(response.body ? Buffer.from(await response.arrayBuffer()) : undefined);\n } catch (err) {\n // A genuine server-side failure: the handler rejected, or serialising its Response threw.\n // Unlike a malformed or abandoned request this IS worth surfacing, so it is logged — and\n // answered with a 500 rather than left to hang the caller. Nothing attacker-controlled\n // reaches this arm without first passing through the handler, so it cannot be used as a\n // log-amplification primitive the way the pre-auth construction path above could.\n console.error(\"archstone serve --http: request handling failed —\", err);\n endResponseQuietly(res, 500);\n }\n}\n\nconst HEALTH_ICON: Record<HealthStatus, string> = { green: \"🟢\", yellow: \"🟡\", red: \"🔴\" };\n\nasync function runVerifyCmd(dir: string, json: boolean): Promise<void> {\n const res = load(dir);\n const diags = validateSemantics(res);\n const errors = diags.filter((d) => d.severity === \"error\");\n const ok = res.ok && errors.length === 0;\n if (!ok) {\n if (json) {\n // ADD-20 D-2: this shape is strictly disjoint from the `{results}` shape below —\n // never add a shared \"envelope\" field (e.g. `ok`) to either.\n console.log(JSON.stringify({ error: \"manifest_invalid\", issues: res.issues, errors }));\n } else {\n console.error(`archstone verify ${dir}: manifest invalid — run 'archstone apply ${dir}' for details`);\n }\n process.exit(2);\n }\n\n const registry = new Registry(compile(res));\n const reports = await runVerify(registry.listCapabilities(), dir, registry.ir.resources);\n\n if (json) {\n // ADD-20 D-2: strictly disjoint from the `{error, issues, errors}` shape above.\n console.log(JSON.stringify({ results: reports }));\n process.exit(reports.some((r) => r.status === \"red\") ? 1 : 0);\n }\n\n console.log(`\\narchstone verify ${dir}\\n`);\n if (reports.length === 0) {\n console.log(\" (no bindings declare a contract: — nothing to verify)\\n\");\n process.exit(0);\n }\n for (const r of reports) {\n console.log(` ${HEALTH_ICON[r.status]} ${r.capabilityId} — ${r.detail}`);\n }\n console.log(\"\");\n process.exit(reports.some((r) => r.status === \"red\") ? 1 : 0);\n}\n\n/** Value of a `--name value` flag pair, plus the index it was found at (-1 if absent) —\n * used both to read the value and to exclude both tokens from the positional args. */\nfunction flagArg(argv: string[], name: string): { value?: string; idx: number } {\n const idx = argv.indexOf(name);\n return { value: idx !== -1 ? argv[idx + 1] : undefined, idx };\n}\n\nasync function main(): Promise<void> {\n const argv = process.argv.slice(2);\n const json = argv.includes(\"--json\");\n const http = argv.includes(\"--http\");\n const out = flagArg(argv, \"--out\");\n const port = flagArg(argv, \"--port\");\n const token = flagArg(argv, \"--token\");\n\n const consumed = new Set<number>();\n for (const f of [out, port, token]) {\n if (f.idx !== -1) {\n consumed.add(f.idx);\n consumed.add(f.idx + 1);\n }\n }\n const positional = argv.filter((a, i) => !consumed.has(i) && a !== \"--json\" && a !== \"--http\");\n const [cmd, dir] = positional;\n\n if (cmd === \"apply\" && dir) {\n runApply(dir);\n return;\n }\n if (cmd === \"serve\" && dir && http) {\n // Bearer token: --token wins over ARCHSTONE_HTTP_TOKEN if both are set (Rule #7 —\n // required, never defaults open).\n runServeHttp(dir, Number(port.value ?? 8787), token.value ?? process.env.ARCHSTONE_HTTP_TOKEN);\n return; // blocks on the HTTP server\n }\n if (cmd === \"serve\" && dir) {\n await serveStdio(dir); // blocks on the stdio transport\n return;\n }\n if (cmd === \"verify\" && dir) {\n await runVerifyCmd(dir, json);\n return;\n }\n if (cmd === \"build\" && dir) {\n runBuild(dir, out.value);\n return;\n }\n if (cmd === \"init\") {\n // Everything `init` needs is in its own argv parser: it has more flags than the other four\n // verbs put together, and threading them through this function's positional logic would\n // make both harder to read.\n process.exit(await runInitCmd(argv));\n }\n\n console.error(\n // `init` is named HERE, in the verb list, and not only in the block below it. It takes a\n // spec file rather than a manifest directory, so it cannot share the first line's shape —\n // which is exactly how it came to be missing from the one line a user actually scans.\n \"usage: archstone <apply|serve|verify|build|init>\\n\\n\" +\n \" archstone <apply|serve|verify|build> <manifest-dir> [--json] [--out path]\\n\" +\n \" archstone serve --http <manifest-dir> [--port <n>] [--token <value>]\\n\" +\n \" bearer token: --token <value>, or the ARCHSTONE_HTTP_TOKEN env var (required — never serves open)\\n\" +\n \" archstone init <spec-file> --out <dir> — start here if you have no manifest yet\\n\\n\" +\n INIT_USAGE,\n );\n process.exit(2);\n}\n\nmain();\n","// `archstone init` — THIN (ADD-37 §6 step 7, D-5).\n//\n// This file owns exactly three things: argv, the terminal gate, and rendering the report.\n// Every decision of substance lives elsewhere and is testable without a terminal:\n// - what a document says → `@archstone/init`'s adapters\n// - what becomes a manifest → `emit`, pure\n// - whether anything is written → `@archstone/init/loop`, one of two terminal states\n// - whether a request is ever made → the probe gate, two independent conditions\n//\n// The gate produces DATA — a Decision Record — and nothing else. That is what lets a hosted\n// \"point us at your spec\" flow (§9's forward constraint) supply the identical structure from a\n// web form and reuse the core verbatim, and it is why this file has no business logic to test.\n\nimport { createInterface } from \"node:readline/promises\";\nimport { existsSync, readFileSync, statSync, writeFileSync } from \"node:fs\";\nimport { dirname, isAbsolute, join, relative, resolve, sep } from \"node:path\";\nimport {\n CAPABILITY_ID_RE,\n COMPANY_ID_RE,\n formatReport,\n isKnown,\n locusCandidates,\n openApiAdapter,\n valueOrUndefined,\n type CapabilityDecision,\n validateDecisionRecord,\n type DecisionRecord,\n type DraftModel,\n type DraftOperation,\n type Effect,\n type SourceAdapter,\n type SourceInput,\n} from \"@archstone/init\";\nimport { runInit } from \"@archstone/init/loop\";\n\n/** Bounded so a malformed or hostile document cannot make the host loop forever fetching. */\nconst MAX_REFERENCE_ROUNDS = 8;\n\nexport interface InitArgs {\n spec: string;\n out: string;\n domain?: string;\n company?: string;\n probe: boolean;\n decisionsFile?: string;\n interactive: boolean;\n force: boolean;\n reportFile?: string;\n}\n\nexport const INIT_USAGE = [\n \"usage: archstone init <spec-file> --out <dir> [options]\",\n \"\",\n \" Read an API description, ask you the questions no tool can answer, and write a CDL\",\n \" manifest the real compiler has already compiled. No LLM is involved, on any path.\",\n \"\",\n \" --out <dir> where the manifest goes (required)\",\n \" --domain <name> the domain half of every capability id (e.g. 'framing')\",\n \" --company <id> company id, lowercase kebab (e.g. 'acme')\",\n \" --decisions <file> a Decision Record JSON file, instead of the interactive gate.\",\n \" Each entry's `operation` is the CANDIDATE KEY, which is\",\n \" `<METHOD> <path>` with the path INCLUDING the server base path\",\n \" from `servers[0].url` — so a document whose `paths:` reads\",\n \" `/catalog/frames` under a server of `https://api.x.test/api/v1`\",\n \" has the key `GET /api/v1/catalog/frames`. Run without --decisions\",\n \" once to see the real keys, or read them off a failed run's report.\",\n \" Not combinable with --company or --domain, which it answers.\",\n \" --report <file> also write the report here (default: <out>/INIT-REPORT.md)\",\n \" --probe OPT-IN, READ-ONLY. Record a golden fixture by making ONE live\",\n \" request per capability you consent to. Never issued for a\",\n \" capability whose confirmed effect is not `read`; a non-GET/HEAD\",\n \" method needs a second, separate confirmation, and is refused\",\n \" outright when there is no terminal. Off by default.\",\n \" --non-interactive no prompts. Requires --decisions: `init` never defaults an\",\n \" `effect`, so with no human and no record there is nothing to do.\",\n \" --force write into a non-empty directory\",\n].join(\"\\n\");\n\n// ---------------------------------------------------------------------------------------\n// D-11's host half: the host fetches, the adapter stays pure.\n// ---------------------------------------------------------------------------------------\n\n/**\n * Resolve one adapter-requested reference to a real path, or refuse.\n *\n * SUBTREE ONLY. The adapter already refuses to emit a `..`, and this refuses to follow one —\n * two independent checks, because the thing being prevented is a spec file turning into an\n * arbitrary file-read primitive, and one check is one bug away from none.\n */\nexport function resolveReference(specFile: string, key: string): string | undefined {\n if (isAbsolute(key) || key.split(/[\\\\/]/).includes(\"..\")) return undefined;\n const root = dirname(resolve(specFile));\n const target = resolve(root, key);\n if (target !== root && !target.startsWith(root + sep)) return undefined;\n return existsSync(target) && statSync(target).isFile() ? target : undefined;\n}\n\n/** Read the primary document and everything the adapter asks for, to closure. */\nexport function loadSource(adapter: SourceAdapter, specFile: string): { input: SourceInput; unresolved: string[] } {\n const input: SourceInput = { origin: relative(process.cwd(), specFile) || specFile, document: readFileSync(specFile, \"utf8\"), documents: {} };\n const unresolved: string[] = [];\n if (!adapter.references) return { input, unresolved };\n\n for (let round = 0; round < MAX_REFERENCE_ROUNDS; round += 1) {\n const wanted = adapter.references(input).filter((key) => input.documents![key] === undefined && !unresolved.includes(key));\n if (wanted.length === 0) break;\n for (const key of wanted) {\n const path = resolveReference(specFile, key);\n // Unresolvable is NOT fatal here. The adapter reports what it is still missing and fails\n // closed on the operations that needed it — that division of labour is the whole point\n // of `references()` being a question rather than a demand.\n if (path === undefined) unresolved.push(key);\n else input.documents![key] = readFileSync(path, \"utf8\");\n }\n }\n return { input, unresolved };\n}\n\n// ---------------------------------------------------------------------------------------\n// The gate\n// ---------------------------------------------------------------------------------------\n\n/** Everything the gate needs to ask, so the asking itself is trivial and the ORDER is\n * reviewable. Product §11.1: the minimum keystroke path for a large spec is the design. */\nexport interface Ask {\n question(text: string, fallback?: string): Promise<string>;\n}\n\n/**\n * Why the gate can no longer ask anything — or `undefined` if this is an ordinary bug.\n *\n * Both members end the run the same way (nothing written, one line, non-zero) and are kept\n * apart only so the line is true.\n *\n * `no-more-input` — `AbortError`. Ctrl+D at a TTY raises it from `_ttyWrite`\n * (`AbortError: Aborted with Ctrl+D`), and so does the signal\n * `terminalAsk` ties to the interface's `close` — which is the case a\n * question PENDING when stdin ends takes, i.e. the ordinary piped/CI one.\n * `terminal-closed` — `ERR_USE_AFTER_CLOSE`. Strictly the NEXT question after readline has\n * already closed.\n *\n * NAMED CAREFULLY, because the obvious split is wrong. \"Cancelled\" would read as \"the user\n * changed their mind\", and the same `AbortError` covers both that and a stdin that simply ran\n * out — which is the more common one in practice. The two are indistinguishable at this point,\n * so the label and the message say only what is actually known: there is no more input.\n *\n * Detected by `name`/`code` rather than `instanceof`, because the classes Node throws are\n * internal and not exported; the name and the code are the documented parts.\n *\n * Returning `undefined` for everything else is deliberate. Swallowing a real bug as \"the user\n * changed their mind\" would be a worse silence than the stack trace this replaces.\n */\nexport function promptFailureKind(error: unknown): \"no-more-input\" | \"terminal-closed\" | undefined {\n if (!(error instanceof Error)) return undefined;\n const code = (error as { code?: string }).code;\n if (error.name === \"AbortError\" || code === \"ABORT_ERR\") return \"no-more-input\";\n if (code === \"ERR_USE_AFTER_CLOSE\") return \"terminal-closed\";\n return undefined;\n}\n\n/**\n * The REAL terminal `Ask` — and the reason it has to exist.\n *\n * `readline/promises`' signature is `question(query[, options])`, where `options` is\n * `{signal}`. Passing a fallback STRING as the second argument is silently ignored: the call\n * type-checks against a `readline.Interface` — which structurally satisfies `Ask`, since\n * `question(text, anything?)` is assignable — resolves with `\"\"` on an empty line, and drops the\n * default on the floor.\n *\n * That is exactly what shipped: `runGate` was handed the `Interface` itself, so EVERY default in\n * the gate was dead. `--company` and `--domain` did nothing interactively, the\n * `${COMPANY}_API_URL` suggestion never appeared, and a computed capability id had to be retyped\n * in full. The minimum-keystroke path product §11.1 calls \"the design\" did not exist.\n *\n * It stayed invisible because the tests drive a fake `Ask` that honours the fallback — so they\n * implement the INTERFACE, and the interface is not where the bug is. The call site passes the\n * fallback correctly; it is dropped at the boundary. Nothing that substitutes for the boundary\n * can see a bug in the boundary.\n *\n * Two things this does, and both are load-bearing:\n * - SHOWS the default, the way `confirm` already shows `[Y/n]`. A default the user cannot see\n * is not a default, it is a coincidence.\n * - Treats an empty line as the default — the identical rule `confirm` applies at its own\n * prompt, which is precisely the logic every other prompt assumed someone else was doing.\n */\nexport function terminalAsk(rl: TerminalInterface): Ask {\n // A question already PENDING when stdin reaches EOF NEVER SETTLES — readline neither resolves\n // it nor rejects it. With no handle left to wait on, Node then drains the event loop and the\n // process exits 0, having asked a question nobody answered and written nothing. Exit 0 is the\n // worst available outcome: a script that pipes answers in reports success.\n //\n // That is exactly what `printf 'a\\nb\\n…' | archstone init` did, and the reason is structural\n // rather than a race: `question` registers a ONE-SHOT line handler, and readline has no queue,\n // so every line that arrives while no question is pending is discarded. A pipe delivers all\n // its lines in one chunk, so answer 1 is consumed and answers 2..n are dropped. Piping answers\n // into the gate has never worked and cannot be made to work here — `--decisions` is the\n // supported way to answer without a human.\n //\n // Tying a signal to the interface's own `close` turns that silent exit-0 into the same clean\n // refusal Ctrl+D gets: one line, nothing written, non-zero. It cannot fire on a healthy\n // terminal, where stdin stays open until the user closes it. `MAX_PROMPT_ATTEMPTS` cannot help\n // here — a bound on ATTEMPTS never fires when the first attempt never returns.\n const controller = new AbortController();\n rl.once?.(\"close\", () => controller.abort());\n return {\n async question(text: string, fallback?: string): Promise<string> {\n const suggestion = fallback !== undefined && fallback !== \"\" ? fallback : undefined;\n const prompt = suggestion === undefined ? text : `${text.trimEnd()} [${suggestion}] `;\n let answer: string;\n try {\n answer = await rl.question(prompt, { signal: controller.signal });\n } catch (error) {\n // TRANSLATED AT THE BOUNDARY, not at the caller. `runGateOverTerminal` used to classify\n // whatever escaped the whole of `runGate`, which meant a future `AbortController`\n // anywhere inside it — a fetch with a timeout, say — would have its abort silently\n // relabelled as \"the user pressed Ctrl+D\" and reported as a clean refusal. Throwing a\n // private sentinel makes that impossible by construction: only this boundary can produce\n // one, so only this boundary's failures can be read as \"no more input\".\n const kind = promptFailureKind(error);\n if (kind === undefined) throw error;\n throw new PromptUnavailable(kind);\n }\n return answer.trim() === \"\" && suggestion !== undefined ? suggestion : answer;\n },\n };\n}\n\n/** The gate cannot ask anything further. Private to this module on purpose — see `terminalAsk`. */\nclass PromptUnavailable extends Error {\n constructor(readonly kind: \"no-more-input\" | \"terminal-closed\") {\n super(kind);\n this.name = \"PromptUnavailable\";\n }\n}\n\n/** The slice of `readline.Interface` this file uses. Narrow on purpose: a wider type is what\n * let the interface itself be passed as an `Ask` in the first place. */\nexport interface TerminalInterface {\n question(query: string, options?: { signal?: AbortSignal }): Promise<string>;\n once?(event: \"close\", listener: () => void): unknown;\n}\n\n/**\n * Run the gate against a REAL readline interface, translating cancellation into a value.\n *\n * Extracted from the command so both halves of the terminal boundary are reachable by a test\n * that constructs an actual `readline.Interface` — which is the only kind of test that could\n * have caught either of the two defects here, since both live on the far side of `Ask` and a\n * substitute for `Ask` is by construction blind to them.\n */\nexport async function runGateOverTerminal(\n draft: DraftModel,\n rl: TerminalInterface,\n args: InitArgs,\n): Promise<DecisionRecord | \"no-more-input\" | \"terminal-closed\" | undefined> {\n try {\n // `terminalAsk`, NEVER the interface itself: `rl` structurally satisfies `Ask` and silently\n // ignores the fallback, which is how every default in the gate came to be dead.\n return await runGate(draft, terminalAsk(rl), args);\n } catch (error) {\n // ONLY the sentinel, which only `terminalAsk` can throw. An abort raised by anything else\n // inside `runGate` is a bug and must keep looking like one.\n if (error instanceof PromptUnavailable) return error.kind;\n throw error;\n }\n}\n\n/**\n * How many times a prompt re-asks before the gate gives up.\n *\n * NOT politeness — a bound on REPEATED INVALID ANSWERS, so a `while (!valid)` loop cannot spin.\n * Found by a test whose script ran out of answers: the worker hit an OOM abort rather than\n * failing.\n *\n * CORRECTED: this comment used to justify the bound with \"`readline.question` resolves with `\"\"`\n * forever once stdin reaches EOF\". That is not what `readline/promises` does — verified against\n * the real interface rather than the test double that stood in for it. At EOF a PENDING question\n * never settles at all, and a question asked AFTER the close throws `ERR_USE_AFTER_CLOSE`.\n * Neither is a spin, and neither is something a bound on attempts could ever have caught: the\n * first never returns, and the second is a rejection. `terminalAsk` handles both — see there.\n * The bound is still right, for the reason above and not for the reason it used to give.\n */\nconst MAX_PROMPT_ATTEMPTS = 5;\n\n/**\n * Ask until the answer validates, or give up.\n *\n * Giving up returns `undefined` and the gate refuses the whole run — which is the correct\n * terminal state, because the alternative is defaulting a value nobody supplied, and every\n * question this gate asks exists precisely because it must not be defaulted.\n */\nasync function askUntil<T>(\n ask: Ask,\n question: string,\n parse: (answer: string) => T | undefined,\n onInvalid: () => void,\n fallback?: string,\n): Promise<T | undefined> {\n for (let attempt = 0; attempt < MAX_PROMPT_ATTEMPTS; attempt += 1) {\n const parsed = parse((await ask.question(question, fallback)).trim());\n if (parsed !== undefined) return parsed;\n onInvalid();\n }\n return undefined;\n}\n\n/** `y`/`n` with an explicit default. Anything unrecognized takes the default — a gate that\n * re-asks forever on a typo is a gate people learn to `--non-interactive` around. */\nasync function confirm(ask: Ask, text: string, fallback: boolean): Promise<boolean> {\n const answer = (await ask.question(`${text} [${fallback ? \"Y/n\" : \"y/N\"}] `)).trim().toLowerCase();\n if (answer === \"\") return fallback;\n return answer.startsWith(\"y\");\n}\n\nconst EFFECTS = new Set<Effect>([\"read\", \"write\", \"irreversible\"]);\n\n/**\n * The interactive gate. Produces a Decision Record and NOTHING else — no files, no requests.\n *\n * Two rules from product §11.1 shape the keystrokes, and both are about a 40-operation spec:\n * DEFAULT-SKIP with a bulk-keep escape (`a`), because most operations in a spec are not\n * capabilities; and `effect` PRE-FILLED ONLY FOR `GET`, blank and mandatory for everything\n * else. A pre-filled `read` on a `DELETE` is the exact keystroke that would make the\n * consequence-bearing asymmetry — the developer runs `init`, the business pays for a wrong\n * `effect` months later, through an agent, in front of a customer — land on the wrong person.\n */\nexport async function runGate(draft: DraftModel, ask: Ask, args: InitArgs): Promise<DecisionRecord | undefined> {\n const companyId = (await ask.question(\"Company id (lowercase, kebab-case) \", args.company)).trim();\n if (!COMPANY_ID_RE.test(companyId)) {\n console.error(`archstone init: '${companyId}' is not a valid company id (^[a-z][a-z0-9-]*$).`);\n return undefined;\n }\n const companyName = (await ask.question(\"Company name (for the manifest header) \", valueOrUndefined(draft.company.name))).trim();\n const domain = (await ask.question(\"Domain for these capabilities (the first half of every id) \", args.domain)).trim();\n\n // Amendment 1 §A-5 gap 4, and NF-A from the re-review: the env-var names are not derivable\n // from any source construct, so they are human answers with sane defaults — the same shape\n // as every other question here. Asked once per run, not per capability, and only for auth\n // when the source actually declared a scheme, so a public API costs zero extra keystrokes.\n const envPrefix = companyId.replace(/-/g, \"_\").toUpperCase();\n const baseUrlEnvVar = (await ask.question(\"Env var holding the backend base URL \", `${envPrefix}_API_URL`)).trim();\n const declaresAuth = draft.auth !== undefined || draft.operations.some((o) => o.auth?.kind === \"header\");\n const authEnvVar = declaresAuth\n ? (await ask.question(\"Env var holding the API credential (never its value) \", `${envPrefix}_API_TOKEN`)).trim()\n : \"\";\n\n const decisions: CapabilityDecision[] = [];\n let keepAll = false;\n\n for (const [index, candidate] of draft.operations.entries()) {\n const operation: DraftOperation = candidate;\n const summary = valueOrUndefined(operation.description) ?? \"\";\n console.log(\"\");\n console.log(`[${index + 1}/${draft.operations.length}] ${operation.key}`);\n if (summary) console.log(` ${summary}`);\n const blocking = operation.notes.filter((n) => n.code.startsWith(\"unsupported\") || n.code === \"declined\");\n for (const n of blocking) console.log(` ! ${n.code}${n.detail ? `: ${n.detail}` : \"\"}`);\n\n let keep = keepAll;\n if (!keep) {\n const answer = (await ask.question(\" keep as a capability? [y/N/a=keep all remaining] \")).trim().toLowerCase();\n if (answer === \"a\") {\n keepAll = true;\n keep = true;\n } else keep = answer.startsWith(\"y\");\n }\n if (!keep) {\n decisions.push({ operation: operation.key, keep: false });\n continue;\n }\n\n const action = valueOrUndefined(operation.suggestedAction);\n const suggestedId = domain !== \"\" && action !== undefined ? `${domain}.${action}` : undefined;\n const capabilityId = (await ask.question(\" capability id (domain.action) \", suggestedId)).trim();\n if (!CAPABILITY_ID_RE.test(capabilityId)) {\n console.error(` '${capabilityId}' is not a valid capability id — skipping this candidate.`);\n decisions.push({ operation: operation.key, keep: false, note: `invalid id '${capabilityId}' supplied at the gate` });\n continue;\n }\n\n // PRE-FILLED ONLY FOR `GET`. `effectHint` exists solely to fill this prompt, and the\n // emitter cannot see it — \"no `effect` without human confirmation\" is a property of the\n // emission signature, not a runtime check someone can route around.\n const prefill = operation.method.toUpperCase() === \"GET\" && operation.effectHint ? operation.effectHint.value : undefined;\n const effect = await askUntil<Effect>(\n ask,\n \" effect (read | write | irreversible) \",\n (answer) => (EFFECTS.has(answer as Effect) ? (answer as Effect) : undefined),\n () => console.error(\" must be one of: read, write, irreversible\"),\n prefill,\n );\n if (effect === undefined) {\n console.error(\"archstone init: no valid `effect` after several attempts — refusing rather than defaulting one.\");\n return undefined;\n }\n\n // D-14 — THE LOCUS, ASKED BEFORE THE NAME. They are the same question at two altitudes:\n // \"it returns a PartQuote\" IS the root answer, \"it returns a list of QuoteWarning\" IS the\n // array answer, and the name is unanswerable until the locus is fixed because the name\n // names the locus.\n //\n // Only asked when a choice exists. On a nine-operation spec that is three questions, not\n // nine — the census is what keeps the keystroke cost proportional.\n let responseLocus: string | undefined;\n const census = locusCandidates(operation.response);\n if (census.candidates.length > 1) {\n // R-11 IS WHY THIS PROMPT LOOKS LIKE THIS, and it is the piece the architect is least\n // confident in: a badly-worded question yields confirmed-but-wrong loci that are WORSE\n // than the silent ones they replace, because a human signed them. Nobody can answer\n // \"$.warnings[*] or root?\" on an endpoint they did not write. They can answer\n // \"a list of (code, message)\" versus \"one thing with (quotedPrice, currency)\".\n // Count-agnostic. The fixed string \"two ways\" was wrong the moment a response carried\n // root scalars plus two lists — a real shape, not a hypothetical one — and it shipped\n // because nothing in the suite reached three candidates.\n console.log(` this response could be read ${census.candidates.length} ways — which one does this capability return?`);\n for (const [index, candidate] of census.candidates.entries()) {\n const shape = candidate.kind === \"root\" ? \"one object, with fields\" : `a list, each with fields`;\n console.log(` ${index + 1}. ${shape}: ${candidate.fields.join(\", \")}`);\n console.log(` (${candidate.id})`);\n }\n // Pre-filled with the sole array-of-objects when there is exactly one — today's answer,\n // so a paginated list costs one keypress. A PROPOSAL, never a decision: the emitter\n // reads the selection and can never re-derive it.\n const collections = census.candidates.filter((c) => c.kind === \"collection\");\n // Pre-filled ONLY when there is exactly one list — that is today's answer, so a\n // paginated list costs one keypress. With two or more lists there is no defensible\n // pre-fill, and offering one would be the branch-order guess D-14 exists to remove.\n const prefill = collections.length === 1 ? String(census.candidates.indexOf(collections[0]!) + 1) : undefined;\n const picked = await askUntil<number>(\n ask,\n ` which one? [1-${census.candidates.length}] `,\n (answer) => {\n const index = Number(answer);\n return Number.isInteger(index) && index >= 1 && index <= census.candidates.length ? index : undefined;\n },\n () => console.error(` answer with a number from 1 to ${census.candidates.length}`),\n prefill,\n );\n if (picked === undefined) {\n console.error(\"archstone init: no response locus chosen after several attempts — refusing rather than guessing one.\");\n return undefined;\n }\n responseLocus = census.candidates[picked - 1]!.id;\n }\n\n const resourceName = (await ask.question(\" resource name (blank = derive from the source) \")).trim();\n\n const decision: Extract<CapabilityDecision, { keep: true }> = {\n operation: operation.key,\n keep: true,\n capabilityId,\n effect: effect as Effect,\n ...(responseLocus !== undefined ? { responseLocus } : {}),\n ...(resourceName !== \"\" ? { resourceName } : {}),\n };\n\n if (args.probe && decision.effect === \"read\") {\n decision.probe = await confirm(ask, ` record a golden fixture with ONE live ${operation.method} to the real backend?`, false);\n if (decision.probe) {\n const method = operation.method.toUpperCase();\n if (method !== \"GET\" && method !== \"HEAD\") {\n // R-8's second, SEPARATE confirmation. Worded so the thing being confirmed is the\n // method and not the effect again — a re-phrasing of the same question is not a\n // second condition.\n decision.probeNonReadMethodConfirmed = await confirm(\n ask,\n ` ${method} is not a GET. Confirm again that this request changes nothing on the backend:`,\n false,\n );\n }\n // D-13: pre-fill from the document's own `example`/`default`, and make the human\n // confirm every value. A probe carries a value to a production backend, and an\n // `example` may name a real customer's record — `init` cannot tell.\n //\n // THE FALLBACK IS KEPT HERE DELIBERATELY, against this gate's usual rule that a\n // consequence-bearing answer is typed rather than Entered (`effect` carries no\n // fallback; a non-`GET` probe needs its own second confirmation). By the time this\n // prompt appears the operator has ALREADY authorised a live read of this capability:\n // `--probe` is opt-in and off by default, consent is per capability, and a non-`GET`\n // method has already been confirmed separately. A sample value is a parameter of a call\n // already authorised, not a fresh authorisation.\n //\n // What makes Enter-to-accept legitimate is that the value is on screen AND ITS ORIGIN\n // IS NAMED. D-13's own worry is that a spec example may name a real customer's record —\n // `id.example: AV45` is a real product code, `artwork_id.example` is a made-up UUID, and\n // `init` cannot tell them apart. Only the human can, and only if they know the value\n // came from the API description rather than from their own last run. The raw source\n // locator used to be printed here, which is not the same thing: it is long enough to\n // skim past and it never says \"somebody else wrote this\".\n const sample: Record<string, unknown> = {};\n for (const field of operation.input) {\n const suggested = isKnown(field.example) ? String(field.example.value) : undefined;\n const origin = suggested === undefined ? \"\" : \" (from the API description)\";\n const required = valueOrUndefined(field.required) === true || field.in === \"path\";\n const typed = (await ask.question(` sample value for ${field.name}${required ? \"\" : \" (optional)\"}${origin} `, suggested)).trim();\n if (typed !== \"\") sample[field.name] = coerce(typed);\n }\n if (Object.keys(sample).length > 0) decision.sampleInput = sample;\n }\n }\n decisions.push(decision);\n }\n\n return {\n version: \"0\",\n company: { id: companyId, ...(companyName !== \"\" ? { name: companyName } : {}) },\n ...(baseUrlEnvVar !== \"\" ? { baseUrlEnvVar } : {}),\n ...(authEnvVar !== \"\" ? { authEnvVar } : {}),\n decisions,\n };\n}\n\n/** A typed sample value from a terminal. JSON first (so `50`, `true`, `[\"a\"]` survive), then\n * the raw string — a backend that wants the string `\"50\"` gets it by quoting. */\nfunction coerce(text: string): unknown {\n try {\n return JSON.parse(text) as unknown;\n } catch {\n return text;\n }\n}\n\n// ---------------------------------------------------------------------------------------\n// argv\n// ---------------------------------------------------------------------------------------\n\nexport function parseInitArgs(argv: string[]): InitArgs | { error: string } {\n const flag = (name: string): string | undefined => {\n const idx = argv.indexOf(name);\n return idx === -1 ? undefined : argv[idx + 1];\n };\n const valued = [\"--out\", \"--domain\", \"--company\", \"--decisions\", \"--report\"];\n const consumed = new Set<number>();\n for (const name of valued) {\n const idx = argv.indexOf(name);\n if (idx !== -1) {\n consumed.add(idx);\n consumed.add(idx + 1);\n }\n }\n const positional = argv.filter((a, i) => !consumed.has(i) && !a.startsWith(\"--\"));\n const spec = positional[1]; // positional[0] is the verb itself\n if (spec === undefined) return { error: \"a spec file is required\" };\n const out = flag(\"--out\");\n if (out === undefined) return { error: \"--out <dir> is required\" };\n\n return {\n spec,\n out,\n ...(flag(\"--domain\") !== undefined ? { domain: flag(\"--domain\")! } : {}),\n ...(flag(\"--company\") !== undefined ? { company: flag(\"--company\")! } : {}),\n probe: argv.includes(\"--probe\"),\n ...(flag(\"--decisions\") !== undefined ? { decisionsFile: flag(\"--decisions\")! } : {}),\n interactive: !argv.includes(\"--non-interactive\"),\n force: argv.includes(\"--force\"),\n ...(flag(\"--report\") !== undefined ? { reportFile: flag(\"--report\")! } : {}),\n };\n}\n\n// ---------------------------------------------------------------------------------------\n// The verb\n// ---------------------------------------------------------------------------------------\n\nexport async function runInitCmd(argv: string[]): Promise<number> {\n if (argv.includes(\"--help\") || argv.includes(\"-h\")) {\n console.log(INIT_USAGE);\n return 0;\n }\n const parsed = parseInitArgs(argv);\n if (\"error\" in parsed) {\n console.error(`archstone init: ${parsed.error}\\n\\n${INIT_USAGE}`);\n return 2;\n }\n const args = parsed;\n\n const specFile = resolve(process.cwd(), args.spec);\n if (!existsSync(specFile)) {\n console.error(`archstone init: no such file: ${specFile}`);\n return 2;\n }\n\n const adapter = openApiAdapter;\n const { input, unresolved } = loadSource(adapter, specFile);\n for (const key of unresolved) {\n console.error(`archstone init: referenced document '${key}' could not be read from the spec's own directory — operations that need it will be skipped.`);\n }\n const draft = adapter.adapt(input);\n\n if (draft.operations.length === 0) {\n console.error(`archstone init: ${adapter.id} found no candidate operations in ${args.spec}.`);\n for (const n of draft.notes) console.error(` - ${n.code}${n.detail ? `: ${n.detail}` : \"\"}`);\n return 1;\n }\n\n let record: DecisionRecord | undefined;\n if (args.decisionsFile !== undefined) {\n // C-3: a flag that answers a question the record already answers is a CONFLICT, not a\n // default. Silently ignoring it is the failure mode the `interactive` fix already closed\n // once — the user said something and the tool pretended they had not.\n const ignored = [args.company !== undefined ? \"--company\" : undefined, args.domain !== undefined ? \"--domain\" : undefined].filter(\n (f): f is string => f !== undefined,\n );\n if (ignored.length > 0) {\n console.error(\n `archstone init: ${ignored.join(\" and \")} ${ignored.length === 1 ? \"is\" : \"are\"} answered by the Decision Record and cannot be combined with --decisions.\\n` +\n ` ${ignored.includes(\"--company\") ? \"Set `company.id` in the record\" : \"\"}${ignored.length === 2 ? \"; \" : \"\"}${ignored.includes(\"--domain\") ? \"the domain is the first half of each `capabilityId` in the record\" : \"\"}.`,\n );\n return 2;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(readFileSync(resolve(process.cwd(), args.decisionsFile), \"utf8\"));\n } catch (err) {\n console.error(`archstone init: cannot read the Decision Record: ${(err as Error).message}`);\n return 2;\n }\n // C-2: the record used to be an unchecked cast, and it was the ONE input `init` trusted\n // completely while refusing to trust anything else. A missing `company` produced a raw\n // TypeError with a stack trace — on the `--non-interactive` path, which is CI, where a\n // stack trace is the least actionable output there is.\n const validation = validateDecisionRecord(parsed);\n if (!validation.ok) {\n console.error(`archstone init: the Decision Record at ${args.decisionsFile} is not valid:`);\n for (const problem of validation.problems) console.error(` - ${problem}`);\n return 2;\n }\n record = validation.record;\n } else if (!args.interactive) {\n // DoD-5(d), and the one refusal in this file that is not about the network: `init` never\n // defaults an `effect`. With no human to ask and no record to read, there is nothing to do\n // that would not be a guess about a value the business pays for months later.\n console.error(\"archstone init: --non-interactive requires --decisions <file>. `init` never defaults an `effect`.\");\n return 2;\n } else {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n let outcome: DecisionRecord | \"no-more-input\" | \"terminal-closed\" | undefined;\n try {\n outcome = await runGateOverTerminal(draft, rl, args);\n } finally {\n rl.close();\n }\n // Ctrl+D is a user saying \"I changed my mind\"; a closed stdin is a terminal that went away.\n // Both deserve the clean terminal state the refusal paths already produce — nothing written,\n // one line, non-zero — rather than the unhandled error and Node stack trace they used to\n // produce. A retry bound cannot cover either: both arrive as a REJECTED PROMISE, and\n // `MAX_PROMPT_ATTEMPTS` counts answers, not failures to be able to ask.\n if (outcome === \"no-more-input\") {\n // Deliberately NOT \"cancelled\": the same `AbortError` covers Ctrl+D and a stdin that ran\n // out, and telling a CI runner it changed its mind is a small lie that costs someone an\n // hour. The hint names the supported way to answer without a human.\n console.error(\"\\narchstone init: no more input (Ctrl+D, or stdin ended) — nothing was written.\");\n console.error(\" To answer without a human, use --decisions <file> --non-interactive.\");\n return 2;\n }\n if (outcome === \"terminal-closed\") {\n console.error(\"\\narchstone init: the terminal closed before the gate finished — nothing was written.\");\n return 2;\n }\n record = outcome;\n if (!record) return 2;\n }\n\n const result = await runInit(draft, record, {\n targetDir: resolve(process.cwd(), args.out),\n force: args.force,\n probe: args.probe,\n // \"Interactive\" for R-8's purposes means A HUMAN WAS ACTUALLY ASKED, not \"the\n // --non-interactive flag was absent\". A Decision Record file supplies every answer up\n // front, so `--decisions` without `--non-interactive` has no prompt either — and treating\n // it as interactive would let a file-supplied `probeNonReadMethodConfirmed` authorize a\n // non-GET probe against a production backend with nobody at the terminal. The second\n // confirmation is a human act performed AT THE MOMENT OF THE CALL; that is the whole\n // reason it is separate from `effect`, which a file may legitimately carry.\n interactive: args.interactive && args.decisionsFile === undefined,\n });\n\n const report = formatReport({\n origin: draft.source.origin,\n adapter: draft.source.adapter,\n targetDir: resolve(process.cwd(), args.out),\n emitted: result.emitted,\n written: result.written,\n failures: result.failures,\n probes: result.probes.map((p) => ({ capabilityId: p.capabilityId, outcome: p.outcome, detail: p.detail })),\n verifications: result.verifications,\n candidates: draft.operations.length,\n });\n console.log(`\\n${report}`);\n\n if (result.ok) {\n // The report goes to a COMMITTABLE FILE as well as to stdout (product §11.2): the file is\n // the pull-request review surface, and a reviewer who was not at the terminal is the second\n // pair of eyes on the one risk automation cannot close (R-9).\n const reportFile = args.reportFile !== undefined ? resolve(process.cwd(), args.reportFile) : join(resolve(process.cwd(), args.out), \"INIT-REPORT.md\");\n try {\n writeFileSync(reportFile, report);\n console.log(`Report also written to ${reportFile}\\n`);\n } catch (err) {\n console.error(`archstone init: could not write the report file: ${(err as Error).message}`);\n }\n }\n\n return result.ok ? 0 : 1;\n}\n"],"mappings":"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","names":["writeFileSync","resolve","index","candidate","prefill","parsed","resolve","writeFileSync"]}
|
package/package.json
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@archstone/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
|
-
"description": "CLI (
|
|
6
|
+
"description": "Archstone CLI — compile a business capability definition (CDL) into tools an AI agent can call: apply, build, serve over MCP, and verify against the live backend.",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"mcp",
|
|
9
|
+
"model-context-protocol",
|
|
10
|
+
"ai-agents",
|
|
11
|
+
"llm",
|
|
12
|
+
"archstone",
|
|
13
|
+
"cdl",
|
|
14
|
+
"cli",
|
|
15
|
+
"compiler",
|
|
16
|
+
"claude",
|
|
17
|
+
"anthropic",
|
|
18
|
+
"code-generation",
|
|
19
|
+
"api-integration"
|
|
20
|
+
],
|
|
7
21
|
"license": "Apache-2.0",
|
|
8
22
|
"homepage": "https://archstone.dev",
|
|
9
23
|
"repository": {
|
|
@@ -22,9 +36,10 @@
|
|
|
22
36
|
"access": "public"
|
|
23
37
|
},
|
|
24
38
|
"dependencies": {
|
|
25
|
-
"@archstone/compiler": "0.
|
|
26
|
-
"@archstone/
|
|
27
|
-
"@archstone/runtime": "0.
|
|
39
|
+
"@archstone/compiler": "0.10.0",
|
|
40
|
+
"@archstone/init": "0.10.0",
|
|
41
|
+
"@archstone/runtime": "0.10.0",
|
|
42
|
+
"@archstone/schema": "0.10.0"
|
|
28
43
|
},
|
|
29
44
|
"devDependencies": {
|
|
30
45
|
"tsup": "^8.5.1"
|