@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.
- package/README.md +101 -0
- package/bin/cluebase-cli.mjs +11 -0
- package/package.json +17 -0
- package/src/cli-command.mjs +515 -0
- package/src/cli-invocation.mjs +17 -0
- package/src/code-evidence-analyzer.mjs +2041 -0
- package/src/contracts.mjs +36 -0
- package/src/generated-code-evidence-contract.mjs +22 -0
- package/src/generated-sdk-version-contract.mjs +5 -0
- package/src/generated-source-path-policy.mjs +20 -0
- package/src/lifecycle-guard.mjs +202 -0
- package/src/path-policy.mjs +81 -0
- package/src/setup-ai-contract.mjs +221 -0
- package/src/setup-check-constants.mjs +110 -0
- package/src/setup-check-scan-a.mjs +849 -0
- package/src/setup-check-scan-b.mjs +994 -0
- package/src/setup-check.mjs +575 -0
- package/src/setup-discover-check.mjs +755 -0
- package/src/setup-doctor-deadline.mjs +221 -0
- package/src/setup-doctor-env.mjs +331 -0
- package/src/setup-doctor-file-boundary.mjs +426 -0
- package/src/setup-doctor-probe.mjs +719 -0
- package/src/setup-doctor-quality-checks-a.mjs +593 -0
- package/src/setup-doctor-quality-checks-b.mjs +638 -0
- package/src/setup-doctor-quality-shared.mjs +382 -0
- package/src/setup-doctor-quality.mjs +209 -0
- package/src/setup-doctor-route-scan.mjs +160 -0
- package/src/setup-doctor-sdk-probe.mjs +340 -0
- package/src/setup-doctor.mjs +545 -0
- package/src/setup-documents.mjs +112 -0
- package/src/setup-help.mjs +130 -0
- package/src/setup-prepare.mjs +360 -0
- package/src/setup-repository-discovery.mjs +764 -0
- package/src/setup-step-builders-discover.mjs +701 -0
- package/src/setup-step-builders-events.mjs +229 -0
- package/src/setup-step-builders-implement.mjs +710 -0
- package/src/setup-step-commands.mjs +427 -0
- 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}`;
|