mikoshi-construct 0.1.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/LICENSE +21 -0
- package/README.md +161 -0
- package/dist/cli.js +1492 -0
- package/package.json +73 -0
- package/templates/ai/claude/CLAUDE.md.eta +18 -0
- package/templates/ai/claude/CLAUDE.md.existing.eta +15 -0
- package/templates/ai/claude/_claude/agents/architect.md +38 -0
- package/templates/ai/claude/_claude/agents/harness.md +40 -0
- package/templates/ai/claude/_claude/agents/implementer.md +40 -0
- package/templates/ai/claude/_claude/commands/plan.md +24 -0
- package/templates/ai/claude/_claude/skills/implement/SKILL.md +42 -0
- package/templates/ai/claude/scripts/construct/implement.workflow.mjs +185 -0
- package/templates/ai/cursor/_cursor/rules/construct.mdc +19 -0
- package/templates/ai/review/_github/workflows/claude-review.yml.eta +51 -0
- package/templates/ai/shared/AGENTS.md.eta +123 -0
- package/templates/ai/shared/AGENTS.md.existing.eta +50 -0
- package/templates/ai/shared/_claude/commands/construct-discover.md +75 -0
- package/templates/ai/shared/_claude/rules/conventions.md +63 -0
- package/templates/ai/shared/_claude/rules/secrets.md +8 -0
- package/templates/ai/shared/_claude/rules/tests.md +6 -0
- package/templates/base/_editorconfig +12 -0
- package/templates/base/_github/workflows/security.yml +41 -0
- package/templates/base/_gitignore +11 -0
- package/templates/base/_gitleaks.toml +2 -0
- package/templates/base/_nvmrc.eta +1 -0
- package/templates/base/_vscode/settings.json +22 -0
- package/templates/base/architecture/checklists.md +42 -0
- package/templates/base/architecture/principles.md +113 -0
- package/templates/base/architecture/security-invariants.md +17 -0
- package/templates/harness/_github/workflows/ci.yml.eta +31 -0
- package/templates/harness/eslint.config.mjs +13 -0
- package/templates/harness/package.json.eta +29 -0
- package/templates/harness/pnpm-workspace.yaml +11 -0
- package/templates/harness/scripts/composition/check.ts +19 -0
- package/templates/harness/scripts/composition/files.ts +34 -0
- package/templates/harness/scripts/composition/model.ts +137 -0
- package/templates/harness/scripts/composition/render.ts +72 -0
- package/templates/harness/scripts/composition/sync-docs.ts +11 -0
- package/templates/harness/scripts/tests/composition/files.test.ts +16 -0
- package/templates/harness/scripts/tests/composition/model.test.ts +41 -0
- package/templates/harness/scripts/tests/composition/render.test.ts +54 -0
- package/templates/harness/tsconfig.base.json +14 -0
- package/templates/harness/tsconfig.json +5 -0
- package/templates/harness/vitest.config.ts +8 -0
- package/templates/presets/monorepo/baseline/eslint.config.mjs.eta +100 -0
- package/templates/presets/monorepo/baseline/package.json.eta +18 -0
- package/templates/presets/monorepo/baseline/packages/shared/package.json.eta +22 -0
- package/templates/presets/monorepo/baseline/packages/shared/src/api/openapi.ts +81 -0
- package/templates/presets/monorepo/baseline/packages/shared/src/index.ts +1 -0
- package/templates/presets/monorepo/baseline/packages/shared/tsconfig.build.json +10 -0
- package/templates/presets/monorepo/baseline/packages/shared/tsconfig.json +4 -0
- package/templates/presets/monorepo/baseline/pnpm-workspace.yaml.eta +31 -0
- package/templates/presets/monorepo/baseline/tsconfig.json +12 -0
- package/templates/presets/monorepo/baseline/vitest.config.ts +8 -0
- package/templates/presets/monorepo/sample/apps/api/package.json.eta +23 -0
- package/templates/presets/monorepo/sample/apps/api/tsconfig.build.json +10 -0
- package/templates/presets/monorepo/sample/apps/api/tsconfig.json +4 -0
- package/templates/presets/monorepo/sample/scripts/tests/lint/syntax-policy.test.ts.eta +36 -0
- package/templates/presets/node-backend/baseline/eslint.config.mjs +59 -0
- package/templates/presets/node-backend/baseline/package.json.eta +20 -0
- package/templates/presets/node-backend/baseline/src/contracts/openapi.ts +81 -0
- package/templates/presets/node-backend/baseline/tsconfig.build.json +10 -0
- package/templates/presets/node-backend/sample/scripts/tests/lint/syntax-policy.test.ts +31 -0
- package/templates/presets/node-frontend/baseline/_claude/rules/css.md +158 -0
- package/templates/presets/node-frontend/baseline/eslint.config.mjs +44 -0
- package/templates/presets/node-frontend/baseline/package.json.eta +15 -0
- package/templates/presets/node-frontend/baseline/tsconfig.json +11 -0
- package/templates/presets/node-frontend/baseline/vitest.config.ts +10 -0
- package/templates/presets/node-frontend/sample/architecture/app.md +37 -0
- package/templates/presets/node-frontend/sample/architecture/composition/app.yaml +41 -0
- package/templates/presets/node-frontend/sample/index.html.eta +12 -0
- package/templates/presets/node-frontend/sample/src/app.ts +32 -0
- package/templates/presets/node-frontend/sample/src/main.ts +9 -0
- package/templates/presets/node-frontend/sample/src/styles/app.css +43 -0
- package/templates/presets/node-frontend/sample/src/styles/tokens.css +16 -0
- package/templates/presets/node-frontend/sample/tests/app.test.ts +21 -0
- package/templates/stacks/express-api/app/src/app.ts +27 -0
- package/templates/stacks/express-api/app/src/config.ts.eta +30 -0
- package/templates/stacks/express-api/app/src/contracts/types.ts.eta +1 -0
- package/templates/stacks/express-api/app/src/health/health.controller.ts +12 -0
- package/templates/stacks/express-api/app/src/health/health.service.ts +23 -0
- package/templates/stacks/express-api/app/src/http/api-contract.middleware.ts +13 -0
- package/templates/stacks/express-api/app/src/http/app-error.ts +14 -0
- package/templates/stacks/express-api/app/src/http/async-handler.middleware.ts +7 -0
- package/templates/stacks/express-api/app/src/http/error-handler.middleware.ts +50 -0
- package/templates/stacks/express-api/app/src/server.ts +9 -0
- package/templates/stacks/express-api/app/tests/health.test.ts +28 -0
- package/templates/stacks/express-api/app/tests/http/error-handler.middleware.test.ts +48 -0
- package/templates/stacks/express-api/repo/architecture/composition/http.yaml.eta +45 -0
- package/templates/stacks/express-api/repo/architecture/http.md +39 -0
- package/templates/stacks/http-contract/_github/workflows/api-contract.yml +36 -0
- package/templates/stacks/http-contract/contracts/api/openapi.yaml.eta +62 -0
- package/templates/stacks/http-contract/package.json.eta +13 -0
- package/templates/stacks/http-contract/redocly.yaml.eta +8 -0
- package/templates/stacks/http-contract/scripts/contracts/types.mjs.eta +20 -0
- package/templates/stacks/http-contract/scripts/tests/contracts/security.test.ts.eta +86 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1492 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import path13 from "path";
|
|
5
|
+
import process4 from "process";
|
|
6
|
+
import { isTTY } from "@clack/prompts";
|
|
7
|
+
import { defineCommand, runMain } from "citty";
|
|
8
|
+
|
|
9
|
+
// src/commands/cost.ts
|
|
10
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
11
|
+
import { homedir } from "os";
|
|
12
|
+
import path from "path";
|
|
13
|
+
function emptyUsage() {
|
|
14
|
+
return { calls: 0, input: 0, cacheWrite: 0, cacheRead: 0, output: 0, models: [] };
|
|
15
|
+
}
|
|
16
|
+
function billable(usage) {
|
|
17
|
+
return usage.input + usage.cacheWrite + usage.cacheRead + usage.output;
|
|
18
|
+
}
|
|
19
|
+
var PRICE_RELATIVE_TO_INPUT = { cacheWrite: 1.25, cacheRead: 0.1, output: 5 };
|
|
20
|
+
function weighted(usage) {
|
|
21
|
+
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
|
+
}
|
|
23
|
+
function add(total, part) {
|
|
24
|
+
total.calls += part.calls;
|
|
25
|
+
total.input += part.input;
|
|
26
|
+
total.cacheWrite += part.cacheWrite;
|
|
27
|
+
total.cacheRead += part.cacheRead;
|
|
28
|
+
total.output += part.output;
|
|
29
|
+
for (const model of part.models) {
|
|
30
|
+
if (!total.models.includes(model))
|
|
31
|
+
total.models.push(model);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function projectKey(cwd) {
|
|
35
|
+
return cwd.replace(/[/.]/g, "-");
|
|
36
|
+
}
|
|
37
|
+
function claudeProjectsDir() {
|
|
38
|
+
return path.join(homedir(), ".claude", "projects");
|
|
39
|
+
}
|
|
40
|
+
function readUsage(file) {
|
|
41
|
+
const totals = emptyUsage();
|
|
42
|
+
for (const line of readFileSync(file, "utf8").split("\n")) {
|
|
43
|
+
if (!line.startsWith("{"))
|
|
44
|
+
continue;
|
|
45
|
+
let entry;
|
|
46
|
+
try {
|
|
47
|
+
entry = JSON.parse(line);
|
|
48
|
+
} catch {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const message = entry.message;
|
|
52
|
+
if (message?.role !== "assistant" || message.usage == null)
|
|
53
|
+
continue;
|
|
54
|
+
totals.calls += 1;
|
|
55
|
+
totals.input += message.usage.input_tokens ?? 0;
|
|
56
|
+
totals.cacheWrite += message.usage.cache_creation_input_tokens ?? 0;
|
|
57
|
+
totals.cacheRead += message.usage.cache_read_input_tokens ?? 0;
|
|
58
|
+
totals.output += message.usage.output_tokens ?? 0;
|
|
59
|
+
if (message.model != null && !totals.models.includes(message.model))
|
|
60
|
+
totals.models.push(message.model);
|
|
61
|
+
}
|
|
62
|
+
return totals;
|
|
63
|
+
}
|
|
64
|
+
function directories(parent) {
|
|
65
|
+
if (!existsSync(parent))
|
|
66
|
+
return [];
|
|
67
|
+
return readdirSync(parent).map((name) => path.join(parent, name)).filter((entry) => statSync(entry).isDirectory());
|
|
68
|
+
}
|
|
69
|
+
function collectRuns(sessionDir) {
|
|
70
|
+
return directories(path.join(sessionDir, "subagents", "workflows")).map((dir) => {
|
|
71
|
+
const agents = readdirSync(dir).filter((file) => file.startsWith("agent-") && file.endsWith(".jsonl")).map((file) => {
|
|
72
|
+
const metaPath = path.join(dir, file.replace(/\.jsonl$/, ".meta.json"));
|
|
73
|
+
const meta = existsSync(metaPath) ? JSON.parse(readFileSync(metaPath, "utf8")) : {};
|
|
74
|
+
return { label: meta.description ?? path.basename(file), type: meta.agentType ?? "?", usage: readUsage(path.join(dir, file)) };
|
|
75
|
+
});
|
|
76
|
+
const total = emptyUsage();
|
|
77
|
+
for (const agent of agents)
|
|
78
|
+
add(total, agent.usage);
|
|
79
|
+
return { session: path.basename(sessionDir), run: path.basename(dir), startedAt: statSync(dir).mtime.toISOString(), agents, total };
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function collectWorkflowRuns(cwd, projectsDir = claudeProjectsDir()) {
|
|
83
|
+
const projectDir = path.join(projectsDir, projectKey(cwd));
|
|
84
|
+
if (!existsSync(projectDir))
|
|
85
|
+
return null;
|
|
86
|
+
return directories(projectDir).flatMap(collectRuns).sort((a, b) => a.startedAt.localeCompare(b.startedAt));
|
|
87
|
+
}
|
|
88
|
+
function fmt(value) {
|
|
89
|
+
return value.toLocaleString("en-US");
|
|
90
|
+
}
|
|
91
|
+
function printCost(ui2, runs, last) {
|
|
92
|
+
if (runs == null) {
|
|
93
|
+
ui2.flatline("No Claude Code session data for this directory.");
|
|
94
|
+
return 1;
|
|
95
|
+
}
|
|
96
|
+
const selected = last ? runs.slice(-1) : runs;
|
|
97
|
+
if (selected.length === 0) {
|
|
98
|
+
ui2.glitch("No /implement runs recorded here yet.");
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
const grand = emptyUsage();
|
|
102
|
+
for (const run of selected) {
|
|
103
|
+
ui2.line(`${ui2.theme.accent(run.run)} ${ui2.theme.dim(`${run.startedAt} \xB7 ${run.total.models.join(", ")}`)}`);
|
|
104
|
+
for (const agent of run.agents) {
|
|
105
|
+
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)}`);
|
|
106
|
+
}
|
|
107
|
+
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`)}`);
|
|
108
|
+
ui2.line();
|
|
109
|
+
add(grand, run.total);
|
|
110
|
+
}
|
|
111
|
+
if (selected.length > 1)
|
|
112
|
+
ui2.line(`${ui2.theme.bold(`${selected.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})`)}`);
|
|
113
|
+
return 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/commands/doctor.ts
|
|
117
|
+
import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
|
|
118
|
+
import path3 from "path";
|
|
119
|
+
|
|
120
|
+
// src/manifest.ts
|
|
121
|
+
import { createHash } from "crypto";
|
|
122
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
123
|
+
import path2 from "path";
|
|
124
|
+
var MANIFEST_FILE = "construct.json";
|
|
125
|
+
var DISCOVERY_MARKERS = [
|
|
126
|
+
"product",
|
|
127
|
+
"module-map",
|
|
128
|
+
"commands",
|
|
129
|
+
"composition-roots",
|
|
130
|
+
"dependency-policy",
|
|
131
|
+
"high-effort-areas",
|
|
132
|
+
"composition",
|
|
133
|
+
"security-invariants",
|
|
134
|
+
"defects-vs-variance",
|
|
135
|
+
"open-questions"
|
|
136
|
+
];
|
|
137
|
+
function sha256(content) {
|
|
138
|
+
return createHash("sha256").update(content).digest("hex");
|
|
139
|
+
}
|
|
140
|
+
function markerFile(marker, compositionDir2 = "architecture/composition") {
|
|
141
|
+
switch (marker) {
|
|
142
|
+
case "composition":
|
|
143
|
+
return compositionDir2;
|
|
144
|
+
case "security-invariants":
|
|
145
|
+
return "architecture/security-invariants.md";
|
|
146
|
+
default:
|
|
147
|
+
return "AGENTS.md";
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function buildManifest(input) {
|
|
151
|
+
const files = {};
|
|
152
|
+
for (const op of input.written)
|
|
153
|
+
files[op.target] = sha256(op.content);
|
|
154
|
+
const discovery = Object.fromEntries(DISCOVERY_MARKERS.map((marker) => [marker, markerFile(marker, input.vars.compositionDir)]));
|
|
155
|
+
return {
|
|
156
|
+
construct: input.version,
|
|
157
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
158
|
+
preset: input.preset,
|
|
159
|
+
ai: input.ai,
|
|
160
|
+
review: input.review === "none" ? null : { provider: input.review, model: input.vars.reviewModel },
|
|
161
|
+
harness: { command: input.vars.harnessCommand },
|
|
162
|
+
report: { usage: true },
|
|
163
|
+
contracts: input.contracts ? { path: input.vars.contractPath, types: input.vars.contractTypesOutput } : null,
|
|
164
|
+
vars: input.vars,
|
|
165
|
+
files,
|
|
166
|
+
discovery
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function writeManifest(root, manifest) {
|
|
170
|
+
writeFileSync(path2.join(root, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}
|
|
171
|
+
`);
|
|
172
|
+
}
|
|
173
|
+
function readManifest(root) {
|
|
174
|
+
const file = path2.join(root, MANIFEST_FILE);
|
|
175
|
+
if (!existsSync2(file))
|
|
176
|
+
return null;
|
|
177
|
+
return JSON.parse(readFileSync2(file, "utf8"));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/commands/doctor.ts
|
|
181
|
+
var DISCOVERY_PLACEHOLDER = "_Not discovered yet \u2014 run `/construct-discover`._";
|
|
182
|
+
function markerOpen(marker) {
|
|
183
|
+
return `<!-- construct:discover:${marker} -->`;
|
|
184
|
+
}
|
|
185
|
+
function markerClose(marker) {
|
|
186
|
+
return `<!-- /construct:discover:${marker} -->`;
|
|
187
|
+
}
|
|
188
|
+
function isMarkerFilled(document, marker) {
|
|
189
|
+
const start = document.indexOf(markerOpen(marker));
|
|
190
|
+
const stop = document.indexOf(markerClose(marker));
|
|
191
|
+
if (start === -1 || stop === -1 || stop < start)
|
|
192
|
+
return false;
|
|
193
|
+
const body = document.slice(start + markerOpen(marker).length, stop).trim();
|
|
194
|
+
return body !== "" && body !== DISCOVERY_PLACEHOLDER;
|
|
195
|
+
}
|
|
196
|
+
var REQUIRED_QUALITY_STEPS = ["lint", "typecheck", "test"];
|
|
197
|
+
function contractProblems(root, contracts, scriptName, quality) {
|
|
198
|
+
if (contracts == null)
|
|
199
|
+
return [];
|
|
200
|
+
const problems = [contracts.path, contracts.types].filter((file) => !existsSync3(path3.join(root, file))).map((file) => `${file} is missing (construct.json \u2192 contracts)`);
|
|
201
|
+
if (!quality.includes("contracts:check"))
|
|
202
|
+
problems.push(`"${scriptName}" does not run contracts:check`);
|
|
203
|
+
return problems;
|
|
204
|
+
}
|
|
205
|
+
function harnessProblems(root, manifest) {
|
|
206
|
+
const command = manifest.harness.command;
|
|
207
|
+
const manifestPath = path3.join(root, "package.json");
|
|
208
|
+
if (!existsSync3(manifestPath))
|
|
209
|
+
return ["package.json is missing"];
|
|
210
|
+
const pkg = JSON.parse(readFileSync3(manifestPath, "utf8"));
|
|
211
|
+
const scripts = pkg.scripts ?? {};
|
|
212
|
+
const scriptName = command.replace(/^(pnpm|npm|yarn|bun)\s+(run\s+)?/, "");
|
|
213
|
+
const quality = scripts[scriptName];
|
|
214
|
+
if (quality == null)
|
|
215
|
+
return [`package.json has no "${scriptName}" script (harness command is "${command}")`];
|
|
216
|
+
return [
|
|
217
|
+
...REQUIRED_QUALITY_STEPS.filter((step) => !quality.includes(step)).map((step) => `"${scriptName}" does not run ${step}`),
|
|
218
|
+
...contractProblems(root, manifest.contracts, scriptName, quality)
|
|
219
|
+
];
|
|
220
|
+
}
|
|
221
|
+
function runDoctor(root) {
|
|
222
|
+
const manifest = readManifest(root);
|
|
223
|
+
if (manifest == null)
|
|
224
|
+
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
|
+
}
|
|
234
|
+
const missingDiscovery = DISCOVERY_MARKERS.filter((marker) => {
|
|
235
|
+
const location = path3.join(root, manifest.discovery[marker]);
|
|
236
|
+
if (marker === "composition")
|
|
237
|
+
return !existsSync3(location) || !readdirSync2(location).some((file) => file.endsWith(".yaml"));
|
|
238
|
+
return !existsSync3(location) || !isMarkerFilled(readFileSync3(location, "utf8"), marker);
|
|
239
|
+
});
|
|
240
|
+
const problems = harnessProblems(root, manifest);
|
|
241
|
+
return {
|
|
242
|
+
ok: missingFiles.length === 0 && problems.length === 0,
|
|
243
|
+
missingFiles,
|
|
244
|
+
modifiedFiles,
|
|
245
|
+
missingDiscovery,
|
|
246
|
+
harnessProblems: problems
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
function printDoctor(ui2, result) {
|
|
250
|
+
if (result == null) {
|
|
251
|
+
ui2.flatline("No construct.json here. Run `construct init` first.");
|
|
252
|
+
return 1;
|
|
253
|
+
}
|
|
254
|
+
if (result.harnessProblems.length > 0)
|
|
255
|
+
ui2.glitch("Harness is broken.", result.harnessProblems);
|
|
256
|
+
if (result.missingFiles.length > 0)
|
|
257
|
+
ui2.glitch("Baseline files are missing.", result.missingFiles);
|
|
258
|
+
if (result.missingDiscovery.length > 0) {
|
|
259
|
+
ui2.glitch(ui2.lore.discoveryIncomplete, ["", "Missing:", ...result.missingDiscovery.map((marker) => ` ${marker}`), "", "Run: claude \u2192 /construct-discover"]);
|
|
260
|
+
}
|
|
261
|
+
if (result.modifiedFiles.length > 0)
|
|
262
|
+
ui2.line(ui2.theme.dim(` ${result.modifiedFiles.length} baseline files modified since init (expected once the project evolves).`));
|
|
263
|
+
if (result.ok) {
|
|
264
|
+
ui2.ok(ui2.lore.stable);
|
|
265
|
+
return 0;
|
|
266
|
+
}
|
|
267
|
+
return 1;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// src/commands/init.ts
|
|
271
|
+
import { mkdirSync as mkdirSync2 } from "fs";
|
|
272
|
+
import path12 from "path";
|
|
273
|
+
|
|
274
|
+
// src/detect/index.ts
|
|
275
|
+
import { existsSync as existsSync7 } from "fs";
|
|
276
|
+
import path7 from "path";
|
|
277
|
+
import process from "process";
|
|
278
|
+
|
|
279
|
+
// src/detect/existing.ts
|
|
280
|
+
import { existsSync as existsSync4, readdirSync as readdirSync3 } from "fs";
|
|
281
|
+
import path4 from "path";
|
|
282
|
+
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
|
+
var COMPOSITION_CANDIDATES = ["architecture/composition", "docs/architecture/composition", "docs/composition", "composition"];
|
|
284
|
+
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
|
+
function anyExists(dir, candidates) {
|
|
286
|
+
return candidates.some((candidate) => existsSync4(path4.join(dir, candidate)));
|
|
287
|
+
}
|
|
288
|
+
function firstExisting(dir, candidates) {
|
|
289
|
+
return candidates.find((candidate) => existsSync4(path4.join(dir, candidate))) ?? null;
|
|
290
|
+
}
|
|
291
|
+
function compositionDir(dir) {
|
|
292
|
+
return COMPOSITION_CANDIDATES.find((candidate) => {
|
|
293
|
+
const absolute = path4.join(dir, candidate);
|
|
294
|
+
return existsSync4(absolute) && readdirSync3(absolute).some((file) => file.endsWith(".yaml") || file.endsWith(".yml"));
|
|
295
|
+
}) ?? null;
|
|
296
|
+
}
|
|
297
|
+
function hasWorkflows(dir) {
|
|
298
|
+
const workflows = path4.join(dir, ".github", "workflows");
|
|
299
|
+
return existsSync4(workflows) && readdirSync3(workflows).some((file) => file.endsWith(".yml") || file.endsWith(".yaml"));
|
|
300
|
+
}
|
|
301
|
+
function detectExisting(dir) {
|
|
302
|
+
return {
|
|
303
|
+
packageJson: existsSync4(path4.join(dir, "package.json")),
|
|
304
|
+
tsconfig: existsSync4(path4.join(dir, "tsconfig.json")),
|
|
305
|
+
eslintConfig: anyExists(dir, ESLINT_CONFIGS),
|
|
306
|
+
githubWorkflows: hasWorkflows(dir),
|
|
307
|
+
claudeMd: existsSync4(path4.join(dir, "CLAUDE.md")),
|
|
308
|
+
agentsMd: existsSync4(path4.join(dir, "AGENTS.md")),
|
|
309
|
+
cursorRules: existsSync4(path4.join(dir, ".cursor", "rules")),
|
|
310
|
+
openapi: firstExisting(dir, OPENAPI_CANDIDATES),
|
|
311
|
+
compositionDir: compositionDir(dir),
|
|
312
|
+
constructJson: existsSync4(path4.join(dir, "construct.json"))
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// src/detect/layout.ts
|
|
317
|
+
import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
|
|
318
|
+
import path5 from "path";
|
|
319
|
+
var IGNORED_ENTRIES = /* @__PURE__ */ new Set([".git", ".DS_Store", ".gitignore", ".gitattributes", "LICENSE", "README.md", ".idea", ".vscode"]);
|
|
320
|
+
function isEmptyDir(dir) {
|
|
321
|
+
if (!existsSync5(dir))
|
|
322
|
+
return true;
|
|
323
|
+
return readdirSync4(dir).every((entry) => IGNORED_ENTRIES.has(entry));
|
|
324
|
+
}
|
|
325
|
+
function hasWorkspacesField(dir) {
|
|
326
|
+
const manifest = path5.join(dir, "package.json");
|
|
327
|
+
if (!existsSync5(manifest))
|
|
328
|
+
return false;
|
|
329
|
+
try {
|
|
330
|
+
const parsed = JSON.parse(readFileSync4(manifest, "utf8"));
|
|
331
|
+
return parsed.workspaces != null;
|
|
332
|
+
} catch {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function detectMonorepoTools(dir) {
|
|
337
|
+
const tools = [];
|
|
338
|
+
if (existsSync5(path5.join(dir, "pnpm-workspace.yaml")))
|
|
339
|
+
tools.push("pnpm-workspace");
|
|
340
|
+
if (hasWorkspacesField(dir))
|
|
341
|
+
tools.push("npm-workspaces");
|
|
342
|
+
if (existsSync5(path5.join(dir, "turbo.json")))
|
|
343
|
+
tools.push("turbo");
|
|
344
|
+
if (existsSync5(path5.join(dir, "nx.json")))
|
|
345
|
+
tools.push("nx");
|
|
346
|
+
return tools;
|
|
347
|
+
}
|
|
348
|
+
function detectWorkspaceDirs(dir) {
|
|
349
|
+
return ["apps", "packages", "libs", "services"].filter((name) => existsSync5(path5.join(dir, name)) && statSync2(path5.join(dir, name)).isDirectory());
|
|
350
|
+
}
|
|
351
|
+
function packageName(dir) {
|
|
352
|
+
try {
|
|
353
|
+
const parsed = JSON.parse(readFileSync4(path5.join(dir, "package.json"), "utf8"));
|
|
354
|
+
return typeof parsed.name === "string" && parsed.name !== "" ? parsed.name : null;
|
|
355
|
+
} catch {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
function detectWorkspacePackages(root, workspaceDirs) {
|
|
360
|
+
return workspaceDirs.flatMap((parent) => readdirSync4(path5.join(root, parent)).sort().map((entry) => `${parent}/${entry}`).filter((dir) => existsSync5(path5.join(root, dir, "package.json"))).map((dir) => ({ dir, name: packageName(path5.join(root, dir)) ?? dir.split("/").at(-1) ?? dir })));
|
|
361
|
+
}
|
|
362
|
+
function detectLayout(dir, monorepoTools, workspaceDirs, hasSrc) {
|
|
363
|
+
if (isEmptyDir(dir))
|
|
364
|
+
return "empty";
|
|
365
|
+
if (monorepoTools.length > 0 || workspaceDirs.length > 0)
|
|
366
|
+
return "monorepo";
|
|
367
|
+
if (hasSrc || existsSync5(path5.join(dir, "package.json")))
|
|
368
|
+
return "single";
|
|
369
|
+
return "unknown";
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// src/detect/package-manager.ts
|
|
373
|
+
import { execFileSync } from "child_process";
|
|
374
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
|
|
375
|
+
import path6 from "path";
|
|
376
|
+
var LOCKFILES = [
|
|
377
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
378
|
+
["bun.lockb", "bun"],
|
|
379
|
+
["bun.lock", "bun"],
|
|
380
|
+
["yarn.lock", "yarn"],
|
|
381
|
+
["package-lock.json", "npm"]
|
|
382
|
+
];
|
|
383
|
+
function fromPackageManagerField(dir) {
|
|
384
|
+
const manifest = path6.join(dir, "package.json");
|
|
385
|
+
if (!existsSync6(manifest))
|
|
386
|
+
return null;
|
|
387
|
+
try {
|
|
388
|
+
const parsed = JSON.parse(readFileSync5(manifest, "utf8"));
|
|
389
|
+
const name = parsed.packageManager?.split("@")[0];
|
|
390
|
+
return name === "pnpm" || name === "npm" || name === "yarn" || name === "bun" ? name : null;
|
|
391
|
+
} catch {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
function detectPackageManager(dir) {
|
|
396
|
+
const declared = fromPackageManagerField(dir);
|
|
397
|
+
if (declared != null)
|
|
398
|
+
return declared;
|
|
399
|
+
for (const [lockfile, manager] of LOCKFILES) {
|
|
400
|
+
if (existsSync6(path6.join(dir, lockfile)))
|
|
401
|
+
return manager;
|
|
402
|
+
}
|
|
403
|
+
return existsSync6(path6.join(dir, "package.json")) ? "npm" : "none";
|
|
404
|
+
}
|
|
405
|
+
var pnpmVersionCache;
|
|
406
|
+
function detectPnpmVersion() {
|
|
407
|
+
if (pnpmVersionCache !== void 0)
|
|
408
|
+
return pnpmVersionCache;
|
|
409
|
+
try {
|
|
410
|
+
pnpmVersionCache = execFileSync("pnpm", ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
|
|
411
|
+
} catch {
|
|
412
|
+
pnpmVersionCache = null;
|
|
413
|
+
}
|
|
414
|
+
return pnpmVersionCache;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// src/detect/index.ts
|
|
418
|
+
function detect(dir) {
|
|
419
|
+
const root = path7.resolve(dir);
|
|
420
|
+
const monorepoTools = detectMonorepoTools(root);
|
|
421
|
+
const workspaceDirs = detectWorkspaceDirs(root);
|
|
422
|
+
const hasSrc = existsSync7(path7.join(root, "src"));
|
|
423
|
+
return {
|
|
424
|
+
dir: root,
|
|
425
|
+
packageManager: detectPackageManager(root),
|
|
426
|
+
pnpmVersion: detectPnpmVersion(),
|
|
427
|
+
layout: detectLayout(root, monorepoTools, workspaceDirs, hasSrc),
|
|
428
|
+
monorepoTools,
|
|
429
|
+
workspaceDirs,
|
|
430
|
+
workspacePackages: detectWorkspacePackages(root, workspaceDirs),
|
|
431
|
+
hasSrc,
|
|
432
|
+
nodeMajor: Number(process.versions.node.split(".")[0]),
|
|
433
|
+
existing: detectExisting(root)
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// src/materialize/apply.ts
|
|
438
|
+
import { mkdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
439
|
+
import path8 from "path";
|
|
440
|
+
function applyPlan(root, ops) {
|
|
441
|
+
const written = [];
|
|
442
|
+
for (const op of ops) {
|
|
443
|
+
if (op.action === "skip")
|
|
444
|
+
continue;
|
|
445
|
+
const absolute = path8.join(root, op.target);
|
|
446
|
+
mkdirSync(path8.dirname(absolute), { recursive: true });
|
|
447
|
+
writeFileSync2(absolute, op.content);
|
|
448
|
+
written.push(op);
|
|
449
|
+
}
|
|
450
|
+
return written;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// src/materialize/plan.ts
|
|
454
|
+
import { existsSync as existsSync9, readFileSync as readFileSync6 } from "fs";
|
|
455
|
+
import path10 from "path";
|
|
456
|
+
|
|
457
|
+
// src/materialize/rules.ts
|
|
458
|
+
var CLAUDE_RULES_DIR = ".claude/rules/";
|
|
459
|
+
var CURSOR_RULES_DIR = ".cursor/rules/";
|
|
460
|
+
var DISCOVERY_PROTOCOL = ".claude/commands/construct-discover.md";
|
|
461
|
+
var CURSOR_DISCOVERY_RULE = ".cursor/rules/construct-discover.mdc";
|
|
462
|
+
var FRONTMATTER = /^---\n([\s\S]*?)\n---\n/;
|
|
463
|
+
function unquote(value) {
|
|
464
|
+
return value.trim().replace(/^(["'])(.*)\1$/, "$2");
|
|
465
|
+
}
|
|
466
|
+
function parseFrontmatter(block) {
|
|
467
|
+
const paths = [];
|
|
468
|
+
let description;
|
|
469
|
+
let inPaths = false;
|
|
470
|
+
for (const line of block.split("\n")) {
|
|
471
|
+
const item = /^\s*-\s+(\S.*)$/.exec(line);
|
|
472
|
+
if (inPaths && item != null) {
|
|
473
|
+
paths.push(unquote(item[1]));
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
inPaths = /^paths:\s*$/.test(line);
|
|
477
|
+
const scalar = /^description:\s*(\S.*)$/.exec(line);
|
|
478
|
+
if (scalar != null)
|
|
479
|
+
description = unquote(scalar[1]);
|
|
480
|
+
}
|
|
481
|
+
return { description, paths };
|
|
482
|
+
}
|
|
483
|
+
function firstHeading(body) {
|
|
484
|
+
return /^#\s+(\S.*)$/m.exec(body)?.[1].trim();
|
|
485
|
+
}
|
|
486
|
+
function parseClaudeRule(content) {
|
|
487
|
+
const match = FRONTMATTER.exec(content);
|
|
488
|
+
const body = match == null ? content : content.slice(match[0].length).replace(/^\n+/, "");
|
|
489
|
+
const front = match == null ? { paths: [] } : parseFrontmatter(match[1]);
|
|
490
|
+
return { description: front.description ?? firstHeading(body) ?? "Project rule", paths: front.paths, body };
|
|
491
|
+
}
|
|
492
|
+
function isClaudeRule(target) {
|
|
493
|
+
return target.startsWith(CLAUDE_RULES_DIR) && target.endsWith(".md");
|
|
494
|
+
}
|
|
495
|
+
function cursorRuleTarget(target) {
|
|
496
|
+
return `${CURSOR_RULES_DIR}${target.slice(CLAUDE_RULES_DIR.length, -".md".length)}.mdc`;
|
|
497
|
+
}
|
|
498
|
+
function toCursorRule(content) {
|
|
499
|
+
const rule = parseClaudeRule(content);
|
|
500
|
+
const always = rule.paths.length === 0;
|
|
501
|
+
const globs = always ? "" : `globs: ${rule.paths.join(", ")}
|
|
502
|
+
`;
|
|
503
|
+
return `---
|
|
504
|
+
description: ${rule.description}
|
|
505
|
+
${globs}alwaysApply: ${always}
|
|
506
|
+
---
|
|
507
|
+
|
|
508
|
+
${rule.body}`;
|
|
509
|
+
}
|
|
510
|
+
function toCursorDiscoveryRule(content) {
|
|
511
|
+
const rule = parseClaudeRule(content);
|
|
512
|
+
const body = rule.body.replaceAll("$ARGUMENTS", "what the user asked to (re)discover");
|
|
513
|
+
return `---
|
|
514
|
+
description: ${rule.description}
|
|
515
|
+
alwaysApply: false
|
|
516
|
+
---
|
|
517
|
+
|
|
518
|
+
${body}`;
|
|
519
|
+
}
|
|
520
|
+
function mapRulesForTargets(files, ai) {
|
|
521
|
+
const result = /* @__PURE__ */ new Map();
|
|
522
|
+
for (const [target, content] of files) {
|
|
523
|
+
const rule = isClaudeRule(target);
|
|
524
|
+
const protocol = target === DISCOVERY_PROTOCOL;
|
|
525
|
+
if (!rule && !protocol) {
|
|
526
|
+
result.set(target, content);
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
if (ai !== "cursor")
|
|
530
|
+
result.set(target, content);
|
|
531
|
+
if (ai !== "claude")
|
|
532
|
+
result.set(protocol ? CURSOR_DISCOVERY_RULE : cursorRuleTarget(target), protocol ? toCursorDiscoveryRule(content) : toCursorRule(content));
|
|
533
|
+
}
|
|
534
|
+
return result;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// src/materialize/strategies.ts
|
|
538
|
+
var MERGE_JSON = /* @__PURE__ */ new Set(["package.json"]);
|
|
539
|
+
var APPEND_BLOCK = /* @__PURE__ */ new Set([".gitignore", "CLAUDE.md", "AGENTS.md"]);
|
|
540
|
+
var BLOCK_BEGIN = "<!-- construct:begin -->";
|
|
541
|
+
var BLOCK_END = "<!-- construct:end -->";
|
|
542
|
+
var GITIGNORE_BEGIN = "# construct:begin";
|
|
543
|
+
var GITIGNORE_END = "# construct:end";
|
|
544
|
+
function strategyFor(target) {
|
|
545
|
+
const basename = target.split("/").at(-1) ?? target;
|
|
546
|
+
if (MERGE_JSON.has(basename))
|
|
547
|
+
return "merge-json";
|
|
548
|
+
if (APPEND_BLOCK.has(basename))
|
|
549
|
+
return "append-block";
|
|
550
|
+
return "create";
|
|
551
|
+
}
|
|
552
|
+
function isObject(value) {
|
|
553
|
+
return typeof value === "object" && value != null && !Array.isArray(value);
|
|
554
|
+
}
|
|
555
|
+
function mergeJson(existing, incoming, conflicts, prefix = "") {
|
|
556
|
+
const result = { ...existing };
|
|
557
|
+
for (const [key, value] of Object.entries(incoming)) {
|
|
558
|
+
const at = prefix === "" ? key : `${prefix}.${key}`;
|
|
559
|
+
if (!(key in existing)) {
|
|
560
|
+
result[key] = value;
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
const current = existing[key];
|
|
564
|
+
if (isObject(current) && isObject(value)) {
|
|
565
|
+
result[key] = mergeJson(current, value, conflicts, at);
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
if (JSON.stringify(current) !== JSON.stringify(value))
|
|
569
|
+
conflicts.push(at);
|
|
570
|
+
}
|
|
571
|
+
return result;
|
|
572
|
+
}
|
|
573
|
+
function markersFor(target) {
|
|
574
|
+
return target.endsWith(".gitignore") ? [GITIGNORE_BEGIN, GITIGNORE_END] : [BLOCK_BEGIN, BLOCK_END];
|
|
575
|
+
}
|
|
576
|
+
var DISCOVERY_OPEN = /<!-- construct:discover:([\w-]+) -->/g;
|
|
577
|
+
function discoveryBlock(document, marker) {
|
|
578
|
+
const open = `<!-- construct:discover:${marker} -->`;
|
|
579
|
+
const close = `<!-- /construct:discover:${marker} -->`;
|
|
580
|
+
const start = document.indexOf(open);
|
|
581
|
+
const end = document.indexOf(close);
|
|
582
|
+
if (start === -1 || end === -1 || end < start)
|
|
583
|
+
return null;
|
|
584
|
+
return { start: start + open.length, end, body: document.slice(start + open.length, end) };
|
|
585
|
+
}
|
|
586
|
+
function preserveDiscovery(existing, incoming) {
|
|
587
|
+
let result = incoming;
|
|
588
|
+
for (const [, marker] of existing.matchAll(DISCOVERY_OPEN)) {
|
|
589
|
+
const previous = discoveryBlock(existing, marker);
|
|
590
|
+
const next = discoveryBlock(result, marker);
|
|
591
|
+
if (previous == null || next == null || previous.body.trim() === "" || previous.body.includes("_Not discovered yet"))
|
|
592
|
+
continue;
|
|
593
|
+
result = `${result.slice(0, next.start)}${previous.body}${result.slice(next.end)}`;
|
|
594
|
+
}
|
|
595
|
+
return result;
|
|
596
|
+
}
|
|
597
|
+
function withoutSecondH1(existing, block) {
|
|
598
|
+
if (existing.trim() === "" || !/^# /m.test(existing))
|
|
599
|
+
return block;
|
|
600
|
+
return block.replace(/^# (.*)$/m, "## $1");
|
|
601
|
+
}
|
|
602
|
+
function appendBlock(existing, block, target) {
|
|
603
|
+
const [begin, end] = markersFor(target);
|
|
604
|
+
const wrapped = `${begin}
|
|
605
|
+
${preserveDiscovery(existing, withoutSecondH1(existing, block)).trimEnd()}
|
|
606
|
+
${end}
|
|
607
|
+
`;
|
|
608
|
+
const start = existing.indexOf(begin);
|
|
609
|
+
const stop = existing.indexOf(end);
|
|
610
|
+
if (start !== -1 && stop !== -1 && stop > start)
|
|
611
|
+
return `${existing.slice(0, start)}${wrapped}${existing.slice(stop + end.length).replace(/^\n/, "")}`;
|
|
612
|
+
const separator = existing.length === 0 || existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
|
|
613
|
+
return `${existing}${separator}${wrapped}`;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// src/materialize/templates.ts
|
|
617
|
+
import { existsSync as existsSync8, readdirSync as readdirSync5, statSync as statSync3 } from "fs";
|
|
618
|
+
import path9 from "path";
|
|
619
|
+
import { fileURLToPath } from "url";
|
|
620
|
+
var HERE = path9.dirname(fileURLToPath(import.meta.url));
|
|
621
|
+
function templatesRoot() {
|
|
622
|
+
const candidates = [path9.resolve(HERE, "../templates"), path9.resolve(HERE, "../../templates")];
|
|
623
|
+
const found = candidates.find((candidate) => existsSync8(candidate));
|
|
624
|
+
if (found == null)
|
|
625
|
+
throw new Error(`templates directory not found next to ${HERE}`);
|
|
626
|
+
return found;
|
|
627
|
+
}
|
|
628
|
+
var EXISTING_SUFFIX = ".existing.eta";
|
|
629
|
+
function toTargetPath(relative) {
|
|
630
|
+
const segments = relative.split(path9.sep).map((segment) => segment.startsWith("_") ? `.${segment.slice(1)}` : segment);
|
|
631
|
+
const joined = segments.join("/");
|
|
632
|
+
if (joined.endsWith(EXISTING_SUFFIX))
|
|
633
|
+
return { target: joined.slice(0, -EXISTING_SUFFIX.length), rendered: true, variant: "existing" };
|
|
634
|
+
return joined.endsWith(".eta") ? { target: joined.slice(0, -".eta".length), rendered: true, variant: "default" } : { target: joined, rendered: false, variant: "default" };
|
|
635
|
+
}
|
|
636
|
+
function walk(root, current, files) {
|
|
637
|
+
for (const entry of readdirSync5(current).sort()) {
|
|
638
|
+
const absolute = path9.join(current, entry);
|
|
639
|
+
if (statSync3(absolute).isDirectory())
|
|
640
|
+
walk(root, absolute, files);
|
|
641
|
+
else if (entry !== ".DS_Store")
|
|
642
|
+
files.push(path9.relative(root, absolute));
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
function listTemplateFiles(group) {
|
|
646
|
+
const root = path9.join(templatesRoot(), group);
|
|
647
|
+
if (!existsSync8(root))
|
|
648
|
+
throw new Error(`template group "${group}" does not exist`);
|
|
649
|
+
const files = [];
|
|
650
|
+
walk(root, root, files);
|
|
651
|
+
return files.map((relative) => {
|
|
652
|
+
const { target, rendered, variant } = toTargetPath(relative);
|
|
653
|
+
return { group, source: path9.join(root, relative), target, rendered, variant };
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
var BLOCK = /^[ \t]*\{\{#(if|unless) (\w+)\}\}[ \t]*\n([\s\S]*?)^[ \t]*\{\{\/\1\}\}[ \t]*\n/gm;
|
|
657
|
+
var VARIABLE = /\{\{\s*(\w+)\s*\}\}/g;
|
|
658
|
+
function lookup(vars, name, match) {
|
|
659
|
+
const value = vars[name];
|
|
660
|
+
if (value == null)
|
|
661
|
+
throw new Error(`template variable "${name}" is not defined (${match})`);
|
|
662
|
+
return value;
|
|
663
|
+
}
|
|
664
|
+
function isTruthy(value) {
|
|
665
|
+
return value !== "" && value !== "false";
|
|
666
|
+
}
|
|
667
|
+
function render(template, vars) {
|
|
668
|
+
const expanded = template.replaceAll(BLOCK, (match, kind, name, body) => isTruthy(lookup(vars, name, match)) === (kind === "if") ? body : "");
|
|
669
|
+
return expanded.replaceAll(VARIABLE, (match, name) => lookup(vars, name, match));
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// src/materialize/plan.ts
|
|
673
|
+
function toMount(group) {
|
|
674
|
+
return typeof group === "string" ? { group } : group;
|
|
675
|
+
}
|
|
676
|
+
function mountTarget(mount, target) {
|
|
677
|
+
return mount.into == null || mount.into === "." ? target : `${mount.into.replace(/\/$/, "")}/${target}`;
|
|
678
|
+
}
|
|
679
|
+
function readTemplate(source, rendered, vars) {
|
|
680
|
+
const raw = readFileSync6(source, "utf8");
|
|
681
|
+
return rendered ? render(raw, vars) : raw;
|
|
682
|
+
}
|
|
683
|
+
var SORTED_SECTIONS = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
|
|
684
|
+
var MANIFEST_KEY_ORDER = [
|
|
685
|
+
"name",
|
|
686
|
+
"type",
|
|
687
|
+
"version",
|
|
688
|
+
"private",
|
|
689
|
+
"packageManager",
|
|
690
|
+
"description",
|
|
691
|
+
"author",
|
|
692
|
+
"license",
|
|
693
|
+
"homepage",
|
|
694
|
+
"repository",
|
|
695
|
+
"bugs",
|
|
696
|
+
"keywords",
|
|
697
|
+
"sideEffects",
|
|
698
|
+
"exports",
|
|
699
|
+
"main",
|
|
700
|
+
"module",
|
|
701
|
+
"types",
|
|
702
|
+
"bin",
|
|
703
|
+
"files",
|
|
704
|
+
"engines",
|
|
705
|
+
"scripts",
|
|
706
|
+
"peerDependencies",
|
|
707
|
+
"dependencies",
|
|
708
|
+
"optionalDependencies",
|
|
709
|
+
"devDependencies",
|
|
710
|
+
"pnpm",
|
|
711
|
+
"overrides"
|
|
712
|
+
];
|
|
713
|
+
function sortSections(manifest) {
|
|
714
|
+
const result = { ...manifest };
|
|
715
|
+
for (const section of SORTED_SECTIONS) {
|
|
716
|
+
const value = result[section];
|
|
717
|
+
if (typeof value === "object" && value != null && !Array.isArray(value))
|
|
718
|
+
result[section] = Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)));
|
|
719
|
+
}
|
|
720
|
+
return result;
|
|
721
|
+
}
|
|
722
|
+
function orderManifestKeys(manifest) {
|
|
723
|
+
const rank = (key) => {
|
|
724
|
+
const index = MANIFEST_KEY_ORDER.indexOf(key);
|
|
725
|
+
return index === -1 ? MANIFEST_KEY_ORDER.length : index;
|
|
726
|
+
};
|
|
727
|
+
return Object.fromEntries(Object.entries(manifest).sort(([a], [b]) => rank(a) - rank(b)));
|
|
728
|
+
}
|
|
729
|
+
function layerJson(earlier, later) {
|
|
730
|
+
const base = JSON.parse(earlier);
|
|
731
|
+
const override = JSON.parse(later);
|
|
732
|
+
return `${JSON.stringify(orderManifestKeys(sortSections(mergeJson(override, base, []))), null, 2)}
|
|
733
|
+
`;
|
|
734
|
+
}
|
|
735
|
+
var NOT_ADDED_TO_EXISTING_MANIFEST = ["version"];
|
|
736
|
+
function planOne(root, target, content, conflicts, existingVariant) {
|
|
737
|
+
const strategy = strategyFor(target);
|
|
738
|
+
const absolute = path10.join(root, target);
|
|
739
|
+
const exists = existsSync9(absolute);
|
|
740
|
+
if (!exists)
|
|
741
|
+
return { target, strategy, action: "create", content: strategy === "append-block" ? appendBlock("", content, target) : content };
|
|
742
|
+
if (strategy === "merge-json") {
|
|
743
|
+
const existing = JSON.parse(readFileSync6(absolute, "utf8"));
|
|
744
|
+
const incoming = JSON.parse(content);
|
|
745
|
+
for (const key of NOT_ADDED_TO_EXISTING_MANIFEST)
|
|
746
|
+
delete incoming[key];
|
|
747
|
+
const localConflicts = [];
|
|
748
|
+
const merged = mergeJson(existing, incoming, localConflicts);
|
|
749
|
+
conflicts.push(...localConflicts.map((key) => `${target}: ${key}`));
|
|
750
|
+
return { target, strategy, action: "merge", content: `${JSON.stringify(orderManifestKeys(sortSections(merged)), null, 2)}
|
|
751
|
+
` };
|
|
752
|
+
}
|
|
753
|
+
if (strategy === "append-block") {
|
|
754
|
+
const existing = readFileSync6(absolute, "utf8");
|
|
755
|
+
return { target, strategy, action: "append", content: appendBlock(existing, existingVariant ?? content, target) };
|
|
756
|
+
}
|
|
757
|
+
return { target, strategy, action: "skip", content, note: "exists, review manually" };
|
|
758
|
+
}
|
|
759
|
+
function planMaterialize(root, groups, vars, options) {
|
|
760
|
+
const conflicts = [];
|
|
761
|
+
const omittedGroups = [];
|
|
762
|
+
const layered = /* @__PURE__ */ new Map();
|
|
763
|
+
const existingVariants = /* @__PURE__ */ new Map();
|
|
764
|
+
for (const mount of groups.map(toMount)) {
|
|
765
|
+
if (mount.onlyWhenEmpty === true && !options.emptyTarget) {
|
|
766
|
+
omittedGroups.push(mount.group);
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
for (const file of listTemplateFiles(mount.group)) {
|
|
770
|
+
const target = mountTarget(mount, file.target);
|
|
771
|
+
const content = readTemplate(file.source, file.rendered, vars);
|
|
772
|
+
if (file.variant === "existing") {
|
|
773
|
+
existingVariants.set(target, content);
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
const previous = layered.get(target);
|
|
777
|
+
layered.set(target, previous == null || strategyFor(target) !== "merge-json" ? content : layerJson(previous, content));
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
const ops = [...mapRulesForTargets(layered, options.ai).entries()].map(([target, content]) => planOne(root, target, content, conflicts, existingVariants.get(target))).sort((a, b) => a.target.localeCompare(b.target));
|
|
781
|
+
return { ops, conflicts, omittedGroups };
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// src/presets/index.ts
|
|
785
|
+
var AI_TARGET_LABELS = {
|
|
786
|
+
claude: "Claude Code",
|
|
787
|
+
cursor: "Cursor",
|
|
788
|
+
both: "Claude Code, Cursor"
|
|
789
|
+
};
|
|
790
|
+
var EXPRESS_APP = "stacks/express-api/app";
|
|
791
|
+
var EXPRESS_REPO = "stacks/express-api/repo";
|
|
792
|
+
var HTTP_CONTRACT = "stacks/http-contract";
|
|
793
|
+
function sampleWorkspace(scope) {
|
|
794
|
+
return [
|
|
795
|
+
{ dir: "packages/shared", name: `${scope}/shared` },
|
|
796
|
+
{ dir: "apps/api", name: `${scope}/api` }
|
|
797
|
+
];
|
|
798
|
+
}
|
|
799
|
+
function quote(value) {
|
|
800
|
+
return `'${value}'`;
|
|
801
|
+
}
|
|
802
|
+
function renderWorkspacePolicy(packages, sample) {
|
|
803
|
+
const names = packages.map((pkg) => pkg.name);
|
|
804
|
+
const allowedFor = (pkg) => {
|
|
805
|
+
if (sample)
|
|
806
|
+
return pkg.dir.startsWith("apps/") ? names.filter((name) => name !== pkg.name) : [];
|
|
807
|
+
return names.filter((name) => name !== pkg.name);
|
|
808
|
+
};
|
|
809
|
+
const lines = packages.map((pkg) => ` ${quote(pkg.dir)}: [${allowedFor(pkg).map(quote).join(", ")}],`);
|
|
810
|
+
return {
|
|
811
|
+
workspacePackages: `[${names.map(quote).join(", ")}]`,
|
|
812
|
+
allowedWorkspaceImports: `{
|
|
813
|
+
${lines.join("\n")}
|
|
814
|
+
}`
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
var PRESETS = {
|
|
818
|
+
"node-backend": {
|
|
819
|
+
id: "node-backend",
|
|
820
|
+
label: "Node.js backend",
|
|
821
|
+
description: "Express + TypeScript, contract-first HTTP API, composition root, harness",
|
|
822
|
+
groups: [
|
|
823
|
+
"base",
|
|
824
|
+
"harness",
|
|
825
|
+
HTTP_CONTRACT,
|
|
826
|
+
{ group: EXPRESS_APP, onlyWhenEmpty: true },
|
|
827
|
+
{ group: EXPRESS_REPO, onlyWhenEmpty: true },
|
|
828
|
+
{ group: "presets/node-backend/sample", onlyWhenEmpty: true },
|
|
829
|
+
"presets/node-backend/baseline"
|
|
830
|
+
],
|
|
831
|
+
contracts: true,
|
|
832
|
+
available: true,
|
|
833
|
+
vars: () => ({
|
|
834
|
+
contractPath: "contracts/api/openapi.yaml",
|
|
835
|
+
contractTypesOutput: "src/contracts/openapi.ts",
|
|
836
|
+
contractTypesImport: "./openapi.js",
|
|
837
|
+
contractPathFromConfig: "../contracts/api/openapi.yaml",
|
|
838
|
+
appRoot: ""
|
|
839
|
+
})
|
|
840
|
+
},
|
|
841
|
+
"node-frontend": {
|
|
842
|
+
id: "node-frontend",
|
|
843
|
+
label: "Node.js frontend",
|
|
844
|
+
description: "Vite + TypeScript, platform CSS rules, composition root, harness; no API contract",
|
|
845
|
+
groups: [
|
|
846
|
+
"base",
|
|
847
|
+
"harness",
|
|
848
|
+
{ group: "presets/node-frontend/sample", onlyWhenEmpty: true },
|
|
849
|
+
"presets/node-frontend/baseline"
|
|
850
|
+
],
|
|
851
|
+
contracts: false,
|
|
852
|
+
available: true,
|
|
853
|
+
vars: () => ({
|
|
854
|
+
contractPath: "",
|
|
855
|
+
contractTypesOutput: ""
|
|
856
|
+
})
|
|
857
|
+
},
|
|
858
|
+
"node-library": {
|
|
859
|
+
id: "node-library",
|
|
860
|
+
label: "Node.js library or CLI",
|
|
861
|
+
description: "TypeScript package with no HTTP contract: architecture policy, composition models, harness",
|
|
862
|
+
groups: ["base", "harness"],
|
|
863
|
+
contracts: false,
|
|
864
|
+
available: true,
|
|
865
|
+
vars: () => ({
|
|
866
|
+
contractPath: "",
|
|
867
|
+
contractTypesOutput: ""
|
|
868
|
+
})
|
|
869
|
+
},
|
|
870
|
+
"monorepo": {
|
|
871
|
+
id: "monorepo",
|
|
872
|
+
label: "pnpm monorepo",
|
|
873
|
+
description: "apps/* + packages/*, catalog:, contract types in packages/shared, dependency policy in lint",
|
|
874
|
+
groups: [
|
|
875
|
+
"base",
|
|
876
|
+
"harness",
|
|
877
|
+
HTTP_CONTRACT,
|
|
878
|
+
{ group: EXPRESS_APP, into: "apps/api", onlyWhenEmpty: true },
|
|
879
|
+
{ group: EXPRESS_REPO, onlyWhenEmpty: true },
|
|
880
|
+
{ group: "presets/monorepo/sample", onlyWhenEmpty: true },
|
|
881
|
+
"presets/monorepo/baseline"
|
|
882
|
+
],
|
|
883
|
+
contracts: true,
|
|
884
|
+
available: true,
|
|
885
|
+
vars: (report, projectName) => {
|
|
886
|
+
const detected = report.workspacePackages;
|
|
887
|
+
const packages = detected.length > 0 ? detected : sampleWorkspace(`@${projectName}`);
|
|
888
|
+
return {
|
|
889
|
+
contractPath: "contracts/api/openapi.yaml",
|
|
890
|
+
contractTypesOutput: "packages/shared/src/api/openapi.ts",
|
|
891
|
+
contractTypesImport: `@${projectName}/shared`,
|
|
892
|
+
contractPathFromConfig: "../../../contracts/api/openapi.yaml",
|
|
893
|
+
appRoot: "apps/api/",
|
|
894
|
+
...renderWorkspacePolicy(packages, detected.length === 0)
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
var PRESET_IDS = Object.keys(PRESETS);
|
|
900
|
+
var PRESET_LIST = Object.values(PRESETS);
|
|
901
|
+
function isPresetId(value) {
|
|
902
|
+
return value in PRESETS;
|
|
903
|
+
}
|
|
904
|
+
function getPreset(id) {
|
|
905
|
+
return PRESETS[id];
|
|
906
|
+
}
|
|
907
|
+
function aiGroups(target) {
|
|
908
|
+
return target === "both" ? ["ai/shared", "ai/claude", "ai/cursor"] : ["ai/shared", `ai/${target}`];
|
|
909
|
+
}
|
|
910
|
+
var DEFAULT_REVIEW_MODEL = "claude-sonnet-5";
|
|
911
|
+
function reviewGroups(provider) {
|
|
912
|
+
return provider === "claude" ? ["ai/review"] : [];
|
|
913
|
+
}
|
|
914
|
+
function defaultProjectName(dir) {
|
|
915
|
+
const base = dir.split(/[\\/]/).filter(Boolean).at(-1) ?? "project";
|
|
916
|
+
return base.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "") || "project";
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// src/ui/prompts.ts
|
|
920
|
+
import { cancel, confirm, isCancel, multiselect, select, text } from "@clack/prompts";
|
|
921
|
+
var PROJECT_NAME_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
|
|
922
|
+
function isValidProjectName(value) {
|
|
923
|
+
return PROJECT_NAME_PATTERN.test(value);
|
|
924
|
+
}
|
|
925
|
+
function toAiTarget(selected) {
|
|
926
|
+
if (selected.includes("claude") && selected.includes("cursor"))
|
|
927
|
+
return "both";
|
|
928
|
+
return selected.includes("cursor") ? "cursor" : "claude";
|
|
929
|
+
}
|
|
930
|
+
function fromAiTarget(target) {
|
|
931
|
+
return target === "both" ? ["claude", "cursor"] : [target];
|
|
932
|
+
}
|
|
933
|
+
function createClackPrompter(lore, streams = {}) {
|
|
934
|
+
const settle = (value) => {
|
|
935
|
+
if (isCancel(value)) {
|
|
936
|
+
cancel(lore.cancelled, streams);
|
|
937
|
+
return void 0;
|
|
938
|
+
}
|
|
939
|
+
return value;
|
|
940
|
+
};
|
|
941
|
+
return {
|
|
942
|
+
async preset(choices, initial) {
|
|
943
|
+
const answer = await select({
|
|
944
|
+
...streams,
|
|
945
|
+
message: lore.askPreset,
|
|
946
|
+
options: choices.map((choice) => ({
|
|
947
|
+
value: choice.id,
|
|
948
|
+
label: choice.label,
|
|
949
|
+
hint: choice.available ? choice.description : lore.presetUnavailable,
|
|
950
|
+
disabled: !choice.available
|
|
951
|
+
})),
|
|
952
|
+
initialValue: initial
|
|
953
|
+
});
|
|
954
|
+
return settle(answer);
|
|
955
|
+
},
|
|
956
|
+
async aiTarget(initial) {
|
|
957
|
+
const answer = await multiselect({
|
|
958
|
+
...streams,
|
|
959
|
+
message: lore.askAi,
|
|
960
|
+
options: [
|
|
961
|
+
{ value: "claude", label: AI_TARGET_LABELS.claude },
|
|
962
|
+
{ value: "cursor", label: AI_TARGET_LABELS.cursor }
|
|
963
|
+
],
|
|
964
|
+
initialValues: fromAiTarget(initial),
|
|
965
|
+
required: true
|
|
966
|
+
});
|
|
967
|
+
const selected = settle(answer);
|
|
968
|
+
return selected == null ? void 0 : toAiTarget(selected);
|
|
969
|
+
},
|
|
970
|
+
async projectName(initial) {
|
|
971
|
+
const answer = await text({
|
|
972
|
+
...streams,
|
|
973
|
+
message: lore.askName,
|
|
974
|
+
initialValue: initial,
|
|
975
|
+
validate: (value2) => isValidProjectName(value2 ?? "") ? void 0 : lore.nameInvalid
|
|
976
|
+
});
|
|
977
|
+
const value = settle(answer);
|
|
978
|
+
return value?.trim();
|
|
979
|
+
},
|
|
980
|
+
async review(initial) {
|
|
981
|
+
return settle(await confirm({ ...streams, message: lore.askReview, initialValue: initial }));
|
|
982
|
+
},
|
|
983
|
+
async confirm(message) {
|
|
984
|
+
return settle(await confirm({ ...streams, message, initialValue: true }));
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// src/version.ts
|
|
990
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
991
|
+
import path11 from "path";
|
|
992
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
993
|
+
var HERE2 = path11.dirname(fileURLToPath2(import.meta.url));
|
|
994
|
+
function readVersion() {
|
|
995
|
+
for (const candidate of ["../package.json", "../../package.json"]) {
|
|
996
|
+
try {
|
|
997
|
+
const parsed = JSON.parse(readFileSync7(path11.resolve(HERE2, candidate), "utf8"));
|
|
998
|
+
if (parsed.name === "mikoshi-construct" && parsed.version != null)
|
|
999
|
+
return parsed.version;
|
|
1000
|
+
} catch {
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
return "0.0.0";
|
|
1004
|
+
}
|
|
1005
|
+
var VERSION = readVersion();
|
|
1006
|
+
|
|
1007
|
+
// src/commands/soulkill.ts
|
|
1008
|
+
function yesNo(value) {
|
|
1009
|
+
return value ? "yes" : "no";
|
|
1010
|
+
}
|
|
1011
|
+
function printDetectReport(ui2, report) {
|
|
1012
|
+
const layout = report.layout === "monorepo" && report.monorepoTools.length > 0 ? `monorepo (${report.monorepoTools.join(", ")})` : report.layout;
|
|
1013
|
+
ui2.tree([
|
|
1014
|
+
["Directory", report.dir],
|
|
1015
|
+
["Runtime", `Node.js ${report.nodeMajor}`],
|
|
1016
|
+
["Package manager", report.pnpmVersion == null ? report.packageManager : `${report.packageManager} (pnpm ${report.pnpmVersion} installed)`],
|
|
1017
|
+
["Layout", layout],
|
|
1018
|
+
["Workspace dirs", report.workspaceDirs.length > 0 ? `${report.workspaceDirs.join(", ")} (${report.workspacePackages.length} packages)` : "none"],
|
|
1019
|
+
["src/", yesNo(report.hasSrc)],
|
|
1020
|
+
["Contracts", report.existing.openapi ?? "none"],
|
|
1021
|
+
["tsconfig / ESLint config", `${yesNo(report.existing.tsconfig)} / ${yesNo(report.existing.eslintConfig)}`],
|
|
1022
|
+
["GitHub workflows", yesNo(report.existing.githubWorkflows)],
|
|
1023
|
+
["CLAUDE.md / AGENTS.md / .cursor/rules", `${yesNo(report.existing.claudeMd)} / ${yesNo(report.existing.agentsMd)} / ${yesNo(report.existing.cursorRules)}`],
|
|
1024
|
+
["construct.json", yesNo(report.existing.constructJson)]
|
|
1025
|
+
]);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// src/commands/init.ts
|
|
1029
|
+
var CANCELLED = /* @__PURE__ */ Symbol("cancelled");
|
|
1030
|
+
function aborted(skipped = [], conflicts = []) {
|
|
1031
|
+
return { status: "aborted", written: [], skipped, conflicts };
|
|
1032
|
+
}
|
|
1033
|
+
function suggestedPreset(report) {
|
|
1034
|
+
if (report.layout === "unknown")
|
|
1035
|
+
return void 0;
|
|
1036
|
+
return report.layout === "monorepo" ? "monorepo" : "node-backend";
|
|
1037
|
+
}
|
|
1038
|
+
function parsePreset(value) {
|
|
1039
|
+
if (!isPresetId(value))
|
|
1040
|
+
throw new Error(`unknown preset "${value}"`);
|
|
1041
|
+
return value;
|
|
1042
|
+
}
|
|
1043
|
+
function parseAi(value) {
|
|
1044
|
+
if (value === "claude" || value === "cursor" || value === "both")
|
|
1045
|
+
return value;
|
|
1046
|
+
throw new Error(`unknown AI target "${value}" (claude | cursor | both)`);
|
|
1047
|
+
}
|
|
1048
|
+
function parseReview(value) {
|
|
1049
|
+
if (value === "claude" || value === "none")
|
|
1050
|
+
return value;
|
|
1051
|
+
throw new Error(`unknown review provider "${value}" (claude | none)`);
|
|
1052
|
+
}
|
|
1053
|
+
function parseProjectName(value) {
|
|
1054
|
+
if (!isValidProjectName(value))
|
|
1055
|
+
throw new Error(`invalid project name "${value}"`);
|
|
1056
|
+
return value;
|
|
1057
|
+
}
|
|
1058
|
+
async function askChoices(ui2, options, report, root, prompter) {
|
|
1059
|
+
const suggested = suggestedPreset(report);
|
|
1060
|
+
let presetId = options.preset == null ? void 0 : parsePreset(options.preset);
|
|
1061
|
+
if (presetId == null) {
|
|
1062
|
+
if (report.layout === "unknown")
|
|
1063
|
+
ui2.glitch(ui2.lore.unknownStructure);
|
|
1064
|
+
if (prompter == null) {
|
|
1065
|
+
if (suggested == null)
|
|
1066
|
+
return CANCELLED;
|
|
1067
|
+
presetId = suggested;
|
|
1068
|
+
} else {
|
|
1069
|
+
presetId = await prompter.preset(PRESET_LIST, suggested);
|
|
1070
|
+
if (presetId == null)
|
|
1071
|
+
return CANCELLED;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
let ai = options.ai == null ? void 0 : parseAi(options.ai);
|
|
1075
|
+
if (ai == null) {
|
|
1076
|
+
ai = prompter == null ? "claude" : await prompter.aiTarget("claude");
|
|
1077
|
+
if (ai == null)
|
|
1078
|
+
return CANCELLED;
|
|
1079
|
+
}
|
|
1080
|
+
let projectName = options.name == null ? void 0 : parseProjectName(options.name);
|
|
1081
|
+
if (projectName == null) {
|
|
1082
|
+
const fallback = defaultProjectName(root);
|
|
1083
|
+
projectName = prompter == null ? fallback : await prompter.projectName(fallback);
|
|
1084
|
+
if (projectName == null)
|
|
1085
|
+
return CANCELLED;
|
|
1086
|
+
}
|
|
1087
|
+
let review = options.review == null ? void 0 : parseReview(options.review);
|
|
1088
|
+
if (review == null) {
|
|
1089
|
+
const wanted = prompter == null ? false : await prompter.review(false);
|
|
1090
|
+
if (wanted == null)
|
|
1091
|
+
return CANCELLED;
|
|
1092
|
+
review = wanted ? "claude" : "none";
|
|
1093
|
+
}
|
|
1094
|
+
return { presetId, ai, projectName, review };
|
|
1095
|
+
}
|
|
1096
|
+
async function runInit(ui2, options, prompter) {
|
|
1097
|
+
const root = path12.resolve(options.dir);
|
|
1098
|
+
mkdirSync2(root, { recursive: true });
|
|
1099
|
+
if (!options.yes && prompter == null) {
|
|
1100
|
+
ui2.glitch(ui2.lore.needsTerminal);
|
|
1101
|
+
return aborted();
|
|
1102
|
+
}
|
|
1103
|
+
const interactive = options.yes ? void 0 : prompter;
|
|
1104
|
+
ui2.soulkiller();
|
|
1105
|
+
ui2.line(` ${ui2.theme.dim("\u2514\u2500")} ${ui2.theme.dim(ui2.lore.soulkillerDetail)}`);
|
|
1106
|
+
ui2.line();
|
|
1107
|
+
const report = detect(root);
|
|
1108
|
+
ui2.phase(1, 4, "\u26A1", ui2.lore.phaseScan);
|
|
1109
|
+
printDetectReport(ui2, report);
|
|
1110
|
+
ui2.line();
|
|
1111
|
+
ui2.phase(2, 4, "\u{1F9E0}", ui2.lore.phaseConfigure);
|
|
1112
|
+
const choices = await askChoices(ui2, options, report, root, interactive);
|
|
1113
|
+
if (choices === CANCELLED)
|
|
1114
|
+
return aborted();
|
|
1115
|
+
const { presetId, ai, projectName, review } = choices;
|
|
1116
|
+
const preset = getPreset(presetId);
|
|
1117
|
+
if (!preset.available)
|
|
1118
|
+
throw new Error(`preset "${presetId}" is not available yet in v${VERSION}`);
|
|
1119
|
+
if (report.packageManager !== "pnpm" && report.packageManager !== "none")
|
|
1120
|
+
ui2.glitch(`This repository uses ${report.packageManager}; the construct harness scripts assume pnpm in v${VERSION}.`);
|
|
1121
|
+
ui2.tree([
|
|
1122
|
+
["Project", projectName],
|
|
1123
|
+
["Preset", `${preset.label} \u2014 ${preset.description}`],
|
|
1124
|
+
["AI Netrunners", AI_TARGET_LABELS[ai]],
|
|
1125
|
+
["Code review", review === "claude" ? `Claude on pull requests (${options.reviewModel ?? DEFAULT_REVIEW_MODEL})` : "none"]
|
|
1126
|
+
]);
|
|
1127
|
+
ui2.line();
|
|
1128
|
+
const vars = {
|
|
1129
|
+
projectName,
|
|
1130
|
+
scope: `@${projectName}`,
|
|
1131
|
+
nodeMajor: String(report.nodeMajor),
|
|
1132
|
+
contracts: preset.contracts ? "true" : "false",
|
|
1133
|
+
contractPath: "contracts/api/openapi.yaml",
|
|
1134
|
+
contractTypesOutput: "src/contracts/openapi.ts",
|
|
1135
|
+
compositionDir: report.existing.compositionDir ?? "architecture/composition",
|
|
1136
|
+
harnessCommand: "pnpm run quality",
|
|
1137
|
+
packageManager: "pnpm",
|
|
1138
|
+
pnpmVersion: report.pnpmVersion ?? "",
|
|
1139
|
+
reviewModel: options.reviewModel ?? DEFAULT_REVIEW_MODEL,
|
|
1140
|
+
constructVersion: VERSION,
|
|
1141
|
+
...preset.vars(report, projectName)
|
|
1142
|
+
};
|
|
1143
|
+
const groups = [...preset.groups, ...aiGroups(ai), ...reviewGroups(review)];
|
|
1144
|
+
const plan = planMaterialize(root, groups, vars, { emptyTarget: report.layout === "empty", ai });
|
|
1145
|
+
ui2.phase(3, 4, "\u{1F4BE}", ui2.lore.phaseMaterialize);
|
|
1146
|
+
ui2.tree([[ui2.lore.materializeAi], [ui2.lore.materializeContracts], [ui2.lore.materializePolicies]]);
|
|
1147
|
+
ui2.line();
|
|
1148
|
+
const creates = plan.ops.filter((op) => op.action === "create");
|
|
1149
|
+
const merges = plan.ops.filter((op) => op.action === "merge" || op.action === "append");
|
|
1150
|
+
const skips = plan.ops.filter((op) => op.action === "skip");
|
|
1151
|
+
const skipped = skips.map((op) => op.target);
|
|
1152
|
+
for (const op of creates)
|
|
1153
|
+
ui2.line(` ${ui2.theme.ok("+")} ${op.target}`);
|
|
1154
|
+
for (const op of merges)
|
|
1155
|
+
ui2.line(` ${ui2.theme.accent("~")} ${op.target} ${ui2.theme.dim(`(${op.action})`)}`);
|
|
1156
|
+
for (const op of skips)
|
|
1157
|
+
ui2.line(` ${ui2.theme.dim("=")} ${ui2.theme.dim(`${op.target} \u2014 ${op.note ?? "exists"}`)}`);
|
|
1158
|
+
ui2.line();
|
|
1159
|
+
if (plan.omittedGroups.length > 0)
|
|
1160
|
+
ui2.line(ui2.theme.dim(` ${ui2.lore.sampleOmitted}`));
|
|
1161
|
+
if (report.existing.compositionDir != null && report.existing.compositionDir !== "architecture/composition")
|
|
1162
|
+
ui2.line(ui2.theme.dim(` Existing composition models found at ${report.existing.compositionDir}/ \u2014 kept there, not moved.`));
|
|
1163
|
+
if (plan.conflicts.length > 0)
|
|
1164
|
+
ui2.glitch("Existing values kept; review these keys by hand:", plan.conflicts);
|
|
1165
|
+
if (skips.length > 0 && !report.existing.constructJson)
|
|
1166
|
+
ui2.glitch(ui2.lore.wireHarness, ui2.lore.wireHarnessSteps);
|
|
1167
|
+
if (options.dryRun) {
|
|
1168
|
+
ui2.line(ui2.theme.dim(ui2.lore.dryRun));
|
|
1169
|
+
return { status: "dry-run", written: [], skipped, conflicts: plan.conflicts };
|
|
1170
|
+
}
|
|
1171
|
+
if (interactive != null && await interactive.confirm(ui2.lore.confirm) !== true)
|
|
1172
|
+
return aborted(skipped, plan.conflicts);
|
|
1173
|
+
const written = applyPlan(root, plan.ops);
|
|
1174
|
+
writeManifest(root, buildManifest({ version: VERSION, preset: presetId, ai, review, vars, written, contracts: preset.contracts }));
|
|
1175
|
+
ui2.phase(4, 4, "\u2705", ui2.lore.phaseOnline);
|
|
1176
|
+
ui2.tree([
|
|
1177
|
+
["Written", `${written.length} files`],
|
|
1178
|
+
["Next", `${vars.packageManager} install && ${vars.harnessCommand}`],
|
|
1179
|
+
["Then", ai === "cursor" ? "open Cursor and ask the agent to run the construct discovery" : "claude \u2192 /construct-discover"]
|
|
1180
|
+
]);
|
|
1181
|
+
return { status: "done", written: written.map((op) => op.target), skipped, conflicts: plan.conflicts };
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// src/ui/console.ts
|
|
1185
|
+
import process2 from "process";
|
|
1186
|
+
|
|
1187
|
+
// src/ui/lore.ts
|
|
1188
|
+
var BANNER = String.raw`
|
|
1189
|
+
███╗ ███╗██╗██╗ ██╗ ██████╗ ███████╗██╗ ██╗██╗
|
|
1190
|
+
████╗ ████║██║██║ ██╔╝██╔═══██╗██╔════╝██║ ██║██║
|
|
1191
|
+
██╔████╔██║██║█████╔╝ ██║ ██║███████╗███████║██║
|
|
1192
|
+
██║╚██╔╝██║██║██╔═██╗ ██║ ██║╚════██║██╔══██║██║
|
|
1193
|
+
██║ ╚═╝ ██║██║██║ ██╗╚██████╔╝███████║██║ ██║██║
|
|
1194
|
+
╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝`;
|
|
1195
|
+
var LORE = {
|
|
1196
|
+
subtitle: (version) => `--- CONSTRUCT ENGINE v${version} // ARASAKA SUB-NET ---`,
|
|
1197
|
+
johnnyWakeUp: "Wake up, Netrunner. We have a repository to build.",
|
|
1198
|
+
soulkiller: "RUNNING SOULKILLER PROTOCOL...",
|
|
1199
|
+
soulkillerDetail: "Extracting codebase consciousness...",
|
|
1200
|
+
phaseScan: "REPOSITORY SCAN COMPLETE",
|
|
1201
|
+
phaseConfigure: "CONSTRUCT CONFIGURATION",
|
|
1202
|
+
phaseMaterialize: "MATERIALIZING CONSTRUCT",
|
|
1203
|
+
phaseOnline: "CONSTRUCT ONLINE",
|
|
1204
|
+
materializeAi: "Injecting instruction sets for AI Netrunners...",
|
|
1205
|
+
materializeContracts: "Generating architecture contracts...",
|
|
1206
|
+
materializePolicies: "Locking ESLint & Security policies...",
|
|
1207
|
+
askPreset: "Which construct are we building?",
|
|
1208
|
+
askAi: "Which Netrunners will jack in?",
|
|
1209
|
+
askName: "Project name",
|
|
1210
|
+
askReview: "Add the Claude code-review workflow on pull requests? (label-triggered, needs a CODE_REVIEW_API_KEY secret)",
|
|
1211
|
+
presetUnavailable: "not materialized yet in this version",
|
|
1212
|
+
nameInvalid: 'lowercase letters, digits, "-", "." and "_" only',
|
|
1213
|
+
cancelled: "Netrunner jacked out. Nothing was written.",
|
|
1214
|
+
needsTerminal: "No terminal for the interactive flow; pass --yes (and --preset) to run non-interactively.",
|
|
1215
|
+
confirm: "Inject Construct into repository?",
|
|
1216
|
+
dryRun: "DRY RUN \u2014 nothing was written.",
|
|
1217
|
+
sampleOmitted: "Sample sources omitted: the directory is not empty. Discovery maps what is already here.",
|
|
1218
|
+
unknownStructure: "I don't recognize this structure. Choose a target directory with --dir.",
|
|
1219
|
+
glitch: "GLITCH",
|
|
1220
|
+
flatlined: "FLATLINED",
|
|
1221
|
+
stable: "CONSTRUCT STABLE",
|
|
1222
|
+
discoveryIncomplete: "Discovery incomplete.",
|
|
1223
|
+
wireHarness: "Existing configs were kept, so the harness is not wired in yet. /construct-discover does this first; by hand:",
|
|
1224
|
+
wireHarnessSteps: [
|
|
1225
|
+
"eslint: ignore scripts/construct/*.workflow.mjs (the ladder script uses top-level return)",
|
|
1226
|
+
"tsconfig: include scripts/**/*.ts; vitest: include scripts/tests/**/*.test.ts",
|
|
1227
|
+
"package.json: make the quality script run composition:check (and contracts:check when there is a contract)"
|
|
1228
|
+
]
|
|
1229
|
+
};
|
|
1230
|
+
var PLAIN_LORE = {
|
|
1231
|
+
subtitle: (version) => `mikoshi-construct v${version}`,
|
|
1232
|
+
johnnyWakeUp: "Wake up, Netrunner. We have a repository to build.",
|
|
1233
|
+
soulkiller: "Inspecting repository...",
|
|
1234
|
+
soulkillerDetail: "Detecting stack and layout...",
|
|
1235
|
+
phaseScan: "Repository scan complete",
|
|
1236
|
+
phaseConfigure: "Configuration",
|
|
1237
|
+
phaseMaterialize: "Writing baseline",
|
|
1238
|
+
phaseOnline: "Done",
|
|
1239
|
+
materializeAi: "Writing AI agent instructions...",
|
|
1240
|
+
materializeContracts: "Writing architecture and contracts...",
|
|
1241
|
+
materializePolicies: "Writing lint and security policies...",
|
|
1242
|
+
askPreset: "Preset",
|
|
1243
|
+
askAi: "AI agents",
|
|
1244
|
+
askName: "Project name",
|
|
1245
|
+
askReview: "Add the Claude code-review workflow on pull requests? (label-triggered, needs a CODE_REVIEW_API_KEY secret)",
|
|
1246
|
+
presetUnavailable: "not available yet in this version",
|
|
1247
|
+
nameInvalid: 'lowercase letters, digits, "-", "." and "_" only',
|
|
1248
|
+
cancelled: "Cancelled. Nothing was written.",
|
|
1249
|
+
needsTerminal: "No terminal for the interactive flow; pass --yes (and --preset) to run non-interactively.",
|
|
1250
|
+
confirm: "Write these files?",
|
|
1251
|
+
dryRun: "Dry run \u2014 nothing was written.",
|
|
1252
|
+
sampleOmitted: "Sample sources omitted: the directory is not empty. Discovery maps what is already here.",
|
|
1253
|
+
unknownStructure: "Unrecognized project structure. Choose a target directory with --dir.",
|
|
1254
|
+
glitch: "WARNING",
|
|
1255
|
+
flatlined: "ERROR",
|
|
1256
|
+
stable: "OK",
|
|
1257
|
+
discoveryIncomplete: "Discovery incomplete.",
|
|
1258
|
+
wireHarness: "Existing configs were kept, so the harness is not wired in yet. /construct-discover does this first; by hand:",
|
|
1259
|
+
wireHarnessSteps: [
|
|
1260
|
+
"eslint: ignore scripts/construct/*.workflow.mjs (the ladder script uses top-level return)",
|
|
1261
|
+
"tsconfig: include scripts/**/*.ts; vitest: include scripts/tests/**/*.test.ts",
|
|
1262
|
+
"package.json: make the quality script run composition:check (and contracts:check when there is a contract)"
|
|
1263
|
+
]
|
|
1264
|
+
};
|
|
1265
|
+
|
|
1266
|
+
// src/ui/console.ts
|
|
1267
|
+
var stdoutWriter = (text2) => {
|
|
1268
|
+
process2.stdout.write(text2);
|
|
1269
|
+
};
|
|
1270
|
+
function createUi(theme, write = stdoutWriter) {
|
|
1271
|
+
const plain = theme.name === "plain";
|
|
1272
|
+
const lore = plain ? PLAIN_LORE : LORE;
|
|
1273
|
+
const out = (text2 = "") => {
|
|
1274
|
+
write(`${text2}
|
|
1275
|
+
`);
|
|
1276
|
+
};
|
|
1277
|
+
const icon = (glyph, fallback) => plain ? fallback : glyph;
|
|
1278
|
+
return {
|
|
1279
|
+
theme,
|
|
1280
|
+
lore,
|
|
1281
|
+
banner(version, johnny2) {
|
|
1282
|
+
if (plain) {
|
|
1283
|
+
out(lore.subtitle(version));
|
|
1284
|
+
out();
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
if (johnny2) {
|
|
1288
|
+
out();
|
|
1289
|
+
out(theme.accent(theme.bold(lore.johnnyWakeUp)));
|
|
1290
|
+
out(theme.dim(lore.subtitle(version)));
|
|
1291
|
+
out();
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
out(theme.primary(BANNER));
|
|
1295
|
+
out(theme.accent(` ${lore.subtitle(version)}`));
|
|
1296
|
+
out();
|
|
1297
|
+
},
|
|
1298
|
+
soulkiller() {
|
|
1299
|
+
out(`${icon("\u{1F480}", ">>")} ${theme.accent(lore.soulkiller)}`);
|
|
1300
|
+
},
|
|
1301
|
+
phase(index, total, glyph, title) {
|
|
1302
|
+
out(`${theme.primary(`[${index}/${total}]`)} ${icon(glyph, "*")} ${theme.accent(theme.bold(title))}`);
|
|
1303
|
+
},
|
|
1304
|
+
tree(lines) {
|
|
1305
|
+
lines.forEach(([label, value], position) => {
|
|
1306
|
+
const branch = position === lines.length - 1 ? "\u2514\u2500" : "\u251C\u2500";
|
|
1307
|
+
const rendered = value == null ? label : `${label}: ${theme.detected(value)}`;
|
|
1308
|
+
out(` ${theme.dim(branch)} ${rendered}`);
|
|
1309
|
+
});
|
|
1310
|
+
},
|
|
1311
|
+
line: out,
|
|
1312
|
+
ok(text2) {
|
|
1313
|
+
out(`${icon("\u2705", "[ok]")} ${theme.ok(theme.bold(text2))}`);
|
|
1314
|
+
},
|
|
1315
|
+
glitch(text2, details = []) {
|
|
1316
|
+
out(`${icon("\u26A0", "[warn]")} ${theme.warn(`${lore.glitch}:`)} ${text2}`);
|
|
1317
|
+
for (const detail of details)
|
|
1318
|
+
out(` ${theme.dim(detail)}`);
|
|
1319
|
+
},
|
|
1320
|
+
flatline(text2) {
|
|
1321
|
+
out(`${icon("\u2620", "[error]")} ${theme.fail(`${lore.flatlined}:`)} ${text2}`);
|
|
1322
|
+
}
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// src/ui/theme.ts
|
|
1327
|
+
import process3 from "process";
|
|
1328
|
+
import pc from "picocolors";
|
|
1329
|
+
function rgb(r, g, b) {
|
|
1330
|
+
return (text2) => `\x1B[38;2;${r};${g};${b}m${text2}\x1B[39m`;
|
|
1331
|
+
}
|
|
1332
|
+
var identity = (text2) => text2;
|
|
1333
|
+
function supportsColor() {
|
|
1334
|
+
if (process3.env.NO_COLOR != null && process3.env.NO_COLOR !== "")
|
|
1335
|
+
return false;
|
|
1336
|
+
if (process3.env.FORCE_COLOR != null && process3.env.FORCE_COLOR !== "0")
|
|
1337
|
+
return true;
|
|
1338
|
+
return pc.isColorSupported;
|
|
1339
|
+
}
|
|
1340
|
+
function truecolor() {
|
|
1341
|
+
const term = process3.env.COLORTERM ?? "";
|
|
1342
|
+
return term === "truecolor" || term === "24bit";
|
|
1343
|
+
}
|
|
1344
|
+
var PLAIN = {
|
|
1345
|
+
name: "plain",
|
|
1346
|
+
primary: identity,
|
|
1347
|
+
accent: identity,
|
|
1348
|
+
detected: identity,
|
|
1349
|
+
dim: identity,
|
|
1350
|
+
ok: identity,
|
|
1351
|
+
warn: identity,
|
|
1352
|
+
fail: identity,
|
|
1353
|
+
bold: identity
|
|
1354
|
+
};
|
|
1355
|
+
function arasaka() {
|
|
1356
|
+
const tc = truecolor();
|
|
1357
|
+
return {
|
|
1358
|
+
name: "arasaka",
|
|
1359
|
+
primary: tc ? rgb(230, 0, 46) : pc.red,
|
|
1360
|
+
accent: tc ? rgb(252, 238, 10) : pc.yellow,
|
|
1361
|
+
detected: tc ? rgb(0, 240, 255) : pc.cyan,
|
|
1362
|
+
dim: pc.dim,
|
|
1363
|
+
ok: tc ? rgb(0, 255, 159) : pc.green,
|
|
1364
|
+
warn: pc.yellow,
|
|
1365
|
+
fail: (text2) => pc.bold(pc.red(text2)),
|
|
1366
|
+
bold: pc.bold
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
function johnny() {
|
|
1370
|
+
const tc = truecolor();
|
|
1371
|
+
return {
|
|
1372
|
+
...arasaka(),
|
|
1373
|
+
name: "johnny",
|
|
1374
|
+
primary: tc ? rgb(255, 60, 0) : pc.red,
|
|
1375
|
+
accent: tc ? rgb(255, 176, 0) : pc.yellow,
|
|
1376
|
+
detected: tc ? rgb(255, 120, 60) : pc.magenta
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
function resolveTheme(options) {
|
|
1380
|
+
if (options.plain === true || !supportsColor())
|
|
1381
|
+
return PLAIN;
|
|
1382
|
+
return options.johnny === true ? johnny() : arasaka();
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// src/cli.ts
|
|
1386
|
+
var commonArgs = {
|
|
1387
|
+
dir: { type: "string", description: "Target directory", default: "." },
|
|
1388
|
+
plain: { type: "boolean", description: "No colors, no lore (CI-friendly)", default: false },
|
|
1389
|
+
johnny: { type: "boolean", description: "Wake up, Netrunner.", default: false }
|
|
1390
|
+
};
|
|
1391
|
+
function ui(args) {
|
|
1392
|
+
return createUi(resolveTheme({ plain: args.plain, johnny: args.johnny }));
|
|
1393
|
+
}
|
|
1394
|
+
var init = defineCommand({
|
|
1395
|
+
meta: { name: "init", description: "Materialize the construct: architecture, contracts, harness, AI instructions" },
|
|
1396
|
+
args: {
|
|
1397
|
+
...commonArgs,
|
|
1398
|
+
preset: { type: "string", description: `Preset: ${PRESET_IDS.join(" | ")}` },
|
|
1399
|
+
ai: { type: "string", description: "AI target: claude | cursor | both (default: claude)" },
|
|
1400
|
+
name: { type: "string", description: "Project name (defaults to the directory name)" },
|
|
1401
|
+
review: { type: "string", description: "AI code review on pull requests: claude | none (default: none)" },
|
|
1402
|
+
reviewModel: { type: "string", description: `Model for the review workflow (default: ${DEFAULT_REVIEW_MODEL})` },
|
|
1403
|
+
yes: { type: "boolean", alias: "y", description: "Non-interactive: take defaults and skip the confirmation", default: false },
|
|
1404
|
+
dryRun: { type: "boolean", description: "Print the plan, write nothing", default: false }
|
|
1405
|
+
},
|
|
1406
|
+
async run({ args }) {
|
|
1407
|
+
const console = ui(args);
|
|
1408
|
+
console.banner(VERSION, args.johnny);
|
|
1409
|
+
try {
|
|
1410
|
+
const prompter = isTTY(process4.stdout) && process4.stdin.isTTY === true ? createClackPrompter(console.lore) : void 0;
|
|
1411
|
+
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
|
+
process4.exitCode = result.status === "aborted" ? 1 : 0;
|
|
1413
|
+
} catch (error) {
|
|
1414
|
+
console.flatline(error instanceof Error ? error.message : String(error));
|
|
1415
|
+
process4.exitCode = 1;
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
});
|
|
1419
|
+
var soulkill = defineCommand({
|
|
1420
|
+
meta: { name: "soulkill", description: "Extract the facts about a repository without writing anything (alias: inspect, capture)" },
|
|
1421
|
+
args: {
|
|
1422
|
+
...commonArgs,
|
|
1423
|
+
json: { type: "boolean", description: "Machine-readable report", default: false }
|
|
1424
|
+
},
|
|
1425
|
+
run({ args }) {
|
|
1426
|
+
const report = detect(args.dir);
|
|
1427
|
+
if (args.json) {
|
|
1428
|
+
process4.stdout.write(`${JSON.stringify(report, null, 2)}
|
|
1429
|
+
`);
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
const console = ui(args);
|
|
1433
|
+
console.banner(VERSION, args.johnny);
|
|
1434
|
+
console.soulkiller();
|
|
1435
|
+
console.line();
|
|
1436
|
+
printDetectReport(console, report);
|
|
1437
|
+
}
|
|
1438
|
+
});
|
|
1439
|
+
var doctor = defineCommand({
|
|
1440
|
+
meta: { name: "doctor", description: "Check that the construct baseline and discovery are intact" },
|
|
1441
|
+
args: {
|
|
1442
|
+
...commonArgs,
|
|
1443
|
+
json: { type: "boolean", description: "Machine-readable report", default: false }
|
|
1444
|
+
},
|
|
1445
|
+
run({ args }) {
|
|
1446
|
+
const result = runDoctor(args.dir);
|
|
1447
|
+
if (args.json) {
|
|
1448
|
+
process4.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
1449
|
+
`);
|
|
1450
|
+
process4.exitCode = result?.ok === true ? 0 : 1;
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
const console = ui(args);
|
|
1454
|
+
process4.exitCode = printDoctor(console, result);
|
|
1455
|
+
}
|
|
1456
|
+
});
|
|
1457
|
+
var cost = defineCommand({
|
|
1458
|
+
meta: { name: "cost", description: "Token usage of the /implement runs recorded for this directory (from Claude Code session data)" },
|
|
1459
|
+
args: {
|
|
1460
|
+
...commonArgs,
|
|
1461
|
+
last: { type: "boolean", description: "Only the most recent run", default: false },
|
|
1462
|
+
json: { type: "boolean", description: "Machine-readable report", default: false }
|
|
1463
|
+
},
|
|
1464
|
+
run({ args }) {
|
|
1465
|
+
const runs = collectWorkflowRuns(path13.resolve(args.dir));
|
|
1466
|
+
if (args.json) {
|
|
1467
|
+
const selected = runs == null ? null : args.last ? runs.slice(-1) : runs;
|
|
1468
|
+
process4.stdout.write(`${JSON.stringify(selected, null, 2)}
|
|
1469
|
+
`);
|
|
1470
|
+
process4.exitCode = runs == null ? 1 : 0;
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
const console = ui(args);
|
|
1474
|
+
process4.exitCode = printCost(console, runs, args.last);
|
|
1475
|
+
}
|
|
1476
|
+
});
|
|
1477
|
+
var main = defineCommand({
|
|
1478
|
+
meta: {
|
|
1479
|
+
name: "construct",
|
|
1480
|
+
version: VERSION,
|
|
1481
|
+
description: "mikoshi-construct \u2014 bootstrap for AI-native software projects"
|
|
1482
|
+
},
|
|
1483
|
+
subCommands: {
|
|
1484
|
+
init,
|
|
1485
|
+
soulkill,
|
|
1486
|
+
inspect: soulkill,
|
|
1487
|
+
capture: soulkill,
|
|
1488
|
+
doctor,
|
|
1489
|
+
cost
|
|
1490
|
+
}
|
|
1491
|
+
});
|
|
1492
|
+
runMain(main);
|