mikoshi-construct 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -4
- package/dist/cli.js +1029 -188
- package/package.json +3 -2
- package/templates/ai/claude/_claude/agents/architect.md +7 -11
- package/templates/ai/claude/_claude/agents/harness.md +7 -11
- package/templates/ai/claude/_claude/agents/implementer.md +6 -10
- package/templates/ai/claude/_claude/skills/implement/SKILL.md +28 -7
- package/templates/ai/claude/scripts/construct/implement.workflow.mjs +53 -9
- package/templates/ai/cursor/_cursor/rules/construct.mdc +1 -1
- package/templates/ai/shared/_claude/commands/construct-discover.md +25 -4
- package/templates/presets/monorepo/baseline/eslint.config.mjs.eta +22 -6
- package/templates/presets/monorepo/baseline/scripts/tests/lint/syntax-policy.test.ts.eta +110 -0
- package/templates/presets/node-backend/baseline/eslint.config.mjs +11 -3
- package/templates/presets/node-backend/baseline/scripts/tests/lint/syntax-policy.test.ts +86 -0
- package/templates/presets/node-frontend/baseline/eslint.config.mjs +12 -3
- package/templates/presets/node-frontend/baseline/scripts/tests/lint/syntax-policy.test.ts +78 -0
- package/templates/presets/monorepo/sample/scripts/tests/lint/syntax-policy.test.ts.eta +0 -36
- package/templates/presets/node-backend/sample/scripts/tests/lint/syntax-policy.test.ts +0 -31
package/dist/cli.js
CHANGED
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import
|
|
5
|
-
import
|
|
4
|
+
import path21 from "path";
|
|
5
|
+
import process6 from "process";
|
|
6
6
|
import { isTTY } from "@clack/prompts";
|
|
7
7
|
import { defineCommand, runMain } from "citty";
|
|
8
8
|
|
|
9
|
-
// src/commands/cost.ts
|
|
10
|
-
import
|
|
9
|
+
// src/commands/cost/index.ts
|
|
10
|
+
import process2 from "process";
|
|
11
|
+
|
|
12
|
+
// src/commands/cost/claude-code.ts
|
|
13
|
+
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "fs";
|
|
11
14
|
import { homedir } from "os";
|
|
12
15
|
import path from "path";
|
|
16
|
+
|
|
17
|
+
// src/commands/cost/usage.ts
|
|
18
|
+
var PRICE_RELATIVE_TO_INPUT = { cacheWrite: 1.25, cacheRead: 0.1, output: 5 };
|
|
13
19
|
function emptyUsage() {
|
|
14
20
|
return { calls: 0, input: 0, cacheWrite: 0, cacheRead: 0, output: 0, models: [] };
|
|
15
21
|
}
|
|
16
22
|
function billable(usage) {
|
|
17
23
|
return usage.input + usage.cacheWrite + usage.cacheRead + usage.output;
|
|
18
24
|
}
|
|
19
|
-
var PRICE_RELATIVE_TO_INPUT = { cacheWrite: 1.25, cacheRead: 0.1, output: 5 };
|
|
20
25
|
function weighted(usage) {
|
|
21
26
|
return Math.round(usage.input + usage.cacheWrite * PRICE_RELATIVE_TO_INPUT.cacheWrite + usage.cacheRead * PRICE_RELATIVE_TO_INPUT.cacheRead + usage.output * PRICE_RELATIVE_TO_INPUT.output);
|
|
22
27
|
}
|
|
@@ -31,6 +36,8 @@ function add(total, part) {
|
|
|
31
36
|
total.models.push(model);
|
|
32
37
|
}
|
|
33
38
|
}
|
|
39
|
+
|
|
40
|
+
// src/commands/cost/claude-code.ts
|
|
34
41
|
function projectKey(cwd) {
|
|
35
42
|
return cwd.replace(/[/.]/g, "-");
|
|
36
43
|
}
|
|
@@ -80,48 +87,181 @@ function collectRuns(sessionDir) {
|
|
|
80
87
|
});
|
|
81
88
|
}
|
|
82
89
|
function collectWorkflowRuns(cwd, projectsDir = claudeProjectsDir()) {
|
|
83
|
-
|
|
84
|
-
|
|
90
|
+
return directories(path.join(projectsDir, projectKey(cwd))).flatMap(collectRuns).sort((a, b) => a.startedAt.localeCompare(b.startedAt));
|
|
91
|
+
}
|
|
92
|
+
function resolvedPath(cwd) {
|
|
93
|
+
try {
|
|
94
|
+
return realpathSync(cwd);
|
|
95
|
+
} catch {
|
|
85
96
|
return null;
|
|
86
|
-
|
|
97
|
+
}
|
|
87
98
|
}
|
|
88
|
-
function
|
|
89
|
-
|
|
99
|
+
function mainWorktreePath(cwd) {
|
|
100
|
+
const dotGit = path.join(cwd, ".git");
|
|
101
|
+
if (!existsSync(dotGit) || !statSync(dotGit).isFile())
|
|
102
|
+
return null;
|
|
103
|
+
const gitdir = readFileSync(dotGit, "utf8").match(/^gitdir: *(\S.*)$/m)?.[1]?.trim();
|
|
104
|
+
return gitdir?.match(/^(.+)\/\.git\/worktrees\/[^/]+\/?$/)?.[1] ?? null;
|
|
90
105
|
}
|
|
91
|
-
function
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
106
|
+
function recordedElsewhere(cwd, key, projectsDir) {
|
|
107
|
+
return [resolvedPath(cwd), mainWorktreePath(cwd)].filter((candidate) => candidate != null).map(projectKey).filter((candidate) => candidate !== key && existsSync(path.join(projectsDir, candidate)));
|
|
108
|
+
}
|
|
109
|
+
function lookalikeKeys(cwd, key, projectsDir) {
|
|
110
|
+
const suffix = `-${path.basename(cwd)}`;
|
|
111
|
+
return directories(projectsDir).map((dir) => path.basename(dir)).filter((name) => name !== key && name.endsWith(suffix));
|
|
112
|
+
}
|
|
113
|
+
var ClaudeCodeCostSource = class {
|
|
114
|
+
constructor(projectsDir = claudeProjectsDir()) {
|
|
115
|
+
this.projectsDir = projectsDir;
|
|
95
116
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return
|
|
117
|
+
projectsDir;
|
|
118
|
+
runtime = "claude-code";
|
|
119
|
+
readable() {
|
|
120
|
+
return existsSync(this.projectsDir);
|
|
100
121
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
122
|
+
read(cwd) {
|
|
123
|
+
const key = projectKey(cwd);
|
|
124
|
+
if (existsSync(path.join(this.projectsDir, key))) {
|
|
125
|
+
const runs = collectWorkflowRuns(cwd, this.projectsDir);
|
|
126
|
+
return { status: runs.length > 0 ? "ok" : "empty", runs, key, candidates: [] };
|
|
106
127
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
128
|
+
const recorded = recordedElsewhere(cwd, key, this.projectsDir);
|
|
129
|
+
if (recorded.length > 0)
|
|
130
|
+
return { status: "mismatch", runs: [], key, candidates: recorded };
|
|
131
|
+
const lookalikes = lookalikeKeys(cwd, key, this.projectsDir);
|
|
132
|
+
if (lookalikes.length > 0)
|
|
133
|
+
return { status: "unknown", runs: [], key, candidates: lookalikes };
|
|
134
|
+
return { status: "empty", runs: [], key, candidates: [] };
|
|
110
135
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// src/commands/cost/ledger.ts
|
|
139
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
140
|
+
import path2 from "path";
|
|
141
|
+
var LEDGER_FILE = ".construct/runs.jsonl";
|
|
142
|
+
var TEXT_FIELDS = ["at", "task", "effort", "status", "rung"];
|
|
143
|
+
var COUNT_FIELDS = ["agents", "toolUses", "seconds"];
|
|
144
|
+
function isText(value) {
|
|
145
|
+
return typeof value === "string" && value !== "";
|
|
146
|
+
}
|
|
147
|
+
function isCount(value) {
|
|
148
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
149
|
+
}
|
|
150
|
+
function isTokenCount(value) {
|
|
151
|
+
return value === "unknown" || isCount(value);
|
|
152
|
+
}
|
|
153
|
+
function attemptFaults(value, index) {
|
|
154
|
+
if (value == null || typeof value !== "object")
|
|
155
|
+
return [`attempts[${index}] is not a record`];
|
|
156
|
+
const attempt = value;
|
|
157
|
+
return [
|
|
158
|
+
isCount(attempt.rung) ? null : `attempts[${index}].rung`,
|
|
159
|
+
isText(attempt.effort) ? null : `attempts[${index}].effort`,
|
|
160
|
+
isText(attempt.outcome) ? null : `attempts[${index}].outcome`,
|
|
161
|
+
typeof attempt.reason === "string" ? null : `attempts[${index}].reason`
|
|
162
|
+
].filter((fault) => fault != null);
|
|
163
|
+
}
|
|
164
|
+
function attemptListFaults(value) {
|
|
165
|
+
if (!Array.isArray(value))
|
|
166
|
+
return ["attempts"];
|
|
167
|
+
return value.flatMap(attemptFaults);
|
|
168
|
+
}
|
|
169
|
+
function undeclaredFields(record) {
|
|
170
|
+
const missing = [
|
|
171
|
+
...TEXT_FIELDS.filter((field) => !isText(record[field])),
|
|
172
|
+
...COUNT_FIELDS.filter((field) => !isCount(record[field]))
|
|
173
|
+
];
|
|
174
|
+
if (!isTokenCount(record.tokens))
|
|
175
|
+
missing.push("tokens");
|
|
176
|
+
missing.push(...attemptListFaults(record.attempts));
|
|
177
|
+
return missing;
|
|
178
|
+
}
|
|
179
|
+
function toAttempt(value) {
|
|
180
|
+
const attempt = value;
|
|
181
|
+
return { rung: attempt.rung, effort: attempt.effort, outcome: attempt.outcome, reason: attempt.reason };
|
|
182
|
+
}
|
|
183
|
+
function toEntry(raw) {
|
|
184
|
+
if (raw == null || typeof raw !== "object" || Array.isArray(raw))
|
|
185
|
+
return "not a run record";
|
|
186
|
+
const record = raw;
|
|
187
|
+
const missing = undeclaredFields(record);
|
|
188
|
+
if (missing.length > 0)
|
|
189
|
+
return `missing or invalid: ${missing.join(", ")}`;
|
|
190
|
+
return {
|
|
191
|
+
run: isText(record.run) ? record.run : null,
|
|
192
|
+
at: record.at,
|
|
193
|
+
task: record.task,
|
|
194
|
+
effort: record.effort,
|
|
195
|
+
status: record.status,
|
|
196
|
+
rung: record.rung,
|
|
197
|
+
attempts: record.attempts.map(toAttempt),
|
|
198
|
+
agents: record.agents,
|
|
199
|
+
tokens: record.tokens,
|
|
200
|
+
toolUses: record.toolUses,
|
|
201
|
+
seconds: record.seconds
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function readLedger(root) {
|
|
205
|
+
const file = path2.join(root, LEDGER_FILE);
|
|
206
|
+
const reading = { entries: [], malformed: [] };
|
|
207
|
+
if (!existsSync2(file))
|
|
208
|
+
return reading;
|
|
209
|
+
readFileSync2(file, "utf8").split("\n").forEach((text2, index) => {
|
|
210
|
+
const line = index + 1;
|
|
211
|
+
if (text2.trim() === "")
|
|
212
|
+
return;
|
|
213
|
+
let raw;
|
|
214
|
+
try {
|
|
215
|
+
raw = JSON.parse(text2);
|
|
216
|
+
} catch {
|
|
217
|
+
reading.malformed.push({ line, reason: "not JSON" });
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const entry = toEntry(raw);
|
|
221
|
+
if (typeof entry === "string")
|
|
222
|
+
reading.malformed.push({ line, reason: entry });
|
|
223
|
+
else
|
|
224
|
+
reading.entries.push(entry);
|
|
225
|
+
});
|
|
226
|
+
return reading;
|
|
227
|
+
}
|
|
228
|
+
function summarizeLedger(reading) {
|
|
229
|
+
const anyTokenUnknown = reading.entries.some((entry) => entry.tokens === "unknown");
|
|
230
|
+
const counted = reading.entries.reduce((total, entry) => total + (entry.tokens === "unknown" ? 0 : entry.tokens), 0);
|
|
231
|
+
return {
|
|
232
|
+
runs: reading.entries.length,
|
|
233
|
+
agents: reading.entries.reduce((total, entry) => total + entry.agents, 0),
|
|
234
|
+
failures: reading.entries.filter((entry) => entry.status !== "done").length,
|
|
235
|
+
tokens: anyTokenUnknown ? "unknown" : counted,
|
|
236
|
+
malformed: reading.malformed
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function withoutTokenTotals(summary) {
|
|
240
|
+
return { ...summary, tokens: "unknown" };
|
|
241
|
+
}
|
|
242
|
+
function hasLedgerFindings(summary) {
|
|
243
|
+
return summary.runs > 0 || summary.malformed.length > 0;
|
|
244
|
+
}
|
|
245
|
+
function reconcile(entries, runs) {
|
|
246
|
+
const sessionRuns = new Set(runs.map((run) => run.run));
|
|
247
|
+
const ledgerRuns = entries.map((entry) => entry.run).filter((run) => run != null);
|
|
248
|
+
const joinable = new Set(ledgerRuns);
|
|
249
|
+
return {
|
|
250
|
+
entriesWithoutSession: [...joinable].filter((run) => !sessionRuns.has(run)),
|
|
251
|
+
sessionsWithoutEntry: runs.map((run) => run.run).filter((run) => !joinable.has(run)),
|
|
252
|
+
unjoinable: entries.length - ledgerRuns.length
|
|
253
|
+
};
|
|
114
254
|
}
|
|
115
255
|
|
|
116
|
-
// src/commands/
|
|
117
|
-
import
|
|
118
|
-
import path3 from "path";
|
|
256
|
+
// src/commands/cost/runtime.ts
|
|
257
|
+
import process from "process";
|
|
119
258
|
|
|
120
259
|
// src/manifest.ts
|
|
121
260
|
import { createHash } from "crypto";
|
|
122
|
-
import { existsSync as
|
|
123
|
-
import
|
|
261
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
262
|
+
import path3 from "path";
|
|
124
263
|
var MANIFEST_FILE = "construct.json";
|
|
264
|
+
var MANIFEST_VERSION = 2;
|
|
125
265
|
var DISCOVERY_MARKERS = [
|
|
126
266
|
"product",
|
|
127
267
|
"module-map",
|
|
@@ -151,8 +291,13 @@ function buildManifest(input) {
|
|
|
151
291
|
const files = {};
|
|
152
292
|
for (const op of input.written)
|
|
153
293
|
files[op.target] = sha256(op.content);
|
|
154
|
-
const
|
|
294
|
+
const markers = Object.fromEntries(DISCOVERY_MARKERS.map((marker) => [marker, {
|
|
295
|
+
file: markerFile(marker, input.vars.compositionDir),
|
|
296
|
+
authoredBy: "unknown",
|
|
297
|
+
sha: null
|
|
298
|
+
}]));
|
|
155
299
|
return {
|
|
300
|
+
manifestVersion: MANIFEST_VERSION,
|
|
156
301
|
construct: input.version,
|
|
157
302
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
158
303
|
preset: input.preset,
|
|
@@ -163,21 +308,378 @@ function buildManifest(input) {
|
|
|
163
308
|
contracts: input.contracts ? { path: input.vars.contractPath, types: input.vars.contractTypesOutput } : null,
|
|
164
309
|
vars: input.vars,
|
|
165
310
|
files,
|
|
166
|
-
discovery
|
|
311
|
+
discovery: { baseSha: null, filledAt: null, markers }
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
function upgradeMarker(recorded, file) {
|
|
315
|
+
if (typeof recorded === "string")
|
|
316
|
+
return { file: recorded, authoredBy: "unknown", sha: null };
|
|
317
|
+
const value = recorded ?? {};
|
|
318
|
+
return {
|
|
319
|
+
file: typeof value.file === "string" ? value.file : file,
|
|
320
|
+
authoredBy: value.authoredBy === "construct" ? "construct" : "unknown",
|
|
321
|
+
sha: typeof value.sha === "string" ? value.sha : null
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
function upgradeManifest(raw) {
|
|
325
|
+
const manifest = raw;
|
|
326
|
+
const discovery = manifest.discovery ?? {};
|
|
327
|
+
const recorded = discovery.markers ?? discovery;
|
|
328
|
+
const markers = Object.fromEntries(DISCOVERY_MARKERS.map((marker) => [
|
|
329
|
+
marker,
|
|
330
|
+
upgradeMarker(recorded[marker], markerFile(marker, manifest.vars?.compositionDir))
|
|
331
|
+
]));
|
|
332
|
+
return {
|
|
333
|
+
...manifest,
|
|
334
|
+
manifestVersion: MANIFEST_VERSION,
|
|
335
|
+
discovery: {
|
|
336
|
+
baseSha: typeof discovery.baseSha === "string" ? discovery.baseSha : null,
|
|
337
|
+
filledAt: typeof discovery.filledAt === "string" ? discovery.filledAt : null,
|
|
338
|
+
markers
|
|
339
|
+
}
|
|
167
340
|
};
|
|
168
341
|
}
|
|
169
342
|
function writeManifest(root, manifest) {
|
|
170
|
-
writeFileSync(
|
|
343
|
+
writeFileSync(path3.join(root, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
|
|
171
344
|
`);
|
|
172
345
|
}
|
|
173
346
|
function readManifest(root) {
|
|
174
|
-
const file =
|
|
175
|
-
if (!
|
|
347
|
+
const file = path3.join(root, MANIFEST_FILE);
|
|
348
|
+
if (!existsSync3(file))
|
|
176
349
|
return null;
|
|
177
|
-
return JSON.parse(
|
|
350
|
+
return upgradeManifest(JSON.parse(readFileSync3(file, "utf8")));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// src/commands/cost/runtime.ts
|
|
354
|
+
var CLAUDE_CODE_ENV = ["CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT"];
|
|
355
|
+
var CURSOR_ENV = ["CURSOR_AGENT", "CURSOR_TRACE_ID"];
|
|
356
|
+
function anySet(env, names) {
|
|
357
|
+
return names.some((name) => (env[name] ?? "") !== "");
|
|
358
|
+
}
|
|
359
|
+
function resolveRuntime(root, env = process.env) {
|
|
360
|
+
if (anySet(env, CLAUDE_CODE_ENV))
|
|
361
|
+
return "claude-code";
|
|
362
|
+
if (anySet(env, CURSOR_ENV))
|
|
363
|
+
return "cursor";
|
|
364
|
+
return readManifest(root)?.ai === "cursor" ? "cursor" : "claude-code";
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// src/commands/cost/report.ts
|
|
368
|
+
var COST_EXIT = {
|
|
369
|
+
ok: 0,
|
|
370
|
+
empty: 0,
|
|
371
|
+
mismatch: 1,
|
|
372
|
+
unknown: 1,
|
|
373
|
+
unsupported: 3
|
|
374
|
+
};
|
|
375
|
+
function selectRuns(report, last) {
|
|
376
|
+
const runs = report.runs ?? [];
|
|
377
|
+
return last ? runs.slice(-1) : runs;
|
|
378
|
+
}
|
|
379
|
+
function costJson(report, last) {
|
|
380
|
+
const runs = selectRuns(report, last);
|
|
381
|
+
return {
|
|
382
|
+
status: report.status,
|
|
383
|
+
runtime: report.runtime,
|
|
384
|
+
...report.key == null ? {} : { key: report.key },
|
|
385
|
+
...report.candidates == null || report.candidates.length === 0 ? {} : { candidates: report.candidates },
|
|
386
|
+
...runs.length === 0 ? {} : { runs },
|
|
387
|
+
...report.ledger == null ? {} : { ledger: report.ledger },
|
|
388
|
+
...report.reconciliation == null ? {} : { reconciliation: report.reconciliation }
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
function fmt(value) {
|
|
392
|
+
return value.toLocaleString("en-US");
|
|
393
|
+
}
|
|
394
|
+
function tokens(value) {
|
|
395
|
+
return value === "unknown" ? "unknown" : fmt(value);
|
|
396
|
+
}
|
|
397
|
+
function printLedger(ui2, ledger, reconciliation) {
|
|
398
|
+
if (ledger == null && reconciliation == null)
|
|
399
|
+
return;
|
|
400
|
+
if (ledger != null) {
|
|
401
|
+
ui2.line(ui2.theme.dim(ui2.lore.ledgerCounts(ledger.runs, ledger.agents, ledger.failures, tokens(ledger.tokens))));
|
|
402
|
+
if (ledger.malformed.length > 0)
|
|
403
|
+
ui2.glitch(ui2.lore.ledgerMalformed(ledger.malformed.length), ledger.malformed.map((entry) => `line ${entry.line}: ${entry.reason}`));
|
|
404
|
+
}
|
|
405
|
+
if (reconciliation == null)
|
|
406
|
+
return;
|
|
407
|
+
ui2.line(ui2.theme.dim(ui2.lore.ledgerDrift(reconciliation.entriesWithoutSession.length, reconciliation.sessionsWithoutEntry.length, reconciliation.unjoinable)));
|
|
408
|
+
for (const run of reconciliation.entriesWithoutSession)
|
|
409
|
+
ui2.line(ui2.theme.dim(` ${run}: ${ui2.lore.ledgerEntryWithoutSession}`));
|
|
410
|
+
for (const run of reconciliation.sessionsWithoutEntry)
|
|
411
|
+
ui2.line(ui2.theme.dim(` ${run}: ${ui2.lore.ledgerSessionWithoutEntry}`));
|
|
412
|
+
}
|
|
413
|
+
function printRuns(ui2, runs) {
|
|
414
|
+
const grand = emptyUsage();
|
|
415
|
+
for (const run of runs) {
|
|
416
|
+
ui2.line(`${ui2.theme.accent(run.run)} ${ui2.theme.dim(`${run.startedAt} \xB7 ${run.total.models.join(", ")}`)}`);
|
|
417
|
+
for (const agent of run.agents)
|
|
418
|
+
ui2.line(` ${agent.type.padEnd(12)} ${agent.label.padEnd(28)} calls ${String(agent.usage.calls).padStart(3)} in ${fmt(agent.usage.input).padStart(8)} cache-w ${fmt(agent.usage.cacheWrite).padStart(9)} cache-r ${fmt(agent.usage.cacheRead).padStart(10)} out ${fmt(agent.usage.output).padStart(7)}`);
|
|
419
|
+
ui2.line(` ${ui2.theme.bold(`total ${fmt(billable(run.total))} billable tokens in ${run.total.calls} calls`)} ${ui2.theme.dim(`\u2248 ${fmt(weighted(run.total))} input-equivalent`)}`);
|
|
420
|
+
ui2.line();
|
|
421
|
+
add(grand, run.total);
|
|
422
|
+
}
|
|
423
|
+
if (runs.length > 1)
|
|
424
|
+
ui2.line(`${ui2.theme.bold(`${runs.length} runs: ${fmt(billable(grand))} billable tokens in ${grand.calls} calls`)} ${ui2.theme.dim(`\u2248 ${fmt(weighted(grand))} input-equivalent (cache-write \xD7${PRICE_RELATIVE_TO_INPUT.cacheWrite}, cache-read \xD7${PRICE_RELATIVE_TO_INPUT.cacheRead}, output \xD7${PRICE_RELATIVE_TO_INPUT.output})`)}`);
|
|
425
|
+
}
|
|
426
|
+
function printCost(ui2, report, last) {
|
|
427
|
+
const key = report.key ?? "";
|
|
428
|
+
const candidates = report.candidates ?? [];
|
|
429
|
+
switch (report.status) {
|
|
430
|
+
case "unsupported":
|
|
431
|
+
ui2.glitch(ui2.lore.costUnsupported(report.runtime));
|
|
432
|
+
break;
|
|
433
|
+
case "mismatch":
|
|
434
|
+
ui2.glitch(ui2.lore.costKeyMismatch(key), candidates);
|
|
435
|
+
break;
|
|
436
|
+
case "unknown":
|
|
437
|
+
ui2.glitch(ui2.lore.costKeyUnknown(key), candidates);
|
|
438
|
+
break;
|
|
439
|
+
case "empty":
|
|
440
|
+
ui2.glitch(ui2.lore.costEmpty);
|
|
441
|
+
break;
|
|
442
|
+
case "ok":
|
|
443
|
+
printRuns(ui2, selectRuns(report, last));
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
printLedger(ui2, report.ledger, report.reconciliation);
|
|
447
|
+
return COST_EXIT[report.status];
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// src/commands/cost/index.ts
|
|
451
|
+
function costReport(cwd, options = {}) {
|
|
452
|
+
const runtime = resolveRuntime(cwd, options.env ?? process2.env);
|
|
453
|
+
const reading = readLedger(cwd);
|
|
454
|
+
const ledger = summarizeLedger(reading);
|
|
455
|
+
const reported = hasLedgerFindings(ledger);
|
|
456
|
+
const source = runtime === "claude-code" ? new ClaudeCodeCostSource(options.projectsDir) : null;
|
|
457
|
+
if (source == null || !source.readable())
|
|
458
|
+
return { status: "unsupported", runtime, ...reported ? { ledger: withoutTokenTotals(ledger) } : {} };
|
|
459
|
+
const result = source.read(cwd);
|
|
460
|
+
const joinable = result.status === "ok" || result.status === "empty";
|
|
461
|
+
return {
|
|
462
|
+
runtime,
|
|
463
|
+
...result,
|
|
464
|
+
...reported ? { ledger } : {},
|
|
465
|
+
...joinable && (reported || result.runs.length > 0) ? { reconciliation: reconcile(reading.entries, result.runs) } : {}
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// src/commands/doctor/baseline.ts
|
|
470
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
471
|
+
import path4 from "path";
|
|
472
|
+
function baselineVerdict(root, manifest) {
|
|
473
|
+
const missingFiles = [];
|
|
474
|
+
const modifiedFiles = [];
|
|
475
|
+
for (const [file, hash] of Object.entries(manifest.files)) {
|
|
476
|
+
const absolute = path4.join(root, file);
|
|
477
|
+
if (!existsSync4(absolute))
|
|
478
|
+
missingFiles.push(file);
|
|
479
|
+
else if (sha256(readFileSync4(absolute, "utf8")) !== hash)
|
|
480
|
+
modifiedFiles.push(file);
|
|
481
|
+
}
|
|
482
|
+
return { missingFiles, modifiedFiles };
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// src/commands/doctor/enforcement.ts
|
|
486
|
+
function harnessReach(evidence) {
|
|
487
|
+
const command = `"${evidence.harness.command}"`;
|
|
488
|
+
if (evidence.workflows.harnessWorkflow != null)
|
|
489
|
+
return { level: "L3", evidence: `${evidence.workflows.harnessWorkflow} runs ${command}` };
|
|
490
|
+
if (evidence.hooks.runsHarness && evidence.hooks.manager != null)
|
|
491
|
+
return { level: "L2", evidence: `${evidence.hooks.manager} runs ${command}, and a local hook is bypassable with --no-verify` };
|
|
492
|
+
return { level: "L0", evidence: `no workflow and no hook configuration runs ${command}` };
|
|
493
|
+
}
|
|
494
|
+
function reached(id, state, evidence, detail) {
|
|
495
|
+
const reach = harnessReach(evidence);
|
|
496
|
+
return { id, level: reach.level, state, evidence: `${detail}; ${reach.evidence}` };
|
|
497
|
+
}
|
|
498
|
+
function absent(id, detail) {
|
|
499
|
+
return { id, level: "L0", state: "absent", evidence: detail };
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// src/commands/doctor/checks/ci.ts
|
|
503
|
+
var ID = "ci";
|
|
504
|
+
var SCOPE = "branch protection and organisation rulesets live in the GitHub API, not in the repository, so doctor cannot see whether this blocks a merge";
|
|
505
|
+
function ciCheck(evidence) {
|
|
506
|
+
const workflows = evidence.workflows;
|
|
507
|
+
const reach = harnessReach(evidence);
|
|
508
|
+
if (workflows.harnessWorkflow != null)
|
|
509
|
+
return { id: ID, level: reach.level, state: "present", evidence: `${reach.evidence}; ${SCOPE}` };
|
|
510
|
+
const seen = workflows.files.length === 0 ? `${workflows.directory} holds no workflow file` : `none of ${workflows.files.map((file) => `${workflows.directory}/${file}`).join(", ")} runs "${evidence.harness.command}"`;
|
|
511
|
+
return { id: ID, level: reach.level, state: "unknown", evidence: `${seen}; ${SCOPE}` };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// src/commands/doctor/runner.ts
|
|
515
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
516
|
+
import path5 from "path";
|
|
517
|
+
var RUNNER_CONFIG_FILES = [
|
|
518
|
+
"vitest.config.ts",
|
|
519
|
+
"vitest.config.mts",
|
|
520
|
+
"vitest.config.js",
|
|
521
|
+
"vitest.config.mjs",
|
|
522
|
+
"vite.config.ts",
|
|
523
|
+
"vite.config.mts",
|
|
524
|
+
"vite.config.js",
|
|
525
|
+
"vite.config.mjs"
|
|
526
|
+
];
|
|
527
|
+
var STRING_LITERAL = /^(['"])(.*)\1$/;
|
|
528
|
+
function literalEntries(body) {
|
|
529
|
+
const trimmed = body.trim();
|
|
530
|
+
if (trimmed === "")
|
|
531
|
+
return [];
|
|
532
|
+
const entries = trimmed.split(",").map((entry) => entry.trim()).filter((entry) => entry !== "");
|
|
533
|
+
const values = entries.map((entry) => STRING_LITERAL.exec(entry)?.[2]);
|
|
534
|
+
return values.every((value) => value != null) ? values : null;
|
|
535
|
+
}
|
|
536
|
+
function includeGlobs(source) {
|
|
537
|
+
const found = [];
|
|
538
|
+
let literal = false;
|
|
539
|
+
for (const match of source.matchAll(/include\s*:\s*/g)) {
|
|
540
|
+
const rest = source.slice(match.index + match[0].length);
|
|
541
|
+
if (!rest.startsWith("["))
|
|
542
|
+
return null;
|
|
543
|
+
const close = rest.indexOf("]");
|
|
544
|
+
if (close === -1)
|
|
545
|
+
return null;
|
|
546
|
+
const entries = literalEntries(rest.slice(1, close));
|
|
547
|
+
if (entries == null)
|
|
548
|
+
return null;
|
|
549
|
+
literal = true;
|
|
550
|
+
found.push(...entries);
|
|
551
|
+
}
|
|
552
|
+
return literal ? found : null;
|
|
553
|
+
}
|
|
554
|
+
function escapeLiteral(character) {
|
|
555
|
+
return /[.+^${}()|[\]\\]/.test(character) ? `\\${character}` : character;
|
|
556
|
+
}
|
|
557
|
+
function globToRegExp(glob) {
|
|
558
|
+
let pattern = "";
|
|
559
|
+
let braces = 0;
|
|
560
|
+
for (let index = 0; index < glob.length; index += 1) {
|
|
561
|
+
const character = glob[index];
|
|
562
|
+
if (character === "*" && glob[index + 1] === "*" && glob[index + 2] === "/") {
|
|
563
|
+
pattern += "(?:[^/]+/)*";
|
|
564
|
+
index += 2;
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
if (character === "*" && glob[index + 1] === "*") {
|
|
568
|
+
pattern += ".*";
|
|
569
|
+
index += 1;
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
if (character === "*") {
|
|
573
|
+
pattern += "[^/]*";
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
if (character === "?") {
|
|
577
|
+
pattern += "[^/]";
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
if (character === "{") {
|
|
581
|
+
braces += 1;
|
|
582
|
+
pattern += "(?:";
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
if (character === "}" && braces > 0) {
|
|
586
|
+
braces -= 1;
|
|
587
|
+
pattern += ")";
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
if (character === "," && braces > 0) {
|
|
591
|
+
pattern += "|";
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
pattern += escapeLiteral(character);
|
|
595
|
+
}
|
|
596
|
+
return new RegExp(`^${pattern}$`);
|
|
597
|
+
}
|
|
598
|
+
function matchesAnyGlob(file, globs) {
|
|
599
|
+
const normalized = file.replace(/^\.\//, "");
|
|
600
|
+
return globs.some((glob) => globToRegExp(glob.replace(/^\.\//, "")).test(normalized));
|
|
601
|
+
}
|
|
602
|
+
function readRunnerFacts(root, harnessText) {
|
|
603
|
+
const invokedByHarness = harnessText.includes("vitest");
|
|
604
|
+
const file = RUNNER_CONFIG_FILES.find((candidate) => existsSync5(path5.join(root, candidate))) ?? null;
|
|
605
|
+
if (file == null) {
|
|
606
|
+
return {
|
|
607
|
+
file: null,
|
|
608
|
+
globs: null,
|
|
609
|
+
note: `no runner config file (${RUNNER_CONFIG_FILES[0]} or a sibling) exists, so the include list cannot be read`,
|
|
610
|
+
invokedByHarness
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
let source;
|
|
614
|
+
try {
|
|
615
|
+
source = readFileSync5(path5.join(root, file), "utf8");
|
|
616
|
+
} catch {
|
|
617
|
+
return { file, globs: null, note: `${file} cannot be read, so the include list is unknown`, invokedByHarness };
|
|
618
|
+
}
|
|
619
|
+
const globs = includeGlobs(source);
|
|
620
|
+
if (globs == null)
|
|
621
|
+
return { file, globs: null, note: `the include in ${file} is not a literal list of strings, so doctor cannot say what the runner collects`, invokedByHarness };
|
|
622
|
+
return { file, globs, note: `${file} includes ${globs.map((glob) => `"${glob}"`).join(", ")}`, invokedByHarness };
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// src/commands/doctor/checks/construct-tests.ts
|
|
626
|
+
var ID2 = "construct-tests";
|
|
627
|
+
function constructTestsCheck(evidence) {
|
|
628
|
+
const recorded = evidence.recordedTests;
|
|
629
|
+
if (recorded.length === 0)
|
|
630
|
+
return absent(ID2, "construct.json records no test file; weakest link: there are no construct tests to run");
|
|
631
|
+
const runner = evidence.runner;
|
|
632
|
+
if (runner.globs == null)
|
|
633
|
+
return reached(ID2, "unknown", evidence, `construct.json records ${recorded.length} test files, but ${runner.note}`);
|
|
634
|
+
const orphan = recorded.find((file) => !matchesAnyGlob(file, runner.globs ?? []));
|
|
635
|
+
if (orphan != null)
|
|
636
|
+
return absent(ID2, `${orphan} is recorded in construct.json, but ${runner.note}, so the runner never collects it; weakest link: a construct test that nothing runs`);
|
|
637
|
+
if (!runner.invokedByHarness)
|
|
638
|
+
return absent(ID2, `construct.json records ${recorded.length} test files and ${runner.note}, but "${evidence.harness.script}" does not run the test runner; weakest link: the harness command never reaches them`);
|
|
639
|
+
return reached(ID2, "present", evidence, `all ${recorded.length} test files recorded in construct.json match the include in ${runner.file}, and "${evidence.harness.script}" runs the test runner`);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// src/commands/doctor/checks/hook.ts
|
|
643
|
+
var ID3 = "hook";
|
|
644
|
+
function hookCheck(evidence) {
|
|
645
|
+
const hooks = evidence.hooks;
|
|
646
|
+
if (hooks.manager != null) {
|
|
647
|
+
const runs = hooks.runsHarness ? `runs "${evidence.harness.command}"` : `does not run "${evidence.harness.command}"`;
|
|
648
|
+
return { id: ID3, level: "L2", state: "present", evidence: `${hooks.manager} installs a git hook and ${runs}; a local hook is bypassable with --no-verify` };
|
|
649
|
+
}
|
|
650
|
+
if (hooks.script != null)
|
|
651
|
+
return { id: ID3, level: "L0", state: "present", evidence: `package.json script "${hooks.script}" is claimed as a guard, but no .husky, lefthook, simple-git-hooks or core.hooksPath configuration installs it; weakest link: nobody is obliged to run it` };
|
|
652
|
+
return { id: ID3, level: "L0", state: "absent", evidence: "no .husky, lefthook, simple-git-hooks or core.hooksPath configuration and no pre-commit script in package.json" };
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// src/commands/doctor/checks/lint-policy.ts
|
|
656
|
+
var ID4 = "lint-policy";
|
|
657
|
+
function lintPolicyCheck(evidence) {
|
|
658
|
+
const policy = evidence.policyTests[0];
|
|
659
|
+
if (policy == null) {
|
|
660
|
+
if (evidence.unreadable.length > 0)
|
|
661
|
+
return reached(ID4, "unknown", evidence, `${evidence.unreadable[0]} cannot be read, so doctor cannot say whether a lint policy check exists`);
|
|
662
|
+
return absent(ID4, "no file recorded in construct.json runs ESLint over the lint policy this repository declares; weakest link: the repository has no policy check to run");
|
|
663
|
+
}
|
|
664
|
+
const runner = evidence.runner;
|
|
665
|
+
if (runner.globs == null)
|
|
666
|
+
return reached(ID4, "unknown", evidence, `${policy} runs ESLint over the lint policy this repository declares, but ${runner.note}`);
|
|
667
|
+
if (!matchesAnyGlob(policy, runner.globs))
|
|
668
|
+
return absent(ID4, `${policy} runs ESLint over the lint policy this repository declares, but ${runner.note}; weakest link: the runner never collects the policy check`);
|
|
669
|
+
if (!runner.invokedByHarness)
|
|
670
|
+
return absent(ID4, `${policy} runs ESLint over the lint policy this repository declares and ${runner.note}, but "${evidence.harness.script}" does not run the test runner; weakest link: the harness command never reaches the policy check`);
|
|
671
|
+
return reached(ID4, "present", evidence, `${policy} runs ESLint over the lint policy this repository declares, ${runner.note}, and "${evidence.harness.script}" runs the test runner`);
|
|
178
672
|
}
|
|
179
673
|
|
|
180
|
-
// src/commands/doctor.ts
|
|
674
|
+
// src/commands/doctor/checks/red-gate.ts
|
|
675
|
+
var ID5 = "red-gate";
|
|
676
|
+
function redGateCheck(evidence) {
|
|
677
|
+
return reached(ID5, "unknown", evidence, `doctor executes nothing from the repository it inspects, so whether "${evidence.harness.command}" passes on a clean checkout is unproven here; CI is where that is proven`);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// src/commands/doctor/discovery.ts
|
|
681
|
+
import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
|
|
682
|
+
import path6 from "path";
|
|
181
683
|
var DISCOVERY_PLACEHOLDER = "_Not discovered yet \u2014 run `/construct-discover`._";
|
|
182
684
|
function markerOpen(marker) {
|
|
183
685
|
return `<!-- construct:discover:${marker} -->`;
|
|
@@ -185,66 +687,290 @@ function markerOpen(marker) {
|
|
|
185
687
|
function markerClose(marker) {
|
|
186
688
|
return `<!-- /construct:discover:${marker} -->`;
|
|
187
689
|
}
|
|
188
|
-
function
|
|
690
|
+
function blockBody(document, marker) {
|
|
189
691
|
const start = document.indexOf(markerOpen(marker));
|
|
190
692
|
const stop = document.indexOf(markerClose(marker));
|
|
191
693
|
if (start === -1 || stop === -1 || stop < start)
|
|
192
|
-
return
|
|
694
|
+
return null;
|
|
193
695
|
const body = document.slice(start + markerOpen(marker).length, stop).trim();
|
|
194
|
-
return body
|
|
696
|
+
return body === "" || body === DISCOVERY_PLACEHOLDER ? null : body;
|
|
195
697
|
}
|
|
698
|
+
function compositionBody(directory) {
|
|
699
|
+
if (!existsSync6(directory))
|
|
700
|
+
return null;
|
|
701
|
+
const models = readdirSync2(directory).filter((file) => file.endsWith(".yaml")).sort();
|
|
702
|
+
if (models.length === 0)
|
|
703
|
+
return null;
|
|
704
|
+
return models.map((model) => `${model}
|
|
705
|
+
${readFileSync6(path6.join(directory, model), "utf8")}`).join("\n");
|
|
706
|
+
}
|
|
707
|
+
function markerBody(root, marker, file) {
|
|
708
|
+
const location = path6.join(root, file);
|
|
709
|
+
if (marker === "composition")
|
|
710
|
+
return compositionBody(location);
|
|
711
|
+
if (!existsSync6(location))
|
|
712
|
+
return null;
|
|
713
|
+
return blockBody(readFileSync6(location, "utf8"), marker);
|
|
714
|
+
}
|
|
715
|
+
function missingDiscovery(root, manifest) {
|
|
716
|
+
return DISCOVERY_MARKERS.filter((marker) => markerBody(root, marker, manifest.discovery.markers[marker].file) == null);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// src/commands/doctor/evidence.ts
|
|
720
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
721
|
+
import path10 from "path";
|
|
722
|
+
|
|
723
|
+
// src/commands/doctor/harness.ts
|
|
724
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
|
|
725
|
+
import path7 from "path";
|
|
196
726
|
var REQUIRED_QUALITY_STEPS = ["lint", "typecheck", "test"];
|
|
197
|
-
|
|
727
|
+
var SCRIPT_REFERENCE = /(?:^|&&|\|\||;)\s*(?:pnpm|npm|yarn|bun)\s+(?:run\s+)?([\w:.-]+)/g;
|
|
728
|
+
function harnessScriptName(command) {
|
|
729
|
+
return command.replace(/^(pnpm|npm|yarn|bun)\s+(run\s+)?/, "");
|
|
730
|
+
}
|
|
731
|
+
function expandScript(scripts, name, seen) {
|
|
732
|
+
if (seen.has(name))
|
|
733
|
+
return "";
|
|
734
|
+
seen.add(name);
|
|
735
|
+
const body = scripts[name];
|
|
736
|
+
if (body == null)
|
|
737
|
+
return "";
|
|
738
|
+
const referenced = [...body.matchAll(SCRIPT_REFERENCE)].map((match) => match[1]);
|
|
739
|
+
return [body, ...referenced.map((reference) => expandScript(scripts, reference, seen))].join(" && ");
|
|
740
|
+
}
|
|
741
|
+
function readHarnessFacts(root, command) {
|
|
742
|
+
const script = harnessScriptName(command);
|
|
743
|
+
const manifestPath = path7.join(root, "package.json");
|
|
744
|
+
const packageJson = existsSync7(manifestPath) ? JSON.parse(readFileSync7(manifestPath, "utf8")) : null;
|
|
745
|
+
const scripts = packageJson?.scripts ?? {};
|
|
746
|
+
const body = scripts[script] ?? null;
|
|
747
|
+
return {
|
|
748
|
+
command,
|
|
749
|
+
script,
|
|
750
|
+
scripts,
|
|
751
|
+
body,
|
|
752
|
+
resolved: expandScript(scripts, script, /* @__PURE__ */ new Set()),
|
|
753
|
+
commandForms: [command, `pnpm run ${script}`, `pnpm ${script}`, `npm run ${script}`, `yarn ${script}`],
|
|
754
|
+
packageJson
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
function runsHarnessCommand(text2, forms) {
|
|
758
|
+
return forms.some((form) => text2.includes(form));
|
|
759
|
+
}
|
|
760
|
+
function contractProblems(root, contracts, script, body) {
|
|
198
761
|
if (contracts == null)
|
|
199
762
|
return [];
|
|
200
|
-
const problems = [contracts.path, contracts.types].filter((file) => !
|
|
201
|
-
if (!
|
|
202
|
-
problems.push(`"${
|
|
763
|
+
const problems = [contracts.path, contracts.types].filter((file) => !existsSync7(path7.join(root, file))).map((file) => `${file} is missing (construct.json \u2192 contracts)`);
|
|
764
|
+
if (!body.includes("contracts:check"))
|
|
765
|
+
problems.push(`"${script}" does not run contracts:check`);
|
|
203
766
|
return problems;
|
|
204
767
|
}
|
|
205
|
-
function harnessProblems(root, manifest) {
|
|
206
|
-
|
|
207
|
-
const manifestPath = path3.join(root, "package.json");
|
|
208
|
-
if (!existsSync3(manifestPath))
|
|
768
|
+
function harnessProblems(root, manifest, facts) {
|
|
769
|
+
if (facts.packageJson == null)
|
|
209
770
|
return ["package.json is missing"];
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const quality = scripts[scriptName];
|
|
214
|
-
if (quality == null)
|
|
215
|
-
return [`package.json has no "${scriptName}" script (harness command is "${command}")`];
|
|
771
|
+
const body = facts.body;
|
|
772
|
+
if (body == null)
|
|
773
|
+
return [`package.json has no "${facts.script}" script (harness command is "${facts.command}")`];
|
|
216
774
|
return [
|
|
217
|
-
...REQUIRED_QUALITY_STEPS.filter((step) => !
|
|
218
|
-
...contractProblems(root, manifest.contracts,
|
|
775
|
+
...REQUIRED_QUALITY_STEPS.filter((step) => !body.includes(step)).map((step) => `"${facts.script}" does not run ${step}`),
|
|
776
|
+
...contractProblems(root, manifest.contracts, facts.script, body)
|
|
219
777
|
];
|
|
220
778
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
779
|
+
|
|
780
|
+
// src/commands/doctor/hooks.ts
|
|
781
|
+
import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync8, statSync as statSync2 } from "fs";
|
|
782
|
+
import path8 from "path";
|
|
783
|
+
var LEFTHOOK_FILES = ["lefthook.yml", "lefthook.yaml", "lefthook.toml", "lefthook.json", ".lefthook.yml", ".lefthook.yaml"];
|
|
784
|
+
var SIMPLE_GIT_HOOKS_FILES = [".simple-git-hooks.js", ".simple-git-hooks.cjs", ".simple-git-hooks.mjs", ".simple-git-hooks.json", "simple-git-hooks.json"];
|
|
785
|
+
var HUSKY_DIR = ".husky";
|
|
786
|
+
var GIT_CONFIG = ".git/config";
|
|
787
|
+
var HOOK_SCRIPTS = ["precommit", "pre-commit", "prepush", "pre-push"];
|
|
788
|
+
function read(root, file) {
|
|
789
|
+
try {
|
|
790
|
+
return readFileSync8(path8.join(root, file), "utf8");
|
|
791
|
+
} catch {
|
|
224
792
|
return null;
|
|
225
|
-
const missingFiles = [];
|
|
226
|
-
const modifiedFiles = [];
|
|
227
|
-
for (const [file, hash] of Object.entries(manifest.files)) {
|
|
228
|
-
const absolute = path3.join(root, file);
|
|
229
|
-
if (!existsSync3(absolute))
|
|
230
|
-
missingFiles.push(file);
|
|
231
|
-
else if (sha256(readFileSync3(absolute, "utf8")) !== hash)
|
|
232
|
-
modifiedFiles.push(file);
|
|
233
793
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
return
|
|
794
|
+
}
|
|
795
|
+
function huskyHooks(root) {
|
|
796
|
+
const directory = path8.join(root, HUSKY_DIR);
|
|
797
|
+
if (!existsSync8(directory) || !statSync2(directory).isDirectory())
|
|
798
|
+
return [];
|
|
799
|
+
return readdirSync3(directory).filter((entry) => !entry.startsWith("_") && !entry.startsWith(".")).sort().map((entry) => `${HUSKY_DIR}/${entry}`);
|
|
800
|
+
}
|
|
801
|
+
function managerFiles(root, packageJson) {
|
|
802
|
+
const files = [...huskyHooks(root)];
|
|
803
|
+
files.push(...[...LEFTHOOK_FILES, ...SIMPLE_GIT_HOOKS_FILES].filter((file) => existsSync8(path8.join(root, file))));
|
|
804
|
+
if (packageJson != null && "simple-git-hooks" in packageJson)
|
|
805
|
+
files.push("package.json (simple-git-hooks)");
|
|
806
|
+
const gitConfig = read(root, GIT_CONFIG);
|
|
807
|
+
if (gitConfig != null && gitConfig.includes("hooksPath"))
|
|
808
|
+
files.push(`${GIT_CONFIG} (core.hooksPath)`);
|
|
809
|
+
return files;
|
|
810
|
+
}
|
|
811
|
+
function readHookFacts(root, packageJson, scripts, commandForms) {
|
|
812
|
+
const files = managerFiles(root, packageJson);
|
|
813
|
+
const script = HOOK_SCRIPTS.find((name) => scripts[name] != null) ?? null;
|
|
814
|
+
const sources = files.map((file) => {
|
|
815
|
+
if (file.startsWith("package.json"))
|
|
816
|
+
return JSON.stringify(packageJson?.["simple-git-hooks"] ?? "");
|
|
817
|
+
return read(root, file.split(" ")[0]) ?? "";
|
|
239
818
|
});
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
819
|
+
const manager = files[0] ?? null;
|
|
820
|
+
const scriptRunsHarness = script != null && runsHarnessCommand(scripts[script] ?? "", commandForms);
|
|
821
|
+
const installed = sources.some((source) => runsHarnessCommand(source, commandForms) || scriptRunsHarness && script != null && source.includes(script));
|
|
822
|
+
return { manager, script, runsHarness: manager != null && installed };
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// src/commands/doctor/workflows.ts
|
|
826
|
+
import { existsSync as existsSync9, readdirSync as readdirSync4, readFileSync as readFileSync9 } from "fs";
|
|
827
|
+
import path9 from "path";
|
|
828
|
+
var WORKFLOWS_DIR = ".github/workflows";
|
|
829
|
+
var RUN_STEP = /(?:^|\s)run:[ \t]*(\S.*)$/;
|
|
830
|
+
function runStepTexts(source) {
|
|
831
|
+
const lines = source.split("\n");
|
|
832
|
+
const steps = [];
|
|
833
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
834
|
+
const inline = RUN_STEP.exec(lines[index])?.[1];
|
|
835
|
+
if (inline == null)
|
|
836
|
+
continue;
|
|
837
|
+
if (!inline.startsWith("|") && !inline.startsWith(">")) {
|
|
838
|
+
steps.push(inline.trim());
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
const indent = lines[index].length - lines[index].trimStart().length;
|
|
842
|
+
const block = [];
|
|
843
|
+
for (let next = index + 1; next < lines.length; next += 1) {
|
|
844
|
+
const line = lines[next];
|
|
845
|
+
if (line.trim() !== "" && line.length - line.trimStart().length <= indent)
|
|
846
|
+
break;
|
|
847
|
+
block.push(line.trim());
|
|
848
|
+
}
|
|
849
|
+
steps.push(block.join("\n"));
|
|
850
|
+
}
|
|
851
|
+
return steps;
|
|
852
|
+
}
|
|
853
|
+
function readWorkflowFacts(root, commandForms) {
|
|
854
|
+
const directory = path9.join(root, WORKFLOWS_DIR);
|
|
855
|
+
if (!existsSync9(directory))
|
|
856
|
+
return { directory: WORKFLOWS_DIR, files: [], harnessWorkflow: null, unreadable: [] };
|
|
857
|
+
const files = readdirSync4(directory).filter((file) => file.endsWith(".yml") || file.endsWith(".yaml")).sort();
|
|
858
|
+
const unreadable = [];
|
|
859
|
+
let harnessWorkflow = null;
|
|
860
|
+
for (const file of files) {
|
|
861
|
+
let source;
|
|
862
|
+
try {
|
|
863
|
+
source = readFileSync9(path9.join(directory, file), "utf8");
|
|
864
|
+
} catch {
|
|
865
|
+
unreadable.push(`${WORKFLOWS_DIR}/${file}`);
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
if (harnessWorkflow == null && runStepTexts(source).some((step) => runsHarnessCommand(step, commandForms)))
|
|
869
|
+
harnessWorkflow = `${WORKFLOWS_DIR}/${file}`;
|
|
870
|
+
}
|
|
871
|
+
return { directory: WORKFLOWS_DIR, files, harnessWorkflow, unreadable };
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// src/commands/doctor/evidence.ts
|
|
875
|
+
var TEST_FILE = /\.test\.[cm]?[jt]s$/;
|
|
876
|
+
var LINT_POLICY_MARKERS = ["calculateConfigForFile", "lintText", "lintFiles"];
|
|
877
|
+
function recordedTestFiles(manifest) {
|
|
878
|
+
return Object.keys(manifest.files).filter((file) => TEST_FILE.test(file)).sort();
|
|
879
|
+
}
|
|
880
|
+
function gatherEvidence(root, manifest) {
|
|
881
|
+
const harness = readHarnessFacts(root, manifest.harness.command);
|
|
882
|
+
const runner = readRunnerFacts(root, harness.resolved);
|
|
883
|
+
const workflows = readWorkflowFacts(root, harness.commandForms);
|
|
884
|
+
const hooks = readHookFacts(root, harness.packageJson, harness.scripts, harness.commandForms);
|
|
885
|
+
const recordedTests = recordedTestFiles(manifest);
|
|
886
|
+
const policyTests = [];
|
|
887
|
+
const unreadable = [...workflows.unreadable];
|
|
888
|
+
for (const file of recordedTests) {
|
|
889
|
+
try {
|
|
890
|
+
const source = readFileSync10(path10.join(root, file), "utf8");
|
|
891
|
+
if (LINT_POLICY_MARKERS.some((marker) => source.includes(marker)))
|
|
892
|
+
policyTests.push(file);
|
|
893
|
+
} catch {
|
|
894
|
+
unreadable.push(file);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
return { harness, runner, workflows, hooks, recordedTests, policyTests, unreadable };
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// src/commands/doctor/provenance.ts
|
|
901
|
+
function markerAuthorship(recorded, body) {
|
|
902
|
+
if (recorded.authoredBy !== "construct" || recorded.sha == null || body == null)
|
|
903
|
+
return "unknown";
|
|
904
|
+
return sha256(body) === recorded.sha ? "construct" : "owner";
|
|
905
|
+
}
|
|
906
|
+
function discoveryProvenance(root, manifest) {
|
|
907
|
+
return DISCOVERY_MARKERS.map((marker) => {
|
|
908
|
+
const recorded = manifest.discovery.markers[marker];
|
|
909
|
+
return {
|
|
910
|
+
marker,
|
|
911
|
+
file: recorded.file,
|
|
912
|
+
authorship: markerAuthorship(recorded, markerBody(root, marker, recorded.file))
|
|
913
|
+
};
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
function constructAuthored(readings) {
|
|
917
|
+
return readings.filter((reading) => reading.authorship === "construct");
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// src/commands/doctor/typecheck.ts
|
|
921
|
+
var CAVEATS = {
|
|
922
|
+
"node-frontend": {
|
|
923
|
+
checkers: ["vue-tsc", "svelte-check"],
|
|
924
|
+
text: "node-frontend: `tsc --noEmit` does not see `.vue` or `.svelte` single-file components, so a Vite app that adds them needs the framework's own checker (vue-tsc, svelte-check) in the harness"
|
|
925
|
+
}
|
|
926
|
+
};
|
|
927
|
+
function typecheckWarnings(preset, evidence) {
|
|
928
|
+
const caveat = CAVEATS[preset];
|
|
929
|
+
if (caveat == null || caveat.checkers.some((checker) => evidence.harness.resolved.includes(checker)))
|
|
930
|
+
return [];
|
|
931
|
+
return [caveat.text];
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// src/commands/doctor/verdict.ts
|
|
935
|
+
var CHECK_IDS = ["lint-policy", "construct-tests", "ci", "hook", "red-gate"];
|
|
936
|
+
var LEVELS = ["L0", "L1", "L2", "L3", "L4"];
|
|
937
|
+
function weakestLink(checks) {
|
|
938
|
+
const claimed = checks.filter((check) => check.state === "present");
|
|
939
|
+
if (claimed.length === 0)
|
|
940
|
+
return null;
|
|
941
|
+
const ordered = [...claimed].sort((left, right) => CHECK_IDS.indexOf(left.id) - CHECK_IDS.indexOf(right.id));
|
|
942
|
+
const weakest = ordered.reduce((current, check) => LEVELS.indexOf(check.level) < LEVELS.indexOf(current.level) ? check : current);
|
|
943
|
+
return { id: weakest.id, level: weakest.level };
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// src/commands/doctor/report.ts
|
|
947
|
+
function checkLine(ui2, check) {
|
|
948
|
+
return ` ${check.id.padEnd(16)} ${check.level} ${check.state.padEnd(8)} ${ui2.theme.dim(check.evidence)}`;
|
|
949
|
+
}
|
|
950
|
+
function printChecks(ui2, checks) {
|
|
951
|
+
ui2.line();
|
|
952
|
+
ui2.line(ui2.theme.accent(ui2.lore.enforcement));
|
|
953
|
+
for (const check of checks)
|
|
954
|
+
ui2.line(checkLine(ui2, check));
|
|
955
|
+
}
|
|
956
|
+
function printProvenance(ui2, provenance) {
|
|
957
|
+
const authored = constructAuthored(provenance);
|
|
958
|
+
if (authored.length === 0)
|
|
959
|
+
return;
|
|
960
|
+
ui2.line();
|
|
961
|
+
ui2.line(ui2.theme.accent(ui2.lore.provenance));
|
|
962
|
+
for (const reading of authored)
|
|
963
|
+
ui2.line(` ${reading.marker.padEnd(20)} ${ui2.theme.dim(reading.file)}`);
|
|
964
|
+
ui2.line(ui2.theme.dim(` ${ui2.lore.stillConstructAuthored(authored.length)}`));
|
|
965
|
+
}
|
|
966
|
+
function printWeakestLink(ui2, weakest) {
|
|
967
|
+
ui2.line();
|
|
968
|
+
if (weakest == null) {
|
|
969
|
+
ui2.line(ui2.theme.bold(ui2.lore.weakestLinkNone));
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
const lift = ui2.lore.levelLift[weakest.level];
|
|
973
|
+
ui2.line(`${ui2.theme.bold(ui2.lore.weakestLink(weakest.id, weakest.level))}${lift == null ? "" : ui2.theme.dim(` \u2014 ${lift}`)}`);
|
|
248
974
|
}
|
|
249
975
|
function printDoctor(ui2, result) {
|
|
250
976
|
if (result == null) {
|
|
@@ -255,124 +981,198 @@ function printDoctor(ui2, result) {
|
|
|
255
981
|
ui2.glitch("Harness is broken.", result.harnessProblems);
|
|
256
982
|
if (result.missingFiles.length > 0)
|
|
257
983
|
ui2.glitch("Baseline files are missing.", result.missingFiles);
|
|
258
|
-
if (result.missingDiscovery.length > 0)
|
|
984
|
+
if (result.missingDiscovery.length > 0)
|
|
259
985
|
ui2.glitch(ui2.lore.discoveryIncomplete, ["", "Missing:", ...result.missingDiscovery.map((marker) => ` ${marker}`), "", "Run: claude \u2192 /construct-discover"]);
|
|
260
|
-
|
|
986
|
+
if (result.warnings.length > 0)
|
|
987
|
+
ui2.glitch(ui2.lore.typecheckCaveat, result.warnings);
|
|
261
988
|
if (result.modifiedFiles.length > 0)
|
|
262
989
|
ui2.line(ui2.theme.dim(` ${result.modifiedFiles.length} baseline files modified since init (expected once the project evolves).`));
|
|
263
|
-
if (result.ok)
|
|
990
|
+
if (result.ok)
|
|
264
991
|
ui2.ok(ui2.lore.stable);
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
992
|
+
printProvenance(ui2, result.provenance);
|
|
993
|
+
printChecks(ui2, result.checks);
|
|
994
|
+
printWeakestLink(ui2, result.weakestLink);
|
|
995
|
+
return result.ok ? 0 : 1;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// src/commands/doctor/index.ts
|
|
999
|
+
function runDoctor(root) {
|
|
1000
|
+
const manifest = readManifest(root);
|
|
1001
|
+
if (manifest == null)
|
|
1002
|
+
return null;
|
|
1003
|
+
const evidence = gatherEvidence(root, manifest);
|
|
1004
|
+
const baseline = baselineVerdict(root, manifest);
|
|
1005
|
+
const problems = harnessProblems(root, manifest, evidence.harness);
|
|
1006
|
+
const checks = [
|
|
1007
|
+
lintPolicyCheck(evidence),
|
|
1008
|
+
constructTestsCheck(evidence),
|
|
1009
|
+
ciCheck(evidence),
|
|
1010
|
+
hookCheck(evidence),
|
|
1011
|
+
redGateCheck(evidence)
|
|
1012
|
+
];
|
|
1013
|
+
return {
|
|
1014
|
+
ok: baseline.missingFiles.length === 0 && problems.length === 0,
|
|
1015
|
+
missingFiles: baseline.missingFiles,
|
|
1016
|
+
modifiedFiles: baseline.modifiedFiles,
|
|
1017
|
+
missingDiscovery: missingDiscovery(root, manifest),
|
|
1018
|
+
provenance: discoveryProvenance(root, manifest),
|
|
1019
|
+
harnessProblems: problems,
|
|
1020
|
+
warnings: typecheckWarnings(manifest.preset, evidence),
|
|
1021
|
+
checks,
|
|
1022
|
+
weakestLink: weakestLink(checks)
|
|
1023
|
+
};
|
|
268
1024
|
}
|
|
269
1025
|
|
|
270
1026
|
// src/commands/init.ts
|
|
271
1027
|
import { mkdirSync as mkdirSync2 } from "fs";
|
|
272
|
-
import
|
|
1028
|
+
import path20 from "path";
|
|
273
1029
|
|
|
274
1030
|
// src/detect/index.ts
|
|
275
|
-
import { existsSync as
|
|
276
|
-
import
|
|
277
|
-
import
|
|
1031
|
+
import { existsSync as existsSync14 } from "fs";
|
|
1032
|
+
import path15 from "path";
|
|
1033
|
+
import process3 from "process";
|
|
278
1034
|
|
|
279
1035
|
// src/detect/existing.ts
|
|
280
|
-
import { existsSync as
|
|
281
|
-
import
|
|
1036
|
+
import { existsSync as existsSync10, readdirSync as readdirSync5 } from "fs";
|
|
1037
|
+
import path11 from "path";
|
|
282
1038
|
var ESLINT_CONFIGS = ["eslint.config.js", "eslint.config.mjs", "eslint.config.cjs", "eslint.config.ts", ".eslintrc", ".eslintrc.js", ".eslintrc.cjs", ".eslintrc.json", ".eslintrc.yml"];
|
|
283
1039
|
var COMPOSITION_CANDIDATES = ["architecture/composition", "docs/architecture/composition", "docs/composition", "composition"];
|
|
284
1040
|
var OPENAPI_CANDIDATES = ["contracts/api/openapi.yaml", "contracts/api/openapi.yml", "contracts/openapi.yaml", "openapi.yaml", "openapi.yml", "openapi.json", "api/openapi.yaml", "docs/openapi.yaml"];
|
|
285
1041
|
function anyExists(dir, candidates) {
|
|
286
|
-
return candidates.some((candidate) =>
|
|
1042
|
+
return candidates.some((candidate) => existsSync10(path11.join(dir, candidate)));
|
|
287
1043
|
}
|
|
288
1044
|
function firstExisting(dir, candidates) {
|
|
289
|
-
return candidates.find((candidate) =>
|
|
1045
|
+
return candidates.find((candidate) => existsSync10(path11.join(dir, candidate))) ?? null;
|
|
290
1046
|
}
|
|
291
1047
|
function compositionDir(dir) {
|
|
292
1048
|
return COMPOSITION_CANDIDATES.find((candidate) => {
|
|
293
|
-
const absolute =
|
|
294
|
-
return
|
|
1049
|
+
const absolute = path11.join(dir, candidate);
|
|
1050
|
+
return existsSync10(absolute) && readdirSync5(absolute).some((file) => file.endsWith(".yaml") || file.endsWith(".yml"));
|
|
295
1051
|
}) ?? null;
|
|
296
1052
|
}
|
|
297
1053
|
function hasWorkflows(dir) {
|
|
298
|
-
const workflows =
|
|
299
|
-
return
|
|
1054
|
+
const workflows = path11.join(dir, ".github", "workflows");
|
|
1055
|
+
return existsSync10(workflows) && readdirSync5(workflows).some((file) => file.endsWith(".yml") || file.endsWith(".yaml"));
|
|
300
1056
|
}
|
|
301
1057
|
function detectExisting(dir) {
|
|
302
1058
|
return {
|
|
303
|
-
packageJson:
|
|
304
|
-
tsconfig:
|
|
1059
|
+
packageJson: existsSync10(path11.join(dir, "package.json")),
|
|
1060
|
+
tsconfig: existsSync10(path11.join(dir, "tsconfig.json")),
|
|
305
1061
|
eslintConfig: anyExists(dir, ESLINT_CONFIGS),
|
|
306
1062
|
githubWorkflows: hasWorkflows(dir),
|
|
307
|
-
claudeMd:
|
|
308
|
-
agentsMd:
|
|
309
|
-
cursorRules:
|
|
1063
|
+
claudeMd: existsSync10(path11.join(dir, "CLAUDE.md")),
|
|
1064
|
+
agentsMd: existsSync10(path11.join(dir, "AGENTS.md")),
|
|
1065
|
+
cursorRules: existsSync10(path11.join(dir, ".cursor", "rules")),
|
|
310
1066
|
openapi: firstExisting(dir, OPENAPI_CANDIDATES),
|
|
311
1067
|
compositionDir: compositionDir(dir),
|
|
312
|
-
constructJson:
|
|
1068
|
+
constructJson: existsSync10(path11.join(dir, "construct.json"))
|
|
313
1069
|
};
|
|
314
1070
|
}
|
|
315
1071
|
|
|
316
1072
|
// src/detect/layout.ts
|
|
317
|
-
import { existsSync as
|
|
318
|
-
import
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
1073
|
+
import { existsSync as existsSync12, readdirSync as readdirSync6, readFileSync as readFileSync12, statSync as statSync3 } from "fs";
|
|
1074
|
+
import path13 from "path";
|
|
1075
|
+
|
|
1076
|
+
// src/detect/workspaces.ts
|
|
1077
|
+
import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
|
|
1078
|
+
import path12 from "path";
|
|
1079
|
+
var TOP_LEVEL_PACKAGES_KEY = /^packages:(.*)$/m;
|
|
1080
|
+
var EMPTY_FLOW_SEQUENCE = /^\[\s*\]$/;
|
|
1081
|
+
var BLOCK_SEQUENCE_ENTRY = /^[ \t]+-[ \t]*\S/;
|
|
1082
|
+
function readIfPresent(file) {
|
|
1083
|
+
if (!existsSync11(file))
|
|
1084
|
+
return null;
|
|
1085
|
+
try {
|
|
1086
|
+
return readFileSync11(file, "utf8");
|
|
1087
|
+
} catch {
|
|
1088
|
+
return null;
|
|
1089
|
+
}
|
|
324
1090
|
}
|
|
325
|
-
function
|
|
326
|
-
const
|
|
327
|
-
|
|
1091
|
+
function startsBlockSequence(rest) {
|
|
1092
|
+
for (const line of rest.split("\n")) {
|
|
1093
|
+
if (line.trim() === "" || line.trimStart().startsWith("#"))
|
|
1094
|
+
continue;
|
|
1095
|
+
return BLOCK_SEQUENCE_ENTRY.test(line);
|
|
1096
|
+
}
|
|
1097
|
+
return false;
|
|
1098
|
+
}
|
|
1099
|
+
function declaresPnpmPackages(dir) {
|
|
1100
|
+
const content = readIfPresent(path12.join(dir, "pnpm-workspace.yaml"));
|
|
1101
|
+
if (content == null)
|
|
1102
|
+
return false;
|
|
1103
|
+
const match = TOP_LEVEL_PACKAGES_KEY.exec(content);
|
|
1104
|
+
if (match == null)
|
|
328
1105
|
return false;
|
|
1106
|
+
const inline = match[1].trim();
|
|
1107
|
+
if (inline.startsWith("["))
|
|
1108
|
+
return !EMPTY_FLOW_SEQUENCE.test(inline);
|
|
1109
|
+
if (inline !== "")
|
|
1110
|
+
return false;
|
|
1111
|
+
return startsBlockSequence(content.slice(match.index + match[0].length));
|
|
1112
|
+
}
|
|
1113
|
+
function declaresNpmWorkspaces(dir) {
|
|
1114
|
+
const content = readIfPresent(path12.join(dir, "package.json"));
|
|
1115
|
+
if (content == null)
|
|
1116
|
+
return false;
|
|
1117
|
+
let workspaces;
|
|
329
1118
|
try {
|
|
330
|
-
|
|
331
|
-
return parsed.workspaces != null;
|
|
1119
|
+
({ workspaces } = JSON.parse(content));
|
|
332
1120
|
} catch {
|
|
333
1121
|
return false;
|
|
334
1122
|
}
|
|
1123
|
+
if (Array.isArray(workspaces))
|
|
1124
|
+
return workspaces.length > 0;
|
|
1125
|
+
const packages = workspaces?.packages;
|
|
1126
|
+
return Array.isArray(packages) && packages.length > 0;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// src/detect/layout.ts
|
|
1130
|
+
var IGNORED_ENTRIES = /* @__PURE__ */ new Set([".git", ".DS_Store", ".gitignore", ".gitattributes", "LICENSE", "README.md", ".idea", ".vscode"]);
|
|
1131
|
+
function isEmptyDir(dir) {
|
|
1132
|
+
if (!existsSync12(dir))
|
|
1133
|
+
return true;
|
|
1134
|
+
return readdirSync6(dir).every((entry) => IGNORED_ENTRIES.has(entry));
|
|
335
1135
|
}
|
|
336
1136
|
function detectMonorepoTools(dir) {
|
|
337
1137
|
const tools = [];
|
|
338
|
-
if (
|
|
1138
|
+
if (declaresPnpmPackages(dir))
|
|
339
1139
|
tools.push("pnpm-workspace");
|
|
340
|
-
if (
|
|
1140
|
+
if (declaresNpmWorkspaces(dir))
|
|
341
1141
|
tools.push("npm-workspaces");
|
|
342
|
-
if (
|
|
1142
|
+
if (existsSync12(path13.join(dir, "turbo.json")))
|
|
343
1143
|
tools.push("turbo");
|
|
344
|
-
if (
|
|
1144
|
+
if (existsSync12(path13.join(dir, "nx.json")))
|
|
345
1145
|
tools.push("nx");
|
|
346
1146
|
return tools;
|
|
347
1147
|
}
|
|
348
1148
|
function detectWorkspaceDirs(dir) {
|
|
349
|
-
return ["apps", "packages", "libs", "services"].filter((name) =>
|
|
1149
|
+
return ["apps", "packages", "libs", "services"].filter((name) => existsSync12(path13.join(dir, name)) && statSync3(path13.join(dir, name)).isDirectory());
|
|
350
1150
|
}
|
|
351
1151
|
function packageName(dir) {
|
|
352
1152
|
try {
|
|
353
|
-
const parsed = JSON.parse(
|
|
1153
|
+
const parsed = JSON.parse(readFileSync12(path13.join(dir, "package.json"), "utf8"));
|
|
354
1154
|
return typeof parsed.name === "string" && parsed.name !== "" ? parsed.name : null;
|
|
355
1155
|
} catch {
|
|
356
1156
|
return null;
|
|
357
1157
|
}
|
|
358
1158
|
}
|
|
359
1159
|
function detectWorkspacePackages(root, workspaceDirs) {
|
|
360
|
-
return workspaceDirs.flatMap((parent) =>
|
|
1160
|
+
return workspaceDirs.flatMap((parent) => readdirSync6(path13.join(root, parent)).sort().map((entry) => `${parent}/${entry}`).filter((dir) => existsSync12(path13.join(root, dir, "package.json"))).map((dir) => ({ dir, name: packageName(path13.join(root, dir)) ?? dir.split("/").at(-1) ?? dir })));
|
|
361
1161
|
}
|
|
362
1162
|
function detectLayout(dir, monorepoTools, workspaceDirs, hasSrc) {
|
|
363
1163
|
if (isEmptyDir(dir))
|
|
364
1164
|
return "empty";
|
|
365
1165
|
if (monorepoTools.length > 0 || workspaceDirs.length > 0)
|
|
366
1166
|
return "monorepo";
|
|
367
|
-
if (hasSrc ||
|
|
1167
|
+
if (hasSrc || existsSync12(path13.join(dir, "package.json")))
|
|
368
1168
|
return "single";
|
|
369
1169
|
return "unknown";
|
|
370
1170
|
}
|
|
371
1171
|
|
|
372
1172
|
// src/detect/package-manager.ts
|
|
373
1173
|
import { execFileSync } from "child_process";
|
|
374
|
-
import { existsSync as
|
|
375
|
-
import
|
|
1174
|
+
import { existsSync as existsSync13, readFileSync as readFileSync13 } from "fs";
|
|
1175
|
+
import path14 from "path";
|
|
376
1176
|
var LOCKFILES = [
|
|
377
1177
|
["pnpm-lock.yaml", "pnpm"],
|
|
378
1178
|
["bun.lockb", "bun"],
|
|
@@ -381,11 +1181,11 @@ var LOCKFILES = [
|
|
|
381
1181
|
["package-lock.json", "npm"]
|
|
382
1182
|
];
|
|
383
1183
|
function fromPackageManagerField(dir) {
|
|
384
|
-
const manifest =
|
|
385
|
-
if (!
|
|
1184
|
+
const manifest = path14.join(dir, "package.json");
|
|
1185
|
+
if (!existsSync13(manifest))
|
|
386
1186
|
return null;
|
|
387
1187
|
try {
|
|
388
|
-
const parsed = JSON.parse(
|
|
1188
|
+
const parsed = JSON.parse(readFileSync13(manifest, "utf8"));
|
|
389
1189
|
const name = parsed.packageManager?.split("@")[0];
|
|
390
1190
|
return name === "pnpm" || name === "npm" || name === "yarn" || name === "bun" ? name : null;
|
|
391
1191
|
} catch {
|
|
@@ -397,10 +1197,10 @@ function detectPackageManager(dir) {
|
|
|
397
1197
|
if (declared != null)
|
|
398
1198
|
return declared;
|
|
399
1199
|
for (const [lockfile, manager] of LOCKFILES) {
|
|
400
|
-
if (
|
|
1200
|
+
if (existsSync13(path14.join(dir, lockfile)))
|
|
401
1201
|
return manager;
|
|
402
1202
|
}
|
|
403
|
-
return
|
|
1203
|
+
return existsSync13(path14.join(dir, "package.json")) ? "npm" : "none";
|
|
404
1204
|
}
|
|
405
1205
|
var pnpmVersionCache;
|
|
406
1206
|
function detectPnpmVersion() {
|
|
@@ -416,10 +1216,10 @@ function detectPnpmVersion() {
|
|
|
416
1216
|
|
|
417
1217
|
// src/detect/index.ts
|
|
418
1218
|
function detect(dir) {
|
|
419
|
-
const root =
|
|
1219
|
+
const root = path15.resolve(dir);
|
|
420
1220
|
const monorepoTools = detectMonorepoTools(root);
|
|
421
1221
|
const workspaceDirs = detectWorkspaceDirs(root);
|
|
422
|
-
const hasSrc =
|
|
1222
|
+
const hasSrc = existsSync14(path15.join(root, "src"));
|
|
423
1223
|
return {
|
|
424
1224
|
dir: root,
|
|
425
1225
|
packageManager: detectPackageManager(root),
|
|
@@ -429,21 +1229,21 @@ function detect(dir) {
|
|
|
429
1229
|
workspaceDirs,
|
|
430
1230
|
workspacePackages: detectWorkspacePackages(root, workspaceDirs),
|
|
431
1231
|
hasSrc,
|
|
432
|
-
nodeMajor: Number(
|
|
1232
|
+
nodeMajor: Number(process3.versions.node.split(".")[0]),
|
|
433
1233
|
existing: detectExisting(root)
|
|
434
1234
|
};
|
|
435
1235
|
}
|
|
436
1236
|
|
|
437
1237
|
// src/materialize/apply.ts
|
|
438
1238
|
import { mkdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
439
|
-
import
|
|
1239
|
+
import path16 from "path";
|
|
440
1240
|
function applyPlan(root, ops) {
|
|
441
1241
|
const written = [];
|
|
442
1242
|
for (const op of ops) {
|
|
443
1243
|
if (op.action === "skip")
|
|
444
1244
|
continue;
|
|
445
|
-
const absolute =
|
|
446
|
-
mkdirSync(
|
|
1245
|
+
const absolute = path16.join(root, op.target);
|
|
1246
|
+
mkdirSync(path16.dirname(absolute), { recursive: true });
|
|
447
1247
|
writeFileSync2(absolute, op.content);
|
|
448
1248
|
written.push(op);
|
|
449
1249
|
}
|
|
@@ -451,8 +1251,8 @@ function applyPlan(root, ops) {
|
|
|
451
1251
|
}
|
|
452
1252
|
|
|
453
1253
|
// src/materialize/plan.ts
|
|
454
|
-
import { existsSync as
|
|
455
|
-
import
|
|
1254
|
+
import { existsSync as existsSync16, readFileSync as readFileSync14 } from "fs";
|
|
1255
|
+
import path18 from "path";
|
|
456
1256
|
|
|
457
1257
|
// src/materialize/rules.ts
|
|
458
1258
|
var CLAUDE_RULES_DIR = ".claude/rules/";
|
|
@@ -614,43 +1414,43 @@ ${end}
|
|
|
614
1414
|
}
|
|
615
1415
|
|
|
616
1416
|
// src/materialize/templates.ts
|
|
617
|
-
import { existsSync as
|
|
618
|
-
import
|
|
1417
|
+
import { existsSync as existsSync15, readdirSync as readdirSync7, statSync as statSync4 } from "fs";
|
|
1418
|
+
import path17 from "path";
|
|
619
1419
|
import { fileURLToPath } from "url";
|
|
620
|
-
var HERE =
|
|
1420
|
+
var HERE = path17.dirname(fileURLToPath(import.meta.url));
|
|
621
1421
|
function templatesRoot() {
|
|
622
|
-
const candidates = [
|
|
623
|
-
const found = candidates.find((candidate) =>
|
|
1422
|
+
const candidates = [path17.resolve(HERE, "../templates"), path17.resolve(HERE, "../../templates")];
|
|
1423
|
+
const found = candidates.find((candidate) => existsSync15(candidate));
|
|
624
1424
|
if (found == null)
|
|
625
1425
|
throw new Error(`templates directory not found next to ${HERE}`);
|
|
626
1426
|
return found;
|
|
627
1427
|
}
|
|
628
1428
|
var EXISTING_SUFFIX = ".existing.eta";
|
|
629
1429
|
function toTargetPath(relative) {
|
|
630
|
-
const segments = relative.split(
|
|
1430
|
+
const segments = relative.split(path17.sep).map((segment) => segment.startsWith("_") ? `.${segment.slice(1)}` : segment);
|
|
631
1431
|
const joined = segments.join("/");
|
|
632
1432
|
if (joined.endsWith(EXISTING_SUFFIX))
|
|
633
1433
|
return { target: joined.slice(0, -EXISTING_SUFFIX.length), rendered: true, variant: "existing" };
|
|
634
1434
|
return joined.endsWith(".eta") ? { target: joined.slice(0, -".eta".length), rendered: true, variant: "default" } : { target: joined, rendered: false, variant: "default" };
|
|
635
1435
|
}
|
|
636
1436
|
function walk(root, current, files) {
|
|
637
|
-
for (const entry of
|
|
638
|
-
const absolute =
|
|
639
|
-
if (
|
|
1437
|
+
for (const entry of readdirSync7(current).sort()) {
|
|
1438
|
+
const absolute = path17.join(current, entry);
|
|
1439
|
+
if (statSync4(absolute).isDirectory())
|
|
640
1440
|
walk(root, absolute, files);
|
|
641
1441
|
else if (entry !== ".DS_Store")
|
|
642
|
-
files.push(
|
|
1442
|
+
files.push(path17.relative(root, absolute));
|
|
643
1443
|
}
|
|
644
1444
|
}
|
|
645
1445
|
function listTemplateFiles(group) {
|
|
646
|
-
const root =
|
|
647
|
-
if (!
|
|
1446
|
+
const root = path17.join(templatesRoot(), group);
|
|
1447
|
+
if (!existsSync15(root))
|
|
648
1448
|
throw new Error(`template group "${group}" does not exist`);
|
|
649
1449
|
const files = [];
|
|
650
1450
|
walk(root, root, files);
|
|
651
1451
|
return files.map((relative) => {
|
|
652
1452
|
const { target, rendered, variant } = toTargetPath(relative);
|
|
653
|
-
return { group, source:
|
|
1453
|
+
return { group, source: path17.join(root, relative), target, rendered, variant };
|
|
654
1454
|
});
|
|
655
1455
|
}
|
|
656
1456
|
var BLOCK = /^[ \t]*\{\{#(if|unless) (\w+)\}\}[ \t]*\n([\s\S]*?)^[ \t]*\{\{\/\1\}\}[ \t]*\n/gm;
|
|
@@ -677,7 +1477,7 @@ function mountTarget(mount, target) {
|
|
|
677
1477
|
return mount.into == null || mount.into === "." ? target : `${mount.into.replace(/\/$/, "")}/${target}`;
|
|
678
1478
|
}
|
|
679
1479
|
function readTemplate(source, rendered, vars) {
|
|
680
|
-
const raw =
|
|
1480
|
+
const raw = readFileSync14(source, "utf8");
|
|
681
1481
|
return rendered ? render(raw, vars) : raw;
|
|
682
1482
|
}
|
|
683
1483
|
var SORTED_SECTIONS = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
|
|
@@ -735,12 +1535,12 @@ function layerJson(earlier, later) {
|
|
|
735
1535
|
var NOT_ADDED_TO_EXISTING_MANIFEST = ["version"];
|
|
736
1536
|
function planOne(root, target, content, conflicts, existingVariant) {
|
|
737
1537
|
const strategy = strategyFor(target);
|
|
738
|
-
const absolute =
|
|
739
|
-
const exists =
|
|
1538
|
+
const absolute = path18.join(root, target);
|
|
1539
|
+
const exists = existsSync16(absolute);
|
|
740
1540
|
if (!exists)
|
|
741
1541
|
return { target, strategy, action: "create", content: strategy === "append-block" ? appendBlock("", content, target) : content };
|
|
742
1542
|
if (strategy === "merge-json") {
|
|
743
|
-
const existing = JSON.parse(
|
|
1543
|
+
const existing = JSON.parse(readFileSync14(absolute, "utf8"));
|
|
744
1544
|
const incoming = JSON.parse(content);
|
|
745
1545
|
for (const key of NOT_ADDED_TO_EXISTING_MANIFEST)
|
|
746
1546
|
delete incoming[key];
|
|
@@ -751,7 +1551,7 @@ function planOne(root, target, content, conflicts, existingVariant) {
|
|
|
751
1551
|
` };
|
|
752
1552
|
}
|
|
753
1553
|
if (strategy === "append-block") {
|
|
754
|
-
const existing =
|
|
1554
|
+
const existing = readFileSync14(absolute, "utf8");
|
|
755
1555
|
return { target, strategy, action: "append", content: appendBlock(existing, existingVariant ?? content, target) };
|
|
756
1556
|
}
|
|
757
1557
|
return { target, strategy, action: "skip", content, note: "exists, review manually" };
|
|
@@ -825,7 +1625,6 @@ var PRESETS = {
|
|
|
825
1625
|
HTTP_CONTRACT,
|
|
826
1626
|
{ group: EXPRESS_APP, onlyWhenEmpty: true },
|
|
827
1627
|
{ group: EXPRESS_REPO, onlyWhenEmpty: true },
|
|
828
|
-
{ group: "presets/node-backend/sample", onlyWhenEmpty: true },
|
|
829
1628
|
"presets/node-backend/baseline"
|
|
830
1629
|
],
|
|
831
1630
|
contracts: true,
|
|
@@ -987,14 +1786,14 @@ function createClackPrompter(lore, streams = {}) {
|
|
|
987
1786
|
}
|
|
988
1787
|
|
|
989
1788
|
// src/version.ts
|
|
990
|
-
import { readFileSync as
|
|
991
|
-
import
|
|
1789
|
+
import { readFileSync as readFileSync15 } from "fs";
|
|
1790
|
+
import path19 from "path";
|
|
992
1791
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
993
|
-
var HERE2 =
|
|
1792
|
+
var HERE2 = path19.dirname(fileURLToPath2(import.meta.url));
|
|
994
1793
|
function readVersion() {
|
|
995
1794
|
for (const candidate of ["../package.json", "../../package.json"]) {
|
|
996
1795
|
try {
|
|
997
|
-
const parsed = JSON.parse(
|
|
1796
|
+
const parsed = JSON.parse(readFileSync15(path19.resolve(HERE2, candidate), "utf8"));
|
|
998
1797
|
if (parsed.name === "mikoshi-construct" && parsed.version != null)
|
|
999
1798
|
return parsed.version;
|
|
1000
1799
|
} catch {
|
|
@@ -1094,7 +1893,7 @@ async function askChoices(ui2, options, report, root, prompter) {
|
|
|
1094
1893
|
return { presetId, ai, projectName, review };
|
|
1095
1894
|
}
|
|
1096
1895
|
async function runInit(ui2, options, prompter) {
|
|
1097
|
-
const root =
|
|
1896
|
+
const root = path20.resolve(options.dir);
|
|
1098
1897
|
mkdirSync2(root, { recursive: true });
|
|
1099
1898
|
if (!options.yes && prompter == null) {
|
|
1100
1899
|
ui2.glitch(ui2.lore.needsTerminal);
|
|
@@ -1182,7 +1981,7 @@ async function runInit(ui2, options, prompter) {
|
|
|
1182
1981
|
}
|
|
1183
1982
|
|
|
1184
1983
|
// src/ui/console.ts
|
|
1185
|
-
import
|
|
1984
|
+
import process4 from "process";
|
|
1186
1985
|
|
|
1187
1986
|
// src/ui/lore.ts
|
|
1188
1987
|
var BANNER = String.raw`
|
|
@@ -1220,12 +2019,34 @@ var LORE = {
|
|
|
1220
2019
|
flatlined: "FLATLINED",
|
|
1221
2020
|
stable: "CONSTRUCT STABLE",
|
|
1222
2021
|
discoveryIncomplete: "Discovery incomplete.",
|
|
2022
|
+
provenance: "AUTHORSHIP TRACE",
|
|
2023
|
+
stillConstructAuthored: (count) => `Still the construct's own words: ${count} marker${count === 1 ? "" : "s"} nobody has stood behind yet.`,
|
|
2024
|
+
enforcement: "ENFORCEMENT TRACE",
|
|
2025
|
+
typecheckCaveat: "Typecheck cannot carry this stack alone.",
|
|
2026
|
+
weakestLink: (id, level) => `WEAKEST LINK: ${id} at ${level}`,
|
|
2027
|
+
levelLift: {
|
|
2028
|
+
L0: "wire it to a hook or a workflow step and it climbs",
|
|
2029
|
+
L1: "only a reviewer stands behind it; a hook or a workflow step raises it",
|
|
2030
|
+
L2: "a hook is bypassable with --no-verify; running it in CI too raises it",
|
|
2031
|
+
L3: "L3 is the ceiling doctor can read: branch protection is what makes it blocking, and that lives in the API",
|
|
2032
|
+
L4: "nothing above this"
|
|
2033
|
+
},
|
|
2034
|
+
weakestLinkNone: "WEAKEST LINK: nothing is claimed",
|
|
1223
2035
|
wireHarness: "Existing configs were kept, so the harness is not wired in yet. /construct-discover does this first; by hand:",
|
|
1224
2036
|
wireHarnessSteps: [
|
|
1225
2037
|
"eslint: ignore scripts/construct/*.workflow.mjs (the ladder script uses top-level return)",
|
|
1226
2038
|
"tsconfig: include scripts/**/*.ts; vitest: include scripts/tests/**/*.test.ts",
|
|
1227
2039
|
"package.json: make the quality script run composition:check (and contracts:check when there is a contract)"
|
|
1228
|
-
]
|
|
2040
|
+
],
|
|
2041
|
+
costUnsupported: (runtime) => `No usage feed: the ${runtime} runtime does not expose per-run token usage.`,
|
|
2042
|
+
costEmpty: "No /implement runs recorded here yet.",
|
|
2043
|
+
costKeyMismatch: (key) => `Runs for this repository were recorded under another path. Looked up: ${key}`,
|
|
2044
|
+
costKeyUnknown: (key) => `Runs may be recorded under another path \u2014 the evidence is not conclusive. Looked up: ${key}`,
|
|
2045
|
+
ledgerCounts: (runs, agents, failures, tokens2) => `Ledger, kept by hand and trusted by nobody: ${runs} runs, ${agents} agents, ${failures} unfinished, ${tokens2} tokens.`,
|
|
2046
|
+
ledgerMalformed: (count) => `${count} ledger line${count === 1 ? "" : "s"} could not be read as a run record.`,
|
|
2047
|
+
ledgerDrift: (entriesWithoutSession, sessionsWithoutEntry, unjoinable) => `Ledger against the traces it claims: ${entriesWithoutSession} entries with no session, ${sessionsWithoutEntry} sessions with no entry, ${unjoinable} entries with no run id.`,
|
|
2048
|
+
ledgerEntryWithoutSession: "logged as a run, no session behind it",
|
|
2049
|
+
ledgerSessionWithoutEntry: "ran, never logged"
|
|
1229
2050
|
};
|
|
1230
2051
|
var PLAIN_LORE = {
|
|
1231
2052
|
subtitle: (version) => `mikoshi-construct v${version}`,
|
|
@@ -1255,17 +2076,39 @@ var PLAIN_LORE = {
|
|
|
1255
2076
|
flatlined: "ERROR",
|
|
1256
2077
|
stable: "OK",
|
|
1257
2078
|
discoveryIncomplete: "Discovery incomplete.",
|
|
2079
|
+
provenance: "Discovery provenance",
|
|
2080
|
+
stillConstructAuthored: (count) => `Unchanged since discovery wrote them: ${count} marker${count === 1 ? "" : "s"} nobody has stood behind yet.`,
|
|
2081
|
+
enforcement: "Enforcement",
|
|
2082
|
+
typecheckCaveat: "Typecheck cannot carry this stack alone.",
|
|
2083
|
+
weakestLink: (id, level) => `Weakest link: ${id} at ${level}`,
|
|
2084
|
+
levelLift: {
|
|
2085
|
+
L0: "wire it to a hook or a workflow step and it climbs",
|
|
2086
|
+
L1: "only a reviewer stands behind it; a hook or a workflow step raises it",
|
|
2087
|
+
L2: "a hook is bypassable with --no-verify; running it in CI too raises it",
|
|
2088
|
+
L3: "L3 is the ceiling doctor can read: branch protection is what makes it blocking, and that lives in the API",
|
|
2089
|
+
L4: "nothing above this"
|
|
2090
|
+
},
|
|
2091
|
+
weakestLinkNone: "Weakest link: nothing is claimed",
|
|
1258
2092
|
wireHarness: "Existing configs were kept, so the harness is not wired in yet. /construct-discover does this first; by hand:",
|
|
1259
2093
|
wireHarnessSteps: [
|
|
1260
2094
|
"eslint: ignore scripts/construct/*.workflow.mjs (the ladder script uses top-level return)",
|
|
1261
2095
|
"tsconfig: include scripts/**/*.ts; vitest: include scripts/tests/**/*.test.ts",
|
|
1262
2096
|
"package.json: make the quality script run composition:check (and contracts:check when there is a contract)"
|
|
1263
|
-
]
|
|
2097
|
+
],
|
|
2098
|
+
costUnsupported: (runtime) => `The ${runtime} runtime does not expose per-run token usage.`,
|
|
2099
|
+
costEmpty: "No /implement runs recorded here yet.",
|
|
2100
|
+
costKeyMismatch: (key) => `Runs for this repository were recorded under another path. Looked up: ${key}`,
|
|
2101
|
+
costKeyUnknown: (key) => `Runs may be recorded under another path \u2014 the evidence is not conclusive. Looked up: ${key}`,
|
|
2102
|
+
ledgerCounts: (runs, agents, failures, tokens2) => `Ledger (a skill step writes it, nothing enforces it): ${runs} runs, ${agents} agents, ${failures} unfinished, ${tokens2} tokens.`,
|
|
2103
|
+
ledgerMalformed: (count) => `${count} ledger line${count === 1 ? "" : "s"} could not be read as a run record.`,
|
|
2104
|
+
ledgerDrift: (entriesWithoutSession, sessionsWithoutEntry, unjoinable) => `Ledger against the runtime: ${entriesWithoutSession} entries with no session, ${sessionsWithoutEntry} sessions with no entry, ${unjoinable} entries with no run id.`,
|
|
2105
|
+
ledgerEntryWithoutSession: "logged as a run, no session behind it",
|
|
2106
|
+
ledgerSessionWithoutEntry: "ran, never logged"
|
|
1264
2107
|
};
|
|
1265
2108
|
|
|
1266
2109
|
// src/ui/console.ts
|
|
1267
2110
|
var stdoutWriter = (text2) => {
|
|
1268
|
-
|
|
2111
|
+
process4.stdout.write(text2);
|
|
1269
2112
|
};
|
|
1270
2113
|
function createUi(theme, write = stdoutWriter) {
|
|
1271
2114
|
const plain = theme.name === "plain";
|
|
@@ -1324,21 +2167,21 @@ function createUi(theme, write = stdoutWriter) {
|
|
|
1324
2167
|
}
|
|
1325
2168
|
|
|
1326
2169
|
// src/ui/theme.ts
|
|
1327
|
-
import
|
|
2170
|
+
import process5 from "process";
|
|
1328
2171
|
import pc from "picocolors";
|
|
1329
2172
|
function rgb(r, g, b) {
|
|
1330
2173
|
return (text2) => `\x1B[38;2;${r};${g};${b}m${text2}\x1B[39m`;
|
|
1331
2174
|
}
|
|
1332
2175
|
var identity = (text2) => text2;
|
|
1333
2176
|
function supportsColor() {
|
|
1334
|
-
if (
|
|
2177
|
+
if (process5.env.NO_COLOR != null && process5.env.NO_COLOR !== "")
|
|
1335
2178
|
return false;
|
|
1336
|
-
if (
|
|
2179
|
+
if (process5.env.FORCE_COLOR != null && process5.env.FORCE_COLOR !== "0")
|
|
1337
2180
|
return true;
|
|
1338
2181
|
return pc.isColorSupported;
|
|
1339
2182
|
}
|
|
1340
2183
|
function truecolor() {
|
|
1341
|
-
const term =
|
|
2184
|
+
const term = process5.env.COLORTERM ?? "";
|
|
1342
2185
|
return term === "truecolor" || term === "24bit";
|
|
1343
2186
|
}
|
|
1344
2187
|
var PLAIN = {
|
|
@@ -1407,12 +2250,12 @@ var init = defineCommand({
|
|
|
1407
2250
|
const console = ui(args);
|
|
1408
2251
|
console.banner(VERSION, args.johnny);
|
|
1409
2252
|
try {
|
|
1410
|
-
const prompter = isTTY(
|
|
2253
|
+
const prompter = isTTY(process6.stdout) && process6.stdin.isTTY === true ? createClackPrompter(console.lore) : void 0;
|
|
1411
2254
|
const result = await runInit(console, { dir: args.dir, preset: args.preset, ai: args.ai, name: args.name, review: args.review, reviewModel: args.reviewModel, yes: args.yes, dryRun: args.dryRun }, prompter);
|
|
1412
|
-
|
|
2255
|
+
process6.exitCode = result.status === "aborted" ? 1 : 0;
|
|
1413
2256
|
} catch (error) {
|
|
1414
2257
|
console.flatline(error instanceof Error ? error.message : String(error));
|
|
1415
|
-
|
|
2258
|
+
process6.exitCode = 1;
|
|
1416
2259
|
}
|
|
1417
2260
|
}
|
|
1418
2261
|
});
|
|
@@ -1425,7 +2268,7 @@ var soulkill = defineCommand({
|
|
|
1425
2268
|
run({ args }) {
|
|
1426
2269
|
const report = detect(args.dir);
|
|
1427
2270
|
if (args.json) {
|
|
1428
|
-
|
|
2271
|
+
process6.stdout.write(`${JSON.stringify(report, null, 2)}
|
|
1429
2272
|
`);
|
|
1430
2273
|
return;
|
|
1431
2274
|
}
|
|
@@ -1445,13 +2288,13 @@ var doctor = defineCommand({
|
|
|
1445
2288
|
run({ args }) {
|
|
1446
2289
|
const result = runDoctor(args.dir);
|
|
1447
2290
|
if (args.json) {
|
|
1448
|
-
|
|
2291
|
+
process6.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
1449
2292
|
`);
|
|
1450
|
-
|
|
2293
|
+
process6.exitCode = result?.ok === true ? 0 : 1;
|
|
1451
2294
|
return;
|
|
1452
2295
|
}
|
|
1453
2296
|
const console = ui(args);
|
|
1454
|
-
|
|
2297
|
+
process6.exitCode = printDoctor(console, result);
|
|
1455
2298
|
}
|
|
1456
2299
|
});
|
|
1457
2300
|
var cost = defineCommand({
|
|
@@ -1462,16 +2305,14 @@ var cost = defineCommand({
|
|
|
1462
2305
|
json: { type: "boolean", description: "Machine-readable report", default: false }
|
|
1463
2306
|
},
|
|
1464
2307
|
run({ args }) {
|
|
1465
|
-
const
|
|
2308
|
+
const report = costReport(path21.resolve(args.dir));
|
|
1466
2309
|
if (args.json) {
|
|
1467
|
-
|
|
1468
|
-
process4.stdout.write(`${JSON.stringify(selected, null, 2)}
|
|
2310
|
+
process6.stdout.write(`${JSON.stringify(costJson(report, args.last), null, 2)}
|
|
1469
2311
|
`);
|
|
1470
|
-
|
|
2312
|
+
process6.exitCode = COST_EXIT[report.status];
|
|
1471
2313
|
return;
|
|
1472
2314
|
}
|
|
1473
|
-
|
|
1474
|
-
process4.exitCode = printCost(console, runs, args.last);
|
|
2315
|
+
process6.exitCode = printCost(ui(args), report, args.last);
|
|
1475
2316
|
}
|
|
1476
2317
|
});
|
|
1477
2318
|
var main = defineCommand({
|