@genn-inc/cluebase-cli 0.0.1

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.
Files changed (38) hide show
  1. package/README.md +101 -0
  2. package/bin/cluebase-cli.mjs +11 -0
  3. package/package.json +17 -0
  4. package/src/cli-command.mjs +515 -0
  5. package/src/cli-invocation.mjs +17 -0
  6. package/src/code-evidence-analyzer.mjs +2041 -0
  7. package/src/contracts.mjs +36 -0
  8. package/src/generated-code-evidence-contract.mjs +22 -0
  9. package/src/generated-sdk-version-contract.mjs +5 -0
  10. package/src/generated-source-path-policy.mjs +20 -0
  11. package/src/lifecycle-guard.mjs +202 -0
  12. package/src/path-policy.mjs +81 -0
  13. package/src/setup-ai-contract.mjs +221 -0
  14. package/src/setup-check-constants.mjs +110 -0
  15. package/src/setup-check-scan-a.mjs +849 -0
  16. package/src/setup-check-scan-b.mjs +994 -0
  17. package/src/setup-check.mjs +575 -0
  18. package/src/setup-discover-check.mjs +755 -0
  19. package/src/setup-doctor-deadline.mjs +221 -0
  20. package/src/setup-doctor-env.mjs +331 -0
  21. package/src/setup-doctor-file-boundary.mjs +426 -0
  22. package/src/setup-doctor-probe.mjs +719 -0
  23. package/src/setup-doctor-quality-checks-a.mjs +593 -0
  24. package/src/setup-doctor-quality-checks-b.mjs +638 -0
  25. package/src/setup-doctor-quality-shared.mjs +382 -0
  26. package/src/setup-doctor-quality.mjs +209 -0
  27. package/src/setup-doctor-route-scan.mjs +160 -0
  28. package/src/setup-doctor-sdk-probe.mjs +340 -0
  29. package/src/setup-doctor.mjs +545 -0
  30. package/src/setup-documents.mjs +112 -0
  31. package/src/setup-help.mjs +130 -0
  32. package/src/setup-prepare.mjs +360 -0
  33. package/src/setup-repository-discovery.mjs +764 -0
  34. package/src/setup-step-builders-discover.mjs +701 -0
  35. package/src/setup-step-builders-events.mjs +229 -0
  36. package/src/setup-step-builders-implement.mjs +710 -0
  37. package/src/setup-step-commands.mjs +427 -0
  38. package/src/setup-tool.mjs +27 -0
package/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # Cluebase CLI
2
+
3
+ This package owns the Cluebase setup CLI.
4
+
5
+ The Cluebase product repository keeps SDKs, shared schemas, and API contracts. Tool implementation lives here so client repositories do not receive tool source code.
6
+
7
+ ## Commands
8
+
9
+ The public npm package is `@genn-inc/cluebase-cli`. The binary exposed by that package is
10
+ `cluebase-ai`, but first-run setup should not assume a global `cluebase-ai` install.
11
+ Use `npx -y @genn-inc/cluebase-cli <command>` unless `.cluebase/setup-manifest.json`
12
+ explicitly provides a different invocation.
13
+
14
+ ```bash
15
+ npx -y @genn-inc/cluebase-cli setup --cluebase-api-key <ak_dev_or_prod_key> --cluebase-api-base-url <cluebase-api-base-url> --project-key <pk_dev_or_prod_key>
16
+ npx -y @genn-inc/cluebase-cli setup-discover-check --manifest .cluebase/setup-manifest.json --discoveries .cluebase/discoveries.json
17
+ npx -y @genn-inc/cluebase-cli setup-check --framework fastapi --backend-root-path backend --repo . --require-sdk-lifecycle
18
+ npx -y @genn-inc/cluebase-cli setup-doctor --local
19
+ ```
20
+
21
+ `npx -y @genn-inc/cluebase-cli setup` installs the target AI-tool skills and, when any
22
+ frontend or backend service is detectable, also runs the machine-owned setup
23
+ preparation:
24
+
25
+ - detects frontend services (React/Vite, Next.js, Vue, Angular, SvelteKit, Nuxt)
26
+ and backend services (Python: FastAPI/Django/Flask and other WSGI/ASGI apps;
27
+ Node: Express/NestJS/Fastify/Koa) from dependency manifests and framework
28
+ import signals — language, framework, and service root only
29
+ - writes `.cluebase/setup-manifest.json`
30
+ - defers service-specific runtime env guidance to the Cluebase setup screen Step 2;
31
+ the CLI does not write `.env.cluebase`
32
+
33
+ A frontend-only project (single-page app plus serverless/static hosting, with no
34
+ detectable backend) is a supported setup path and continues as a frontend SDK
35
+ integration rather than being blocked. Setup is blocked only when no frontend or
36
+ backend service is detected at all.
37
+
38
+ Route and lifecycle-boundary inventory is not enumerated by a per-framework
39
+ parser. It is discovered semantically by the AI setup STEP commands
40
+ (`cluebase-discover` and its review/context passes) reading the customer codebase,
41
+ so unsupported frameworks and unknown routes degrade gracefully instead of
42
+ blocking setup.
43
+
44
+ ## Universal HTTP ingest contract
45
+
46
+ The Cluebase SDKs are a convenience over three stable HTTP endpoints. Any stack that
47
+ has no matching Cluebase SDK can integrate by calling these endpoints directly, which
48
+ is the universal fallback setup path:
49
+
50
+ - `POST /api/v1/ingest/browser-tokens` — the browser obtains a short-lived token
51
+ from the Cluebase backend directly, authenticated by the public project key and the
52
+ request `Origin`. No customer-backend proxy route.
53
+ - `POST /api/v1/ingest/browser` — the browser sends canonical observation source
54
+ event batches with the short-lived token in `x-cluebase-browser-token`.
55
+ - `POST /api/v1/ingest/backend` — the customer backend sends server-side batches
56
+ with `CLUEBASE_API_KEY` in `x-cluebase-api-key`.
57
+
58
+ The Cluebase environment (dev/prod) is derived from the project key prefix
59
+ (`pk_dev_` vs `pk_prod_`); no environment variable is passed. Use this contract
60
+ directly for any language or framework without a Cluebase SDK; do not add Cluebase proxy
61
+ routes under the customer backend.
62
+
63
+ `npx -y @genn-inc/cluebase-cli setup-check` mechanically verifies installed setup skills,
64
+ obvious secret leaks, and SDK lifecycle presence when requested. With
65
+ `--require-sdk-lifecycle`, a passing result is still static only; dependency
66
+ installation, SDK imports in the target environments, app startup, and event
67
+ delivery remain required before setup can be called complete.
68
+
69
+ `npx -y @genn-inc/cluebase-cli setup-doctor --local` checks API connectivity before
70
+ user-operated lifecycle verification. It verifies three setup hops:
71
+
72
+ 1. frontend SDK to Cluebase `/api/v1/ingest/browser-tokens` (short-lived token
73
+ issuance — the frontend SDK calls the Cluebase backend directly using the
74
+ public project key and request Origin, no customer-backend proxy in between).
75
+ 2. customer frontend to Cluebase canonical browser observation batch ingest with
76
+ the short-lived token.
77
+ 3. customer backend to Cluebase `/api/v1/ingest/backend` (server-side ingest
78
+ with `CLUEBASE_API_KEY`).
79
+
80
+ It also scans the customer backend for `/api/v1/cluebase/*` proxy routes and
81
+ emits a blocking error requiring removal (`customer_backend_cluebase_route_forbidden`).
82
+ Setup-doctor does not replace real login, organization, or logout flows; confirm
83
+ those flows from the Cluebase setup screen or published batch evidence after operating the
84
+ local customer frontend/backend.
85
+
86
+ `npx -y @genn-inc/cluebase-cli setup` reads the Cluebase API base URL, project key, and API
87
+ key from setup screen flags, detects local services, writes
88
+ `.cluebase/setup-manifest.json`, and points runtime env setup to the Cluebase setup
89
+ screen Step 2.
90
+
91
+ ## Required Environment
92
+
93
+ - `CLUEBASE_API_KEY`: Cluebase setup screen issues this value.
94
+ - `CLUEBASE_PROJECT_KEY`: Cluebase setup screen issues this value.
95
+ - `CLUEBASE_API_BASE_URL`: Cluebase API base URL shown by the setup screen.
96
+
97
+ ## Boundaries
98
+
99
+ - The tool may read allowed source paths in the client repository.
100
+ - The tool must not read `.env`, secrets, logs, dumps, build output, or vendor directories.
101
+ - Raw source code, raw SQL, bind values, function names, class names, file paths, and import graphs must not be sent to Cluebase.
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from "../src/cli-command.mjs";
3
+
4
+ process.stdin.unref?.();
5
+
6
+ runCli(process.argv.slice(2)).catch((error) => {
7
+ process.stderr.write(
8
+ `${error instanceof Error ? error.message : String(error)}\n`,
9
+ );
10
+ process.exitCode = 1;
11
+ });
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@genn-inc/cluebase-cli",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "bin": {
6
+ "cluebase-ai": "bin/cluebase-cli.mjs"
7
+ },
8
+ "files": [
9
+ "bin/",
10
+ "src/",
11
+ "README.md"
12
+ ],
13
+ "dependencies": {},
14
+ "engines": {
15
+ "node": ">=20"
16
+ }
17
+ }
@@ -0,0 +1,515 @@
1
+ import { execFile } from "node:child_process";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { dirname, resolve } from "node:path";
4
+ import { promisify } from "node:util";
5
+
6
+ const execFileAsync = promisify(execFile);
7
+
8
+ import {
9
+ CLUEBASE_CLI_BINARY_NAME,
10
+ CLUEBASE_CLI_PACKAGE_NAME,
11
+ CLUEBASE_CLI_RECOMMENDED_PREFIX,
12
+ cluebaseCliCommand,
13
+ } from "./cli-invocation.mjs";
14
+ import { analyzeCodeEvidenceReport } from "./code-evidence-analyzer.mjs";
15
+ import {
16
+ deriveEnvironmentFromProjectApiKey,
17
+ deriveEnvironmentFromProjectKey,
18
+ } from "./contracts.mjs";
19
+ import { runSetupCheck } from "./setup-check.mjs";
20
+ import { runSetupDiscoverCheck } from "./setup-discover-check.mjs";
21
+ import { runSetupDoctor } from "./setup-doctor.mjs";
22
+ import { buildAiSetupHelp } from "./setup-help.mjs";
23
+ import { runSetupPrepare } from "./setup-prepare.mjs";
24
+ import { discoverSetupContract } from "./setup-repository-discovery.mjs";
25
+ import { installSetupSteps } from "./setup-tool.mjs";
26
+
27
+ export const parseArgs = (argv) => {
28
+ const [command = "help", ...tokens] = argv;
29
+ const flags = new Map();
30
+ for (let index = 0; index < tokens.length; index += 1) {
31
+ const token = tokens[index];
32
+ if (!token.startsWith("--")) continue;
33
+ const key = token.slice(2);
34
+ const next = tokens[index + 1];
35
+ if (next && !next.startsWith("--")) {
36
+ flags.set(key, next);
37
+ index += 1;
38
+ } else {
39
+ flags.set(key, true);
40
+ }
41
+ }
42
+ return { command, flags };
43
+ };
44
+
45
+ const readJson = async (path) => JSON.parse(await readFile(path, "utf8"));
46
+
47
+ const readPackageVersion = async () => {
48
+ const packageJson = JSON.parse(
49
+ await readFile(new URL("../package.json", import.meta.url), "utf8"),
50
+ );
51
+ return String(packageJson.version);
52
+ };
53
+
54
+ const readTextIfExists = async (path) => {
55
+ try {
56
+ return await readFile(path, "utf8");
57
+ } catch (error) {
58
+ if (error?.code === "ENOENT") return "";
59
+ throw error;
60
+ }
61
+ };
62
+
63
+ const DEFAULT_SETUP_MANIFEST_PATH = ".cluebase/setup-manifest.json";
64
+
65
+ const isGitignoreEntryPresent = (content, entry) =>
66
+ content
67
+ .split(/\r?\n/)
68
+ .map((line) => line.trim())
69
+ .includes(entry);
70
+
71
+ const appendGitignoreEntry = async ({ repoRoot, entry }) => {
72
+ const gitignorePath = resolve(repoRoot, ".gitignore");
73
+ const current = await readTextIfExists(gitignorePath);
74
+ if (isGitignoreEntryPresent(current, entry)) {
75
+ return "already_ignored";
76
+ }
77
+ const prefix = current.length > 0 && !current.endsWith("\n") ? "\n" : "";
78
+ await writeFile(gitignorePath, `${current}${prefix}${entry}\n`, "utf8");
79
+ return "added";
80
+ };
81
+
82
+ const CLUEBASE_ARTIFACT_GITIGNORE_ENTRIES = [
83
+ ".cluebase/discoveries.json",
84
+ ".cluebase/setup-check.json",
85
+ ".cluebase/setup-diff.patch",
86
+ ".cluebase/setup-review-findings.md",
87
+ ".cluebase/secrets.json",
88
+ ];
89
+
90
+ const protectCluebaseArtifacts = async ({ repoRoot }) => {
91
+ const gitignorePath = resolve(repoRoot, ".gitignore");
92
+ const current = await readTextIfExists(gitignorePath);
93
+ const results = [];
94
+ for (const entry of CLUEBASE_ARTIFACT_GITIGNORE_ENTRIES) {
95
+ if (isGitignoreEntryPresent(current, entry)) {
96
+ results.push({ entry, gitignore_status: "already_ignored" });
97
+ continue;
98
+ }
99
+ const status = await appendGitignoreEntry({ repoRoot, entry });
100
+ results.push({ entry, gitignore_status: status });
101
+ }
102
+ return results;
103
+ };
104
+
105
+ const SECRETS_FILE_PATH = ".cluebase/secrets.json";
106
+
107
+ const writeSecretsFile = async ({ repoRoot, apiKey }) => {
108
+ if (typeof apiKey !== "string" || apiKey.trim().length === 0) {
109
+ return { status: "skipped", reason: "no_api_key_provided" };
110
+ }
111
+ const absolutePath = resolve(repoRoot, SECRETS_FILE_PATH);
112
+ const body = {
113
+ cluebase_api_key: apiKey.trim(),
114
+ _warning:
115
+ "DO NOT COMMIT. This file contains the Cluebase API key. The Cluebase setup CLI auto-adds `.cluebase/secrets.json` to .gitignore — keep it that way.",
116
+ };
117
+ await mkdir(dirname(absolutePath), { recursive: true });
118
+ await writeFile(absolutePath, `${JSON.stringify(body, null, 2)}\n`, {
119
+ encoding: "utf8",
120
+ mode: 0o600,
121
+ });
122
+ return { status: "written", path: SECRETS_FILE_PATH };
123
+ };
124
+
125
+ const usage = () =>
126
+ [
127
+ "Cluebase CLI:",
128
+ ` npm package: ${CLUEBASE_CLI_PACKAGE_NAME}`,
129
+ ` binary: ${CLUEBASE_CLI_BINARY_NAME}`,
130
+ ` first-run invocation: ${CLUEBASE_CLI_RECOMMENDED_PREFIX} <command>`,
131
+ ` global ${CLUEBASE_CLI_BINARY_NAME} installation is not required`,
132
+ ` AI setup help: ${cluebaseCliCommand("help --json")}`,
133
+ "",
134
+ "Usage:",
135
+ ` ${cluebaseCliCommand("setup --cluebase-api-key <ak_dev_or_prod_key> --cluebase-api-base-url <url> --project-key <pk_dev_or_prod_key> --documents-url <url>")}`,
136
+ ` ${cluebaseCliCommand("setup-check --framework fastapi --backend-root-path backend --repo .")}`,
137
+ ` ${cluebaseCliCommand("setup-discover --repo .")}`,
138
+ ` ${cluebaseCliCommand("setup-discover-check --manifest .cluebase/setup-manifest.json --discoveries .cluebase/discoveries.json")}`,
139
+ ` ${cluebaseCliCommand("setup-doctor --local")}`,
140
+ ` ${cluebaseCliCommand("code-evidence --source-revision-sha <40-char-commit-sha> --repo .")}`,
141
+ ].join("\n");
142
+
143
+ const renderEnvironmentInstructions = (instructions) => {
144
+ if (!instructions || instructions.status !== "deferred_to_setup_wizard") {
145
+ return "";
146
+ }
147
+ const services = Array.isArray(instructions.detected_services)
148
+ ? instructions.detected_services
149
+ : [];
150
+ const serviceLines = services.map((service) => {
151
+ const kindLabel = service.kind === "frontend" ? "frontend" : "backend";
152
+ const frameworkLabel = service.framework ?? "unknown";
153
+ return ` - [${kindLabel}] ${service.root_path} (${frameworkLabel})`;
154
+ });
155
+ const lines = [
156
+ "",
157
+ "環境変数の設定は setup 画面の Step2 を見て行ってください。",
158
+ "(CLI 側では .env.cluebase ファイルは生成しません。setup 画面に必要な値が表示されます)",
159
+ "",
160
+ "検出したサービス:",
161
+ ...(serviceLines.length > 0 ? serviceLines : [" (なし)"]),
162
+ "",
163
+ ];
164
+ return `${lines.join("\n")}\n`;
165
+ };
166
+
167
+ const renderSetupResult = ({ preparation }) => {
168
+ if (preparation?.status === "ready_for_ai") {
169
+ const manifestPath =
170
+ preparation.artifacts?.setup_manifest_path ??
171
+ ".cluebase/setup-manifest.json";
172
+ return [
173
+ "Cluebase セットアップの準備が完了しました。",
174
+ "",
175
+ "作成したファイル:",
176
+ `- ${manifestPath}`,
177
+ "",
178
+ "次にやること:",
179
+ "1. Claude Code を使う場合: `claude` を起動し、`/cluebase-discover` -> `/cluebase-discover-review` -> `/cluebase-discover-context` -> `/cluebase-discover-check` -> `/cluebase-implement` -> `/cluebase-implement-check` -> `/cluebase-implement-review` -> `/cluebase-doctor` の順に実行してください。",
180
+ "2. Codex を使う場合: `codex` を起動し、`$cluebase-discover` -> `$cluebase-discover-review` -> `$cluebase-discover-context` -> `$cluebase-discover-check` -> `$cluebase-implement` -> `$cluebase-implement-check` -> `$cluebase-implement-review` -> `$cluebase-doctor` の順に実行してください。",
181
+ "",
182
+ "環境変数は Step 2 のコマンド (/cluebase-implement) が env ファイルへ自動で書き込みます。手動設定は通常不要です(自動書込が動かなかった場合のみ、setup 画面の env セクションに表示された値をコピーしてください)。",
183
+ ].join("\n");
184
+ }
185
+
186
+ const blockers = Array.isArray(preparation?.blockers)
187
+ ? preparation.blockers
188
+ .map((blocker) => blocker.message ?? blocker.reason ?? blocker.code)
189
+ .filter(Boolean)
190
+ : [];
191
+ return [
192
+ "Cluebase setup に失敗しました。",
193
+ "",
194
+ "原因:",
195
+ ...(blockers.length > 0
196
+ ? blockers.map((blocker) => `- ${blocker}`)
197
+ : ["- setup に必要な情報を検出できませんでした。"]),
198
+ ].join("\n");
199
+ };
200
+
201
+ const defaultIo = {
202
+ stdout: process.stdout,
203
+ stderr: process.stderr,
204
+ setExitCode: (code) => {
205
+ process.exitCode = code;
206
+ },
207
+ };
208
+
209
+ export const runCli = async (argv, io = defaultIo) => {
210
+ const { command, flags } = parseArgs(argv);
211
+ if (
212
+ command === "version" ||
213
+ command === "--version" ||
214
+ flags.has("version")
215
+ ) {
216
+ io.stdout.write(`${await readPackageVersion()}\n`);
217
+ return;
218
+ }
219
+
220
+ if (
221
+ (command === "help" || command === "--help" || flags.has("help")) &&
222
+ flags.has("json")
223
+ ) {
224
+ io.stdout.write(`${JSON.stringify(buildAiSetupHelp(), null, 2)}\n`);
225
+ return;
226
+ }
227
+
228
+ if (command === "help" || command === "--help" || flags.has("help")) {
229
+ io.stdout.write(`${usage()}\n`);
230
+ return;
231
+ }
232
+
233
+ const repoRoot = resolve(String(flags.get("repo") || "."));
234
+
235
+ if (command === "setup-doctor") {
236
+ const report = await runSetupDoctor({ flags, repoRoot });
237
+ io.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
238
+ if (!report.passed) {
239
+ io.setExitCode(1);
240
+ }
241
+ return;
242
+ }
243
+
244
+ if (command === "code-evidence" || command === "analyze-code-evidence") {
245
+ const sourceRevisionSha = flags.get("source-revision-sha");
246
+ if (typeof sourceRevisionSha !== "string") {
247
+ throw new Error("--source-revision-sha is required");
248
+ }
249
+ const csvPaths = (value) =>
250
+ typeof value === "string"
251
+ ? value
252
+ .split(",")
253
+ .map((entry) => entry.trim())
254
+ .filter(Boolean)
255
+ : undefined;
256
+ const report = await analyzeCodeEvidenceReport({
257
+ repoRoot,
258
+ sourceRevisionSha,
259
+ allowedSourcePaths: csvPaths(flags.get("allowed-source-paths")),
260
+ excludedSourcePaths: csvPaths(flags.get("excluded-source-paths")),
261
+ setupDiscoveryPath:
262
+ typeof flags.get("setup-discovery") === "string"
263
+ ? String(flags.get("setup-discovery"))
264
+ : undefined,
265
+ externalIdentityContextPath:
266
+ typeof flags.get("external-identity-context") === "string"
267
+ ? String(flags.get("external-identity-context"))
268
+ : undefined,
269
+ });
270
+ io.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
271
+ if (report.setup_plan.status !== "healthy") {
272
+ io.setExitCode(1);
273
+ }
274
+ return;
275
+ }
276
+
277
+ if (command === "setup-discover") {
278
+ const setupDiscovery = await discoverSetupContract({ repoRoot });
279
+ io.stdout.write(
280
+ `${JSON.stringify({ setup_discovery: setupDiscovery }, null, 2)}\n`,
281
+ );
282
+ return;
283
+ }
284
+
285
+ if (command === "setup") {
286
+ const setupProjectKey = flags.get("project-key");
287
+ const setupCluebaseApiKey = flags.get("cluebase-api-key");
288
+ if (typeof setupProjectKey === "string" && setupProjectKey.trim()) {
289
+ const projectKeyEnv = deriveEnvironmentFromProjectKey(setupProjectKey);
290
+ if (!projectKeyEnv) {
291
+ throw new Error("--project-key must start with pk_dev_ or pk_prod_");
292
+ }
293
+ if (
294
+ typeof setupCluebaseApiKey === "string" &&
295
+ setupCluebaseApiKey.trim()
296
+ ) {
297
+ const apiKeyEnv =
298
+ deriveEnvironmentFromProjectApiKey(setupCluebaseApiKey);
299
+ if (!apiKeyEnv) {
300
+ throw new Error(
301
+ "--cluebase-api-key must start with ak_dev_ or ak_prod_",
302
+ );
303
+ }
304
+ if (projectKeyEnv !== apiKeyEnv) {
305
+ throw new Error(
306
+ `environment mismatch: --project-key prefix indicates ${projectKeyEnv} but --cluebase-api-key prefix indicates ${apiKeyEnv}. Use a key pair from the same environment.`,
307
+ );
308
+ }
309
+ }
310
+ }
311
+ const report = await installSetupSteps({
312
+ repoRoot,
313
+ documentsUrl: flags.get("documents-url"),
314
+ });
315
+ const preparation = flags.has("skills-only")
316
+ ? {
317
+ status: "skipped",
318
+ reason: "skills-only flag was provided",
319
+ }
320
+ : await runSetupPrepare({
321
+ repoRoot,
322
+ setupContext: {
323
+ cluebaseApiKey: flags.get("cluebase-api-key"),
324
+ cluebaseApiBaseUrl: flags.get("cluebase-api-base-url"),
325
+ documentsUrl: flags.get("documents-url"),
326
+ projectKey: flags.get("project-key"),
327
+ },
328
+ });
329
+ const cluebaseArtifactProtection = await protectCluebaseArtifacts({
330
+ repoRoot,
331
+ });
332
+ preparation.cluebase_artifact_protection = cluebaseArtifactProtection;
333
+ const secretsProtection = await writeSecretsFile({
334
+ repoRoot,
335
+ apiKey: flags.get("cluebase-api-key"),
336
+ });
337
+ preparation.secrets_protection = secretsProtection;
338
+ const environmentInstructions = renderEnvironmentInstructions(
339
+ preparation.environment_instructions,
340
+ );
341
+ if (environmentInstructions && flags.has("json")) {
342
+ io.stderr.write(environmentInstructions);
343
+ }
344
+ if (flags.has("json")) {
345
+ io.stdout.write(
346
+ `${JSON.stringify({ ...report, preparation }, null, 2)}\n`,
347
+ );
348
+ } else {
349
+ io.stdout.write(`${renderSetupResult({ preparation })}\n`);
350
+ }
351
+ if (!flags.has("json") && preparation.status === "blocked") {
352
+ io.setExitCode(1);
353
+ }
354
+ return;
355
+ }
356
+
357
+ if (command === "setup-discover-check") {
358
+ const manifestPath = String(
359
+ flags.get("manifest") || DEFAULT_SETUP_MANIFEST_PATH,
360
+ );
361
+ const discoveriesPath = flags.get("discoveries");
362
+ if (typeof discoveriesPath !== "string") {
363
+ throw new Error("--discoveries is required");
364
+ }
365
+ const report = await runSetupDiscoverCheck({
366
+ manifestPath,
367
+ discoveriesPath,
368
+ repoRoot,
369
+ });
370
+ if (report.passed) {
371
+ io.stderr.write(
372
+ "OK — 発見結果に問題は見つかりませんでした。次の STEP に進めます。\n",
373
+ );
374
+ } else {
375
+ const issueCount = Array.isArray(report.errors)
376
+ ? report.errors.length
377
+ : 0;
378
+ io.stderr.write(
379
+ `FAIL — ${issueCount} 件の指摘があります。下の JSON 出力の errors の内容を Claude Code に伝えて修正してから、もう一度この STEP を実行してください。\n`,
380
+ );
381
+ }
382
+ io.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
383
+ if (!report.passed) {
384
+ io.setExitCode(report.exit_code ?? 1);
385
+ }
386
+ return;
387
+ }
388
+
389
+ if (command === "setup-check") {
390
+ const explicitFramework =
391
+ typeof flags.get("framework") === "string"
392
+ ? flags.get("framework")
393
+ : null;
394
+ const explicitBackendRootPath =
395
+ typeof flags.get("backend-root-path") === "string"
396
+ ? flags.get("backend-root-path")
397
+ : null;
398
+ let resolvedFramework = explicitFramework;
399
+ let resolvedBackendRootPath = explicitBackendRootPath;
400
+ const autoDetectedFields = [];
401
+ if (!resolvedFramework) {
402
+ let discoveries;
403
+ const sourcePath = ".cluebase/discoveries.json";
404
+ try {
405
+ discoveries = await readJson(resolve(repoRoot, sourcePath));
406
+ } catch {
407
+ discoveries = null;
408
+ }
409
+ const candidate =
410
+ typeof discoveries?.framework_backend === "string"
411
+ ? discoveries.framework_backend.trim()
412
+ : "";
413
+ if (candidate) {
414
+ resolvedFramework = candidate;
415
+ autoDetectedFields.push(`--framework ${candidate} (${sourcePath})`);
416
+ }
417
+ }
418
+ if (!resolvedBackendRootPath) {
419
+ try {
420
+ const manifest = await readJson(
421
+ resolve(repoRoot, DEFAULT_SETUP_MANIFEST_PATH),
422
+ );
423
+ const candidate =
424
+ typeof manifest?.detected?.backend_root_path === "string"
425
+ ? manifest.detected.backend_root_path.trim()
426
+ : "";
427
+ if (candidate) {
428
+ resolvedBackendRootPath = candidate;
429
+ autoDetectedFields.push(
430
+ `--backend-root-path ${candidate} (${DEFAULT_SETUP_MANIFEST_PATH})`,
431
+ );
432
+ }
433
+ } catch {
434
+ // A missing manifest leaves the explicit command contract unchanged.
435
+ }
436
+ }
437
+ if (autoDetectedFields.length > 0) {
438
+ io.stderr.write(
439
+ `setup-check: auto-detected ${autoDetectedFields.join(" + ")}\n`,
440
+ );
441
+ }
442
+ const hasInventoryFlags =
443
+ typeof resolvedFramework === "string" &&
444
+ typeof resolvedBackendRootPath === "string";
445
+ const request = hasInventoryFlags
446
+ ? {
447
+ framework: resolvedFramework,
448
+ backend_root_path: resolvedBackendRootPath,
449
+ allowed_source_paths:
450
+ typeof flags.get("allowed-source-paths") === "string"
451
+ ? String(flags.get("allowed-source-paths"))
452
+ .split(",")
453
+ .map((entry) => entry.trim())
454
+ .filter(Boolean)
455
+ : [resolvedBackendRootPath],
456
+ excluded_source_paths:
457
+ typeof flags.get("excluded-source-paths") === "string"
458
+ ? String(flags.get("excluded-source-paths"))
459
+ .split(",")
460
+ .map((entry) => entry.trim())
461
+ .filter(Boolean)
462
+ : [],
463
+ }
464
+ : undefined;
465
+ const report = await runSetupCheck({
466
+ repoRoot,
467
+ request,
468
+ requireSdkLifecycle: flags.has("require-sdk-lifecycle"),
469
+ });
470
+ if (flags.has("write-snapshot")) {
471
+ const cluebaseDir = resolve(repoRoot, ".cluebase");
472
+ await mkdir(cluebaseDir, { recursive: true });
473
+ const reportPath = resolve(cluebaseDir, "setup-check.json");
474
+ await writeFile(
475
+ reportPath,
476
+ `${JSON.stringify(report, null, 2)}\n`,
477
+ "utf8",
478
+ );
479
+ let gitDiffOutput = "";
480
+ let gitDiffError = null;
481
+ try {
482
+ const { stdout } = await execFileAsync("git", ["diff", "HEAD"], {
483
+ cwd: repoRoot,
484
+ maxBuffer: 50 * 1024 * 1024,
485
+ });
486
+ gitDiffOutput = stdout;
487
+ } catch (err) {
488
+ gitDiffError = err instanceof Error ? err.message : String(err);
489
+ }
490
+ const diffPath = resolve(repoRoot, ".cluebase/setup-diff.patch");
491
+ await writeFile(diffPath, gitDiffOutput, "utf8");
492
+ const reportPassedLabel = report.passed ? "passed" : "FAILED";
493
+ io.stderr.write(
494
+ `setup-check: wrote .cluebase/setup-check.json (${reportPassedLabel}) and .cluebase/setup-diff.patch (${gitDiffOutput.length} bytes)\n`,
495
+ );
496
+ if (gitDiffError) {
497
+ io.stderr.write(
498
+ `setup-check: warning — git diff HEAD failed: ${gitDiffError}\n`,
499
+ );
500
+ } else if (gitDiffOutput.trim().length === 0) {
501
+ io.stderr.write(
502
+ "setup-check: warning — .cluebase/setup-diff.patch is empty. If implementation changes were already committed, you may need: git diff HEAD~1 > .cluebase/setup-diff.patch\n",
503
+ );
504
+ }
505
+ return;
506
+ }
507
+ io.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
508
+ if (!report.passed) {
509
+ io.setExitCode(1);
510
+ }
511
+ return;
512
+ }
513
+
514
+ throw new Error(`Unknown command: ${command}\n${usage()}`);
515
+ };
@@ -0,0 +1,17 @@
1
+ export const CLUEBASE_CLI_PACKAGE_NAME = "@genn-inc/cluebase-cli";
2
+ export const CLUEBASE_CLI_BINARY_NAME = "cluebase-ai";
3
+ export const CLUEBASE_CLI_RECOMMENDED_PREFIX = `npx -y ${CLUEBASE_CLI_PACKAGE_NAME}`;
4
+
5
+ export const CLUEBASE_CLI_INVOCATION_CONTRACT = {
6
+ package_name: CLUEBASE_CLI_PACKAGE_NAME,
7
+ binary_name: CLUEBASE_CLI_BINARY_NAME,
8
+ recommended_prefix: CLUEBASE_CLI_RECOMMENDED_PREFIX,
9
+ global_binary_required: false,
10
+ version_check_command: `${CLUEBASE_CLI_RECOMMENDED_PREFIX} --version`,
11
+ help_command: `${CLUEBASE_CLI_RECOMMENDED_PREFIX} --help`,
12
+ ai_help_command: `${CLUEBASE_CLI_RECOMMENDED_PREFIX} help --json`,
13
+ rule: "Use the recommended_prefix for Cluebase CLI commands. Do not discover or require a global cluebase-ai binary; missing global cluebase-ai is normal. For AI setup scope and responsibility rules, run ai_help_command.",
14
+ };
15
+
16
+ export const cluebaseCliCommand = (args) =>
17
+ `${CLUEBASE_CLI_RECOMMENDED_PREFIX} ${args}`;