mikoshi-construct 0.1.3 → 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 +981 -184
- 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 };
|
|
178
623
|
}
|
|
179
624
|
|
|
180
|
-
// src/commands/doctor.ts
|
|
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`);
|
|
672
|
+
}
|
|
673
|
+
|
|
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;
|
|
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);
|
|
195
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,79 +981,109 @@ 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
|
|
1073
|
+
import { existsSync as existsSync12, readdirSync as readdirSync6, readFileSync as readFileSync12, statSync as statSync3 } from "fs";
|
|
1074
|
+
import path13 from "path";
|
|
319
1075
|
|
|
320
1076
|
// src/detect/workspaces.ts
|
|
321
|
-
import { existsSync as
|
|
322
|
-
import
|
|
1077
|
+
import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
|
|
1078
|
+
import path12 from "path";
|
|
323
1079
|
var TOP_LEVEL_PACKAGES_KEY = /^packages:(.*)$/m;
|
|
324
1080
|
var EMPTY_FLOW_SEQUENCE = /^\[\s*\]$/;
|
|
325
1081
|
var BLOCK_SEQUENCE_ENTRY = /^[ \t]+-[ \t]*\S/;
|
|
326
1082
|
function readIfPresent(file) {
|
|
327
|
-
if (!
|
|
1083
|
+
if (!existsSync11(file))
|
|
328
1084
|
return null;
|
|
329
1085
|
try {
|
|
330
|
-
return
|
|
1086
|
+
return readFileSync11(file, "utf8");
|
|
331
1087
|
} catch {
|
|
332
1088
|
return null;
|
|
333
1089
|
}
|
|
@@ -341,7 +1097,7 @@ function startsBlockSequence(rest) {
|
|
|
341
1097
|
return false;
|
|
342
1098
|
}
|
|
343
1099
|
function declaresPnpmPackages(dir) {
|
|
344
|
-
const content = readIfPresent(
|
|
1100
|
+
const content = readIfPresent(path12.join(dir, "pnpm-workspace.yaml"));
|
|
345
1101
|
if (content == null)
|
|
346
1102
|
return false;
|
|
347
1103
|
const match = TOP_LEVEL_PACKAGES_KEY.exec(content);
|
|
@@ -355,7 +1111,7 @@ function declaresPnpmPackages(dir) {
|
|
|
355
1111
|
return startsBlockSequence(content.slice(match.index + match[0].length));
|
|
356
1112
|
}
|
|
357
1113
|
function declaresNpmWorkspaces(dir) {
|
|
358
|
-
const content = readIfPresent(
|
|
1114
|
+
const content = readIfPresent(path12.join(dir, "package.json"));
|
|
359
1115
|
if (content == null)
|
|
360
1116
|
return false;
|
|
361
1117
|
let workspaces;
|
|
@@ -373,9 +1129,9 @@ function declaresNpmWorkspaces(dir) {
|
|
|
373
1129
|
// src/detect/layout.ts
|
|
374
1130
|
var IGNORED_ENTRIES = /* @__PURE__ */ new Set([".git", ".DS_Store", ".gitignore", ".gitattributes", "LICENSE", "README.md", ".idea", ".vscode"]);
|
|
375
1131
|
function isEmptyDir(dir) {
|
|
376
|
-
if (!
|
|
1132
|
+
if (!existsSync12(dir))
|
|
377
1133
|
return true;
|
|
378
|
-
return
|
|
1134
|
+
return readdirSync6(dir).every((entry) => IGNORED_ENTRIES.has(entry));
|
|
379
1135
|
}
|
|
380
1136
|
function detectMonorepoTools(dir) {
|
|
381
1137
|
const tools = [];
|
|
@@ -383,40 +1139,40 @@ function detectMonorepoTools(dir) {
|
|
|
383
1139
|
tools.push("pnpm-workspace");
|
|
384
1140
|
if (declaresNpmWorkspaces(dir))
|
|
385
1141
|
tools.push("npm-workspaces");
|
|
386
|
-
if (
|
|
1142
|
+
if (existsSync12(path13.join(dir, "turbo.json")))
|
|
387
1143
|
tools.push("turbo");
|
|
388
|
-
if (
|
|
1144
|
+
if (existsSync12(path13.join(dir, "nx.json")))
|
|
389
1145
|
tools.push("nx");
|
|
390
1146
|
return tools;
|
|
391
1147
|
}
|
|
392
1148
|
function detectWorkspaceDirs(dir) {
|
|
393
|
-
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());
|
|
394
1150
|
}
|
|
395
1151
|
function packageName(dir) {
|
|
396
1152
|
try {
|
|
397
|
-
const parsed = JSON.parse(
|
|
1153
|
+
const parsed = JSON.parse(readFileSync12(path13.join(dir, "package.json"), "utf8"));
|
|
398
1154
|
return typeof parsed.name === "string" && parsed.name !== "" ? parsed.name : null;
|
|
399
1155
|
} catch {
|
|
400
1156
|
return null;
|
|
401
1157
|
}
|
|
402
1158
|
}
|
|
403
1159
|
function detectWorkspacePackages(root, workspaceDirs) {
|
|
404
|
-
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 })));
|
|
405
1161
|
}
|
|
406
1162
|
function detectLayout(dir, monorepoTools, workspaceDirs, hasSrc) {
|
|
407
1163
|
if (isEmptyDir(dir))
|
|
408
1164
|
return "empty";
|
|
409
1165
|
if (monorepoTools.length > 0 || workspaceDirs.length > 0)
|
|
410
1166
|
return "monorepo";
|
|
411
|
-
if (hasSrc ||
|
|
1167
|
+
if (hasSrc || existsSync12(path13.join(dir, "package.json")))
|
|
412
1168
|
return "single";
|
|
413
1169
|
return "unknown";
|
|
414
1170
|
}
|
|
415
1171
|
|
|
416
1172
|
// src/detect/package-manager.ts
|
|
417
1173
|
import { execFileSync } from "child_process";
|
|
418
|
-
import { existsSync as
|
|
419
|
-
import
|
|
1174
|
+
import { existsSync as existsSync13, readFileSync as readFileSync13 } from "fs";
|
|
1175
|
+
import path14 from "path";
|
|
420
1176
|
var LOCKFILES = [
|
|
421
1177
|
["pnpm-lock.yaml", "pnpm"],
|
|
422
1178
|
["bun.lockb", "bun"],
|
|
@@ -425,11 +1181,11 @@ var LOCKFILES = [
|
|
|
425
1181
|
["package-lock.json", "npm"]
|
|
426
1182
|
];
|
|
427
1183
|
function fromPackageManagerField(dir) {
|
|
428
|
-
const manifest =
|
|
429
|
-
if (!
|
|
1184
|
+
const manifest = path14.join(dir, "package.json");
|
|
1185
|
+
if (!existsSync13(manifest))
|
|
430
1186
|
return null;
|
|
431
1187
|
try {
|
|
432
|
-
const parsed = JSON.parse(
|
|
1188
|
+
const parsed = JSON.parse(readFileSync13(manifest, "utf8"));
|
|
433
1189
|
const name = parsed.packageManager?.split("@")[0];
|
|
434
1190
|
return name === "pnpm" || name === "npm" || name === "yarn" || name === "bun" ? name : null;
|
|
435
1191
|
} catch {
|
|
@@ -441,10 +1197,10 @@ function detectPackageManager(dir) {
|
|
|
441
1197
|
if (declared != null)
|
|
442
1198
|
return declared;
|
|
443
1199
|
for (const [lockfile, manager] of LOCKFILES) {
|
|
444
|
-
if (
|
|
1200
|
+
if (existsSync13(path14.join(dir, lockfile)))
|
|
445
1201
|
return manager;
|
|
446
1202
|
}
|
|
447
|
-
return
|
|
1203
|
+
return existsSync13(path14.join(dir, "package.json")) ? "npm" : "none";
|
|
448
1204
|
}
|
|
449
1205
|
var pnpmVersionCache;
|
|
450
1206
|
function detectPnpmVersion() {
|
|
@@ -460,10 +1216,10 @@ function detectPnpmVersion() {
|
|
|
460
1216
|
|
|
461
1217
|
// src/detect/index.ts
|
|
462
1218
|
function detect(dir) {
|
|
463
|
-
const root =
|
|
1219
|
+
const root = path15.resolve(dir);
|
|
464
1220
|
const monorepoTools = detectMonorepoTools(root);
|
|
465
1221
|
const workspaceDirs = detectWorkspaceDirs(root);
|
|
466
|
-
const hasSrc =
|
|
1222
|
+
const hasSrc = existsSync14(path15.join(root, "src"));
|
|
467
1223
|
return {
|
|
468
1224
|
dir: root,
|
|
469
1225
|
packageManager: detectPackageManager(root),
|
|
@@ -473,21 +1229,21 @@ function detect(dir) {
|
|
|
473
1229
|
workspaceDirs,
|
|
474
1230
|
workspacePackages: detectWorkspacePackages(root, workspaceDirs),
|
|
475
1231
|
hasSrc,
|
|
476
|
-
nodeMajor: Number(
|
|
1232
|
+
nodeMajor: Number(process3.versions.node.split(".")[0]),
|
|
477
1233
|
existing: detectExisting(root)
|
|
478
1234
|
};
|
|
479
1235
|
}
|
|
480
1236
|
|
|
481
1237
|
// src/materialize/apply.ts
|
|
482
1238
|
import { mkdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
483
|
-
import
|
|
1239
|
+
import path16 from "path";
|
|
484
1240
|
function applyPlan(root, ops) {
|
|
485
1241
|
const written = [];
|
|
486
1242
|
for (const op of ops) {
|
|
487
1243
|
if (op.action === "skip")
|
|
488
1244
|
continue;
|
|
489
|
-
const absolute =
|
|
490
|
-
mkdirSync(
|
|
1245
|
+
const absolute = path16.join(root, op.target);
|
|
1246
|
+
mkdirSync(path16.dirname(absolute), { recursive: true });
|
|
491
1247
|
writeFileSync2(absolute, op.content);
|
|
492
1248
|
written.push(op);
|
|
493
1249
|
}
|
|
@@ -495,8 +1251,8 @@ function applyPlan(root, ops) {
|
|
|
495
1251
|
}
|
|
496
1252
|
|
|
497
1253
|
// src/materialize/plan.ts
|
|
498
|
-
import { existsSync as
|
|
499
|
-
import
|
|
1254
|
+
import { existsSync as existsSync16, readFileSync as readFileSync14 } from "fs";
|
|
1255
|
+
import path18 from "path";
|
|
500
1256
|
|
|
501
1257
|
// src/materialize/rules.ts
|
|
502
1258
|
var CLAUDE_RULES_DIR = ".claude/rules/";
|
|
@@ -658,43 +1414,43 @@ ${end}
|
|
|
658
1414
|
}
|
|
659
1415
|
|
|
660
1416
|
// src/materialize/templates.ts
|
|
661
|
-
import { existsSync as
|
|
662
|
-
import
|
|
1417
|
+
import { existsSync as existsSync15, readdirSync as readdirSync7, statSync as statSync4 } from "fs";
|
|
1418
|
+
import path17 from "path";
|
|
663
1419
|
import { fileURLToPath } from "url";
|
|
664
|
-
var HERE =
|
|
1420
|
+
var HERE = path17.dirname(fileURLToPath(import.meta.url));
|
|
665
1421
|
function templatesRoot() {
|
|
666
|
-
const candidates = [
|
|
667
|
-
const found = candidates.find((candidate) =>
|
|
1422
|
+
const candidates = [path17.resolve(HERE, "../templates"), path17.resolve(HERE, "../../templates")];
|
|
1423
|
+
const found = candidates.find((candidate) => existsSync15(candidate));
|
|
668
1424
|
if (found == null)
|
|
669
1425
|
throw new Error(`templates directory not found next to ${HERE}`);
|
|
670
1426
|
return found;
|
|
671
1427
|
}
|
|
672
1428
|
var EXISTING_SUFFIX = ".existing.eta";
|
|
673
1429
|
function toTargetPath(relative) {
|
|
674
|
-
const segments = relative.split(
|
|
1430
|
+
const segments = relative.split(path17.sep).map((segment) => segment.startsWith("_") ? `.${segment.slice(1)}` : segment);
|
|
675
1431
|
const joined = segments.join("/");
|
|
676
1432
|
if (joined.endsWith(EXISTING_SUFFIX))
|
|
677
1433
|
return { target: joined.slice(0, -EXISTING_SUFFIX.length), rendered: true, variant: "existing" };
|
|
678
1434
|
return joined.endsWith(".eta") ? { target: joined.slice(0, -".eta".length), rendered: true, variant: "default" } : { target: joined, rendered: false, variant: "default" };
|
|
679
1435
|
}
|
|
680
1436
|
function walk(root, current, files) {
|
|
681
|
-
for (const entry of
|
|
682
|
-
const absolute =
|
|
683
|
-
if (
|
|
1437
|
+
for (const entry of readdirSync7(current).sort()) {
|
|
1438
|
+
const absolute = path17.join(current, entry);
|
|
1439
|
+
if (statSync4(absolute).isDirectory())
|
|
684
1440
|
walk(root, absolute, files);
|
|
685
1441
|
else if (entry !== ".DS_Store")
|
|
686
|
-
files.push(
|
|
1442
|
+
files.push(path17.relative(root, absolute));
|
|
687
1443
|
}
|
|
688
1444
|
}
|
|
689
1445
|
function listTemplateFiles(group) {
|
|
690
|
-
const root =
|
|
691
|
-
if (!
|
|
1446
|
+
const root = path17.join(templatesRoot(), group);
|
|
1447
|
+
if (!existsSync15(root))
|
|
692
1448
|
throw new Error(`template group "${group}" does not exist`);
|
|
693
1449
|
const files = [];
|
|
694
1450
|
walk(root, root, files);
|
|
695
1451
|
return files.map((relative) => {
|
|
696
1452
|
const { target, rendered, variant } = toTargetPath(relative);
|
|
697
|
-
return { group, source:
|
|
1453
|
+
return { group, source: path17.join(root, relative), target, rendered, variant };
|
|
698
1454
|
});
|
|
699
1455
|
}
|
|
700
1456
|
var BLOCK = /^[ \t]*\{\{#(if|unless) (\w+)\}\}[ \t]*\n([\s\S]*?)^[ \t]*\{\{\/\1\}\}[ \t]*\n/gm;
|
|
@@ -721,7 +1477,7 @@ function mountTarget(mount, target) {
|
|
|
721
1477
|
return mount.into == null || mount.into === "." ? target : `${mount.into.replace(/\/$/, "")}/${target}`;
|
|
722
1478
|
}
|
|
723
1479
|
function readTemplate(source, rendered, vars) {
|
|
724
|
-
const raw =
|
|
1480
|
+
const raw = readFileSync14(source, "utf8");
|
|
725
1481
|
return rendered ? render(raw, vars) : raw;
|
|
726
1482
|
}
|
|
727
1483
|
var SORTED_SECTIONS = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
|
|
@@ -779,12 +1535,12 @@ function layerJson(earlier, later) {
|
|
|
779
1535
|
var NOT_ADDED_TO_EXISTING_MANIFEST = ["version"];
|
|
780
1536
|
function planOne(root, target, content, conflicts, existingVariant) {
|
|
781
1537
|
const strategy = strategyFor(target);
|
|
782
|
-
const absolute =
|
|
783
|
-
const exists =
|
|
1538
|
+
const absolute = path18.join(root, target);
|
|
1539
|
+
const exists = existsSync16(absolute);
|
|
784
1540
|
if (!exists)
|
|
785
1541
|
return { target, strategy, action: "create", content: strategy === "append-block" ? appendBlock("", content, target) : content };
|
|
786
1542
|
if (strategy === "merge-json") {
|
|
787
|
-
const existing = JSON.parse(
|
|
1543
|
+
const existing = JSON.parse(readFileSync14(absolute, "utf8"));
|
|
788
1544
|
const incoming = JSON.parse(content);
|
|
789
1545
|
for (const key of NOT_ADDED_TO_EXISTING_MANIFEST)
|
|
790
1546
|
delete incoming[key];
|
|
@@ -795,7 +1551,7 @@ function planOne(root, target, content, conflicts, existingVariant) {
|
|
|
795
1551
|
` };
|
|
796
1552
|
}
|
|
797
1553
|
if (strategy === "append-block") {
|
|
798
|
-
const existing =
|
|
1554
|
+
const existing = readFileSync14(absolute, "utf8");
|
|
799
1555
|
return { target, strategy, action: "append", content: appendBlock(existing, existingVariant ?? content, target) };
|
|
800
1556
|
}
|
|
801
1557
|
return { target, strategy, action: "skip", content, note: "exists, review manually" };
|
|
@@ -869,7 +1625,6 @@ var PRESETS = {
|
|
|
869
1625
|
HTTP_CONTRACT,
|
|
870
1626
|
{ group: EXPRESS_APP, onlyWhenEmpty: true },
|
|
871
1627
|
{ group: EXPRESS_REPO, onlyWhenEmpty: true },
|
|
872
|
-
{ group: "presets/node-backend/sample", onlyWhenEmpty: true },
|
|
873
1628
|
"presets/node-backend/baseline"
|
|
874
1629
|
],
|
|
875
1630
|
contracts: true,
|
|
@@ -1031,14 +1786,14 @@ function createClackPrompter(lore, streams = {}) {
|
|
|
1031
1786
|
}
|
|
1032
1787
|
|
|
1033
1788
|
// src/version.ts
|
|
1034
|
-
import { readFileSync as
|
|
1035
|
-
import
|
|
1789
|
+
import { readFileSync as readFileSync15 } from "fs";
|
|
1790
|
+
import path19 from "path";
|
|
1036
1791
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1037
|
-
var HERE2 =
|
|
1792
|
+
var HERE2 = path19.dirname(fileURLToPath2(import.meta.url));
|
|
1038
1793
|
function readVersion() {
|
|
1039
1794
|
for (const candidate of ["../package.json", "../../package.json"]) {
|
|
1040
1795
|
try {
|
|
1041
|
-
const parsed = JSON.parse(
|
|
1796
|
+
const parsed = JSON.parse(readFileSync15(path19.resolve(HERE2, candidate), "utf8"));
|
|
1042
1797
|
if (parsed.name === "mikoshi-construct" && parsed.version != null)
|
|
1043
1798
|
return parsed.version;
|
|
1044
1799
|
} catch {
|
|
@@ -1138,7 +1893,7 @@ async function askChoices(ui2, options, report, root, prompter) {
|
|
|
1138
1893
|
return { presetId, ai, projectName, review };
|
|
1139
1894
|
}
|
|
1140
1895
|
async function runInit(ui2, options, prompter) {
|
|
1141
|
-
const root =
|
|
1896
|
+
const root = path20.resolve(options.dir);
|
|
1142
1897
|
mkdirSync2(root, { recursive: true });
|
|
1143
1898
|
if (!options.yes && prompter == null) {
|
|
1144
1899
|
ui2.glitch(ui2.lore.needsTerminal);
|
|
@@ -1226,7 +1981,7 @@ async function runInit(ui2, options, prompter) {
|
|
|
1226
1981
|
}
|
|
1227
1982
|
|
|
1228
1983
|
// src/ui/console.ts
|
|
1229
|
-
import
|
|
1984
|
+
import process4 from "process";
|
|
1230
1985
|
|
|
1231
1986
|
// src/ui/lore.ts
|
|
1232
1987
|
var BANNER = String.raw`
|
|
@@ -1264,12 +2019,34 @@ var LORE = {
|
|
|
1264
2019
|
flatlined: "FLATLINED",
|
|
1265
2020
|
stable: "CONSTRUCT STABLE",
|
|
1266
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",
|
|
1267
2035
|
wireHarness: "Existing configs were kept, so the harness is not wired in yet. /construct-discover does this first; by hand:",
|
|
1268
2036
|
wireHarnessSteps: [
|
|
1269
2037
|
"eslint: ignore scripts/construct/*.workflow.mjs (the ladder script uses top-level return)",
|
|
1270
2038
|
"tsconfig: include scripts/**/*.ts; vitest: include scripts/tests/**/*.test.ts",
|
|
1271
2039
|
"package.json: make the quality script run composition:check (and contracts:check when there is a contract)"
|
|
1272
|
-
]
|
|
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"
|
|
1273
2050
|
};
|
|
1274
2051
|
var PLAIN_LORE = {
|
|
1275
2052
|
subtitle: (version) => `mikoshi-construct v${version}`,
|
|
@@ -1299,17 +2076,39 @@ var PLAIN_LORE = {
|
|
|
1299
2076
|
flatlined: "ERROR",
|
|
1300
2077
|
stable: "OK",
|
|
1301
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",
|
|
1302
2092
|
wireHarness: "Existing configs were kept, so the harness is not wired in yet. /construct-discover does this first; by hand:",
|
|
1303
2093
|
wireHarnessSteps: [
|
|
1304
2094
|
"eslint: ignore scripts/construct/*.workflow.mjs (the ladder script uses top-level return)",
|
|
1305
2095
|
"tsconfig: include scripts/**/*.ts; vitest: include scripts/tests/**/*.test.ts",
|
|
1306
2096
|
"package.json: make the quality script run composition:check (and contracts:check when there is a contract)"
|
|
1307
|
-
]
|
|
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"
|
|
1308
2107
|
};
|
|
1309
2108
|
|
|
1310
2109
|
// src/ui/console.ts
|
|
1311
2110
|
var stdoutWriter = (text2) => {
|
|
1312
|
-
|
|
2111
|
+
process4.stdout.write(text2);
|
|
1313
2112
|
};
|
|
1314
2113
|
function createUi(theme, write = stdoutWriter) {
|
|
1315
2114
|
const plain = theme.name === "plain";
|
|
@@ -1368,21 +2167,21 @@ function createUi(theme, write = stdoutWriter) {
|
|
|
1368
2167
|
}
|
|
1369
2168
|
|
|
1370
2169
|
// src/ui/theme.ts
|
|
1371
|
-
import
|
|
2170
|
+
import process5 from "process";
|
|
1372
2171
|
import pc from "picocolors";
|
|
1373
2172
|
function rgb(r, g, b) {
|
|
1374
2173
|
return (text2) => `\x1B[38;2;${r};${g};${b}m${text2}\x1B[39m`;
|
|
1375
2174
|
}
|
|
1376
2175
|
var identity = (text2) => text2;
|
|
1377
2176
|
function supportsColor() {
|
|
1378
|
-
if (
|
|
2177
|
+
if (process5.env.NO_COLOR != null && process5.env.NO_COLOR !== "")
|
|
1379
2178
|
return false;
|
|
1380
|
-
if (
|
|
2179
|
+
if (process5.env.FORCE_COLOR != null && process5.env.FORCE_COLOR !== "0")
|
|
1381
2180
|
return true;
|
|
1382
2181
|
return pc.isColorSupported;
|
|
1383
2182
|
}
|
|
1384
2183
|
function truecolor() {
|
|
1385
|
-
const term =
|
|
2184
|
+
const term = process5.env.COLORTERM ?? "";
|
|
1386
2185
|
return term === "truecolor" || term === "24bit";
|
|
1387
2186
|
}
|
|
1388
2187
|
var PLAIN = {
|
|
@@ -1451,12 +2250,12 @@ var init = defineCommand({
|
|
|
1451
2250
|
const console = ui(args);
|
|
1452
2251
|
console.banner(VERSION, args.johnny);
|
|
1453
2252
|
try {
|
|
1454
|
-
const prompter = isTTY(
|
|
2253
|
+
const prompter = isTTY(process6.stdout) && process6.stdin.isTTY === true ? createClackPrompter(console.lore) : void 0;
|
|
1455
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);
|
|
1456
|
-
|
|
2255
|
+
process6.exitCode = result.status === "aborted" ? 1 : 0;
|
|
1457
2256
|
} catch (error) {
|
|
1458
2257
|
console.flatline(error instanceof Error ? error.message : String(error));
|
|
1459
|
-
|
|
2258
|
+
process6.exitCode = 1;
|
|
1460
2259
|
}
|
|
1461
2260
|
}
|
|
1462
2261
|
});
|
|
@@ -1469,7 +2268,7 @@ var soulkill = defineCommand({
|
|
|
1469
2268
|
run({ args }) {
|
|
1470
2269
|
const report = detect(args.dir);
|
|
1471
2270
|
if (args.json) {
|
|
1472
|
-
|
|
2271
|
+
process6.stdout.write(`${JSON.stringify(report, null, 2)}
|
|
1473
2272
|
`);
|
|
1474
2273
|
return;
|
|
1475
2274
|
}
|
|
@@ -1489,13 +2288,13 @@ var doctor = defineCommand({
|
|
|
1489
2288
|
run({ args }) {
|
|
1490
2289
|
const result = runDoctor(args.dir);
|
|
1491
2290
|
if (args.json) {
|
|
1492
|
-
|
|
2291
|
+
process6.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
1493
2292
|
`);
|
|
1494
|
-
|
|
2293
|
+
process6.exitCode = result?.ok === true ? 0 : 1;
|
|
1495
2294
|
return;
|
|
1496
2295
|
}
|
|
1497
2296
|
const console = ui(args);
|
|
1498
|
-
|
|
2297
|
+
process6.exitCode = printDoctor(console, result);
|
|
1499
2298
|
}
|
|
1500
2299
|
});
|
|
1501
2300
|
var cost = defineCommand({
|
|
@@ -1506,16 +2305,14 @@ var cost = defineCommand({
|
|
|
1506
2305
|
json: { type: "boolean", description: "Machine-readable report", default: false }
|
|
1507
2306
|
},
|
|
1508
2307
|
run({ args }) {
|
|
1509
|
-
const
|
|
2308
|
+
const report = costReport(path21.resolve(args.dir));
|
|
1510
2309
|
if (args.json) {
|
|
1511
|
-
|
|
1512
|
-
process4.stdout.write(`${JSON.stringify(selected, null, 2)}
|
|
2310
|
+
process6.stdout.write(`${JSON.stringify(costJson(report, args.last), null, 2)}
|
|
1513
2311
|
`);
|
|
1514
|
-
|
|
2312
|
+
process6.exitCode = COST_EXIT[report.status];
|
|
1515
2313
|
return;
|
|
1516
2314
|
}
|
|
1517
|
-
|
|
1518
|
-
process4.exitCode = printCost(console, runs, args.last);
|
|
2315
|
+
process6.exitCode = printCost(ui(args), report, args.last);
|
|
1519
2316
|
}
|
|
1520
2317
|
});
|
|
1521
2318
|
var main = defineCommand({
|