@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
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// setup-doctor customer-backend Cluebase route scan.
|
|
2
|
+
// Extracted from setup-doctor for file-size limits; content is unchanged.
|
|
3
|
+
|
|
4
|
+
import { readdir } from "node:fs/promises";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { isExcludedSourcePath } from "./path-policy.mjs";
|
|
7
|
+
import { throwIfSetupDoctorDeadlineExceeded } from "./setup-doctor-deadline.mjs";
|
|
8
|
+
import {
|
|
9
|
+
readSetupDoctorFile,
|
|
10
|
+
validateSetupDoctorDirectory,
|
|
11
|
+
} from "./setup-doctor-file-boundary.mjs";
|
|
12
|
+
import {
|
|
13
|
+
CLUEBASE_RESERVED_ROUTE_PATTERN,
|
|
14
|
+
BROWSER_TOKEN_PROXY_HINT_PATTERN,
|
|
15
|
+
ROUTE_DECLARATION_PATTERN,
|
|
16
|
+
PROXY_SCAN_FILE_EXTENSIONS,
|
|
17
|
+
PROXY_SCAN_EXCLUDED_PATHS,
|
|
18
|
+
PROXY_SCAN_MAX_FILES,
|
|
19
|
+
optionalString,
|
|
20
|
+
manifestDetectedServices,
|
|
21
|
+
relativeInsideRepo,
|
|
22
|
+
} from "./setup-doctor-env.mjs";
|
|
23
|
+
|
|
24
|
+
export const requiredInputCheck = ({ id, missing, url = null }) => ({
|
|
25
|
+
id,
|
|
26
|
+
severity: "error",
|
|
27
|
+
method: "POST",
|
|
28
|
+
url,
|
|
29
|
+
passed: false,
|
|
30
|
+
status: null,
|
|
31
|
+
error: `missing required input: ${missing.join(", ")}`,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export const fileExtension = (filePath) => {
|
|
35
|
+
const dot = filePath.lastIndexOf(".");
|
|
36
|
+
if (dot < 0) return "";
|
|
37
|
+
return filePath.slice(dot).toLowerCase();
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const pathLooksLikeCustomerBackendCluebaseRoute = (path) =>
|
|
41
|
+
path
|
|
42
|
+
.split(/[\\/]+/)
|
|
43
|
+
.map((part) => part.toLowerCase())
|
|
44
|
+
.join("/")
|
|
45
|
+
.includes("api/v1/cluebase/");
|
|
46
|
+
|
|
47
|
+
export const containsCustomerBackendCluebaseRoute = (text) =>
|
|
48
|
+
(ROUTE_DECLARATION_PATTERN.test(text) &&
|
|
49
|
+
CLUEBASE_RESERVED_ROUTE_PATTERN.test(text)) ||
|
|
50
|
+
(BROWSER_TOKEN_PROXY_HINT_PATTERN.test(text) &&
|
|
51
|
+
ROUTE_DECLARATION_PATTERN.test(text));
|
|
52
|
+
|
|
53
|
+
// Walk customer backend directories and collect files that appear to define a
|
|
54
|
+
// Cluebase-specific route handler. Setup requires direct browser-to-Cluebase token
|
|
55
|
+
// issuance, so customer backends should not own this route surface.
|
|
56
|
+
export const collectCustomerBackendCluebaseRouteFiles = async ({
|
|
57
|
+
repoRoot = ".",
|
|
58
|
+
rootAbs,
|
|
59
|
+
limit = PROXY_SCAN_MAX_FILES,
|
|
60
|
+
signal,
|
|
61
|
+
}) => {
|
|
62
|
+
const matched = [];
|
|
63
|
+
let scanned = 0;
|
|
64
|
+
let truncated = false;
|
|
65
|
+
const walk = async (dirAbs, relPath) => {
|
|
66
|
+
throwIfSetupDoctorDeadlineExceeded(signal);
|
|
67
|
+
if (scanned >= limit) {
|
|
68
|
+
truncated = true;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
let entries;
|
|
72
|
+
try {
|
|
73
|
+
const directory = await validateSetupDoctorDirectory({
|
|
74
|
+
repoRoot,
|
|
75
|
+
path: dirAbs,
|
|
76
|
+
signal,
|
|
77
|
+
});
|
|
78
|
+
entries = await readdir(directory.absolutePath, { withFileTypes: true });
|
|
79
|
+
} catch (error) {
|
|
80
|
+
throwIfSetupDoctorDeadlineExceeded(signal, error);
|
|
81
|
+
truncated = true;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
85
|
+
for (const entry of entries) {
|
|
86
|
+
if (scanned >= limit) {
|
|
87
|
+
truncated = true;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (entry.name.startsWith(".") && entry.name !== ".env") continue;
|
|
91
|
+
const childAbs = join(dirAbs, entry.name);
|
|
92
|
+
const childRel = relPath ? join(relPath, entry.name) : entry.name;
|
|
93
|
+
if (isExcludedSourcePath(childRel, PROXY_SCAN_EXCLUDED_PATHS)) continue;
|
|
94
|
+
if (entry.isDirectory()) {
|
|
95
|
+
await walk(childAbs, childRel);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (!entry.isFile()) {
|
|
99
|
+
truncated = true;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const ext = fileExtension(entry.name);
|
|
103
|
+
if (!PROXY_SCAN_FILE_EXTENSIONS.has(ext)) continue;
|
|
104
|
+
let text;
|
|
105
|
+
try {
|
|
106
|
+
text = await readSetupDoctorFile({
|
|
107
|
+
repoRoot,
|
|
108
|
+
path: childAbs,
|
|
109
|
+
signal,
|
|
110
|
+
});
|
|
111
|
+
} catch (error) {
|
|
112
|
+
throwIfSetupDoctorDeadlineExceeded(signal, error);
|
|
113
|
+
truncated = true;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (pathLooksLikeCustomerBackendCluebaseRoute(childRel)) {
|
|
117
|
+
matched.push(childRel);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
scanned += 1;
|
|
121
|
+
throwIfSetupDoctorDeadlineExceeded(signal);
|
|
122
|
+
if (containsCustomerBackendCluebaseRoute(text)) {
|
|
123
|
+
matched.push(childRel);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
throwIfSetupDoctorDeadlineExceeded(signal);
|
|
128
|
+
await walk(rootAbs, "");
|
|
129
|
+
throwIfSetupDoctorDeadlineExceeded(signal);
|
|
130
|
+
return { files: matched, scanned, truncated };
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export const resolveBackendRootCandidates = ({ flags, manifest, repoRoot }) => {
|
|
134
|
+
const flagRoot = optionalString(flags.get("backend-root-path"));
|
|
135
|
+
if (flagRoot) {
|
|
136
|
+
const relativeRoot = relativeInsideRepo({ path: flagRoot, repoRoot });
|
|
137
|
+
return [relativeRoot]
|
|
138
|
+
.filter(Boolean)
|
|
139
|
+
.map((root) => root);
|
|
140
|
+
}
|
|
141
|
+
const detectedRoot = optionalString(manifest?.detected?.backend_root_path);
|
|
142
|
+
if (detectedRoot) {
|
|
143
|
+
const relativeRoot = relativeInsideRepo({ path: detectedRoot, repoRoot });
|
|
144
|
+
return [relativeRoot]
|
|
145
|
+
.filter(Boolean)
|
|
146
|
+
.map((root) => root);
|
|
147
|
+
}
|
|
148
|
+
const backendTargets = manifestDetectedServices(manifest, "backend");
|
|
149
|
+
const targetRoots = backendTargets
|
|
150
|
+
.map((target) => optionalString(target.root_path ?? target.path))
|
|
151
|
+
.filter(Boolean)
|
|
152
|
+
.flatMap((path) => {
|
|
153
|
+
const relativeRoot = relativeInsideRepo({ path, repoRoot });
|
|
154
|
+
return [relativeRoot]
|
|
155
|
+
.filter(Boolean)
|
|
156
|
+
.map((root) => root);
|
|
157
|
+
});
|
|
158
|
+
if (targetRoots.length > 0) return targetRoots;
|
|
159
|
+
return [];
|
|
160
|
+
};
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
// Backend SDK real-import probe for setup-doctor.
|
|
2
|
+
//
|
|
3
|
+
// The static (grep-based) setup-check only sees the source text; it cannot tell
|
|
4
|
+
// that the customer's INSTALLED `cluebase-backend-sdk` actually provides the
|
|
5
|
+
// `cluebase` facade the STEP 5 code calls. A published version that predates the
|
|
6
|
+
// facade (e.g. 0.1.1) passes the grep check yet crashes the customer backend at
|
|
7
|
+
// import time. This probe runs the real `from cluebase_backend_sdk import cluebase`
|
|
8
|
+
// import in the customer's Python so that drift surfaces as an actionable
|
|
9
|
+
// failure instead of a false OK. It never weakens any other check.
|
|
10
|
+
|
|
11
|
+
import { execFile } from "node:child_process";
|
|
12
|
+
import { basename, join } from "node:path";
|
|
13
|
+
import { promisify } from "node:util";
|
|
14
|
+
import {
|
|
15
|
+
setupDoctorDeadlineError,
|
|
16
|
+
throwIfSetupDoctorDeadlineExceeded,
|
|
17
|
+
} from "./setup-doctor-deadline.mjs";
|
|
18
|
+
import {
|
|
19
|
+
isSetupDoctorFileBoundaryError,
|
|
20
|
+
prepareSetupDoctorPythonProbe,
|
|
21
|
+
readSetupDoctorJson,
|
|
22
|
+
} from "./setup-doctor-file-boundary.mjs";
|
|
23
|
+
|
|
24
|
+
const execFileAsync = promisify(execFile);
|
|
25
|
+
|
|
26
|
+
export const MIN_BACKEND_SDK_VERSION = "0.0.1";
|
|
27
|
+
|
|
28
|
+
// FastAPI / Django / Flask are the Python backends whose STEP 5 wiring imports
|
|
29
|
+
// the `cluebase` facade from `cluebase_backend_sdk`. Node backends use the separate
|
|
30
|
+
// `@genn-inc/cluebase-backend-sdk` package and are out of scope for this Python probe.
|
|
31
|
+
export const PYTHON_BACKEND_FRAMEWORKS = ["fastapi", "django", "flask"];
|
|
32
|
+
|
|
33
|
+
const BACKEND_SDK_IMPORT_CODE = "from cluebase_backend_sdk import cluebase";
|
|
34
|
+
|
|
35
|
+
const isUnreadablePythonError = (error) =>
|
|
36
|
+
error?.code === "EACCES" || error?.code === "EPERM";
|
|
37
|
+
|
|
38
|
+
export const backendFrameworksFromManifest = (manifest) => {
|
|
39
|
+
const selected = Array.isArray(manifest?.documentation?.selected_frameworks)
|
|
40
|
+
? manifest.documentation.selected_frameworks
|
|
41
|
+
: [];
|
|
42
|
+
const detected =
|
|
43
|
+
typeof manifest?.detected?.framework === "string"
|
|
44
|
+
? [manifest.detected.framework]
|
|
45
|
+
: [];
|
|
46
|
+
return [...selected, ...detected].map((value) => String(value).toLowerCase());
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const manifestHasPythonBackend = (manifest) =>
|
|
50
|
+
backendFrameworksFromManifest(manifest).some((framework) =>
|
|
51
|
+
PYTHON_BACKEND_FRAMEWORKS.includes(framework),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// Node backends wire `@genn-inc/cluebase-backend-sdk`, whose instrumentation must be
|
|
55
|
+
// preloaded before the server modules are loaded.
|
|
56
|
+
export const NODE_BACKEND_FRAMEWORKS = [
|
|
57
|
+
"express",
|
|
58
|
+
"nestjs",
|
|
59
|
+
"fastify",
|
|
60
|
+
"koa",
|
|
61
|
+
"hono",
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
// Backend frameworks across languages. Used to decide whether the doctor's
|
|
65
|
+
// backend-specific hops apply at all: a frontend-only setup (SPA + static /
|
|
66
|
+
// serverless hosting, no backend) has no backend ingest and no customer
|
|
67
|
+
// backend that could expose a Cluebase proxy route.
|
|
68
|
+
export const BACKEND_FRAMEWORKS = [
|
|
69
|
+
...PYTHON_BACKEND_FRAMEWORKS,
|
|
70
|
+
...NODE_BACKEND_FRAMEWORKS,
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
export const manifestHasNodeBackend = (manifest) =>
|
|
74
|
+
backendFrameworksFromManifest(manifest).some((framework) =>
|
|
75
|
+
NODE_BACKEND_FRAMEWORKS.includes(framework),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
export const manifestHasBackend = (manifest) => {
|
|
79
|
+
const backendRootPath = manifest?.detected?.backend_root_path;
|
|
80
|
+
if (typeof backendRootPath === "string" && backendRootPath.trim()) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
return backendFrameworksFromManifest(manifest).some((framework) =>
|
|
84
|
+
BACKEND_FRAMEWORKS.includes(framework),
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Candidate Python interpreters, venv first (customer installs into a venv far
|
|
89
|
+
// more often than into system site-packages), then the bare commands.
|
|
90
|
+
export const pythonInterpreterCandidates = ({ repoRoot, backendRootPath }) => {
|
|
91
|
+
const roots = [];
|
|
92
|
+
if (typeof backendRootPath === "string" && backendRootPath.trim()) {
|
|
93
|
+
roots.push(join(repoRoot, backendRootPath));
|
|
94
|
+
}
|
|
95
|
+
roots.push(repoRoot);
|
|
96
|
+
const venvPythons = roots.flatMap((root) => [
|
|
97
|
+
join(root, ".venv", "bin", "python"),
|
|
98
|
+
join(root, "venv", "bin", "python"),
|
|
99
|
+
]);
|
|
100
|
+
return [...venvPythons, "python3", "python"];
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const classifyImportFailure = (stderr) => {
|
|
104
|
+
const text = String(stderr ?? "");
|
|
105
|
+
if (/No module named ['"]cluebase_backend_sdk['"]/.test(text)) {
|
|
106
|
+
return "not_installed";
|
|
107
|
+
}
|
|
108
|
+
if (/cannot import name ['"]cluebase['"]/.test(text)) {
|
|
109
|
+
return "facade_missing";
|
|
110
|
+
}
|
|
111
|
+
return "unknown";
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// Runs the import in the first interpreter that actually executes. `runPython`
|
|
115
|
+
// is injectable for tests: it resolves to { code, stdout, stderr } and throws
|
|
116
|
+
// (e.g. ENOENT) when the interpreter is not runnable.
|
|
117
|
+
export const probeBackendSdkImport = async ({
|
|
118
|
+
repoRoot,
|
|
119
|
+
backendRootPath,
|
|
120
|
+
signal,
|
|
121
|
+
runPython = (python, code, options = {}) =>
|
|
122
|
+
execFileAsync(python, ["-c", code], options)
|
|
123
|
+
.then(() => ({ code: 0, stdout: "", stderr: "" }))
|
|
124
|
+
.catch((error) => {
|
|
125
|
+
if (error?.code === "ENOENT" || isUnreadablePythonError(error)) {
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
code: typeof error?.code === "number" ? error.code : 1,
|
|
130
|
+
stdout: String(error?.stdout ?? ""),
|
|
131
|
+
stderr: String(error?.stderr ?? error?.message ?? ""),
|
|
132
|
+
};
|
|
133
|
+
}),
|
|
134
|
+
candidates,
|
|
135
|
+
} = {}) => {
|
|
136
|
+
let secured;
|
|
137
|
+
try {
|
|
138
|
+
secured = await prepareSetupDoctorPythonProbe({
|
|
139
|
+
repoRoot,
|
|
140
|
+
backendRootPath,
|
|
141
|
+
candidates,
|
|
142
|
+
createCandidates: pythonInterpreterCandidates,
|
|
143
|
+
signal,
|
|
144
|
+
});
|
|
145
|
+
} catch (error) {
|
|
146
|
+
throwIfSetupDoctorDeadlineExceeded(signal, error);
|
|
147
|
+
if (isSetupDoctorFileBoundaryError(error)) {
|
|
148
|
+
return { status: "invalid_path" };
|
|
149
|
+
}
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for (const { candidate, executable } of secured.candidates) {
|
|
154
|
+
throwIfSetupDoctorDeadlineExceeded(signal);
|
|
155
|
+
let result;
|
|
156
|
+
try {
|
|
157
|
+
result = await runPython(executable, BACKEND_SDK_IMPORT_CODE, { signal });
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (signal?.aborted) throw setupDoctorDeadlineError();
|
|
160
|
+
if (error?.code === "ENOENT") continue;
|
|
161
|
+
if (isUnreadablePythonError(error)) return { status: "invalid_path" };
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
throwIfSetupDoctorDeadlineExceeded(signal);
|
|
165
|
+
if (result.code === 0) {
|
|
166
|
+
return { status: "ok", python: basename(candidate) };
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
status: classifyImportFailure(result.stderr),
|
|
170
|
+
python: basename(candidate),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return { status: "python_unavailable" };
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// Node instrumentation patches a module while it is being loaded, so it cannot
|
|
177
|
+
// reach a module that has already been loaded. `cluebase.init(...)` runs in the
|
|
178
|
+
// body of the entry file, after `express` / `pg` are evaluated, so the
|
|
179
|
+
// instrumentation has to be preloaded instead. Nothing in the source text
|
|
180
|
+
// reveals whether that happened — the customer's server starts fine and simply
|
|
181
|
+
// records nothing — which is why this reads the start commands.
|
|
182
|
+
export const CLUEBASE_REGISTER_SPECIFIER = "@genn-inc/cluebase-backend-sdk/register";
|
|
183
|
+
|
|
184
|
+
const CLUEBASE_PRELOAD_PATTERN =
|
|
185
|
+
/--(?:import|require)(?:[=\s]+)['"]?@genn-inc\/cluebase-backend-sdk\/register['"]?/;
|
|
186
|
+
|
|
187
|
+
const isServerStartScriptName = (name) =>
|
|
188
|
+
name === "dev" || name === "start" || name.startsWith("start:");
|
|
189
|
+
|
|
190
|
+
export const probeNodeBackendPreload = async ({
|
|
191
|
+
repoRoot,
|
|
192
|
+
backendRootPath,
|
|
193
|
+
nodeOptions,
|
|
194
|
+
signal,
|
|
195
|
+
readJson = readSetupDoctorJson,
|
|
196
|
+
} = {}) => {
|
|
197
|
+
if (typeof nodeOptions === "string" && CLUEBASE_PRELOAD_PATTERN.test(nodeOptions)) {
|
|
198
|
+
return { status: "ok", source: "node_options" };
|
|
199
|
+
}
|
|
200
|
+
const backendRoot =
|
|
201
|
+
typeof backendRootPath === "string" && backendRootPath.trim()
|
|
202
|
+
? backendRootPath.trim().replace(/\/+$/, "")
|
|
203
|
+
: null;
|
|
204
|
+
const manifestPath = backendRoot
|
|
205
|
+
? `${backendRoot}/package.json`
|
|
206
|
+
: "package.json";
|
|
207
|
+
let packageJson;
|
|
208
|
+
try {
|
|
209
|
+
packageJson = await readJson({
|
|
210
|
+
repoRoot,
|
|
211
|
+
path: manifestPath,
|
|
212
|
+
signal,
|
|
213
|
+
optional: true,
|
|
214
|
+
});
|
|
215
|
+
} catch (error) {
|
|
216
|
+
throwIfSetupDoctorDeadlineExceeded(signal, error);
|
|
217
|
+
if (isSetupDoctorFileBoundaryError(error)) {
|
|
218
|
+
return { status: "invalid_path", path: manifestPath };
|
|
219
|
+
}
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
if (packageJson === null) {
|
|
223
|
+
return { status: "package_json_missing", path: manifestPath };
|
|
224
|
+
}
|
|
225
|
+
const scripts =
|
|
226
|
+
packageJson.scripts && typeof packageJson.scripts === "object"
|
|
227
|
+
? packageJson.scripts
|
|
228
|
+
: {};
|
|
229
|
+
const startScripts = Object.entries(scripts).filter(
|
|
230
|
+
([name, command]) =>
|
|
231
|
+
isServerStartScriptName(name) && typeof command === "string",
|
|
232
|
+
);
|
|
233
|
+
if (startScripts.length === 0) {
|
|
234
|
+
return { status: "no_start_script", path: manifestPath };
|
|
235
|
+
}
|
|
236
|
+
const missing = startScripts
|
|
237
|
+
.filter(([, command]) => !CLUEBASE_PRELOAD_PATTERN.test(command))
|
|
238
|
+
.map(([name]) => name);
|
|
239
|
+
if (missing.length > 0) {
|
|
240
|
+
return { status: "missing", path: manifestPath, scripts: missing };
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
status: "ok",
|
|
244
|
+
source: "scripts",
|
|
245
|
+
path: manifestPath,
|
|
246
|
+
scripts: startScripts.map(([name]) => name),
|
|
247
|
+
};
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// What the customer loses is the point of the message: without the preload the
|
|
251
|
+
// server runs and reports no error, and four of the five inputs that build one
|
|
252
|
+
// user action stay empty.
|
|
253
|
+
const PRELOAD_CONSEQUENCE =
|
|
254
|
+
"この指定が無いと、顧客の backend で「どの処理が動いたか」「どのデータを読み書きしたか」「どの分岐で判断したか」「どの外部サービスを呼んだか」が 1 件も記録されません。server は正常に起動しエラーも出ないため、記録が空であることに誰も気づけません。";
|
|
255
|
+
|
|
256
|
+
const PRELOAD_REMEDY = `起動コマンドに \`--import ${CLUEBASE_REGISTER_SPECIFIER}\` を追加してください (例: \`node --import ${CLUEBASE_REGISTER_SPECIFIER} dist/main.js\`)。Docker / PM2 など package.json 以外から起動している場合は、そちらの起動コマンドか NODE_OPTIONS に同じ指定を入れてください。`;
|
|
257
|
+
|
|
258
|
+
export const nodeBackendPreloadCheck = (probe) => {
|
|
259
|
+
const id = "backend_sdk_instrumentation_preload";
|
|
260
|
+
if (probe.status === "ok") {
|
|
261
|
+
return {
|
|
262
|
+
id,
|
|
263
|
+
severity: "error",
|
|
264
|
+
passed: true,
|
|
265
|
+
source: probe.source,
|
|
266
|
+
...(probe.scripts ? { scripts: probe.scripts } : {}),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
if (probe.status === "missing") {
|
|
270
|
+
return {
|
|
271
|
+
id,
|
|
272
|
+
severity: "error",
|
|
273
|
+
passed: false,
|
|
274
|
+
scripts: probe.scripts,
|
|
275
|
+
error: `× backend の起動コマンドに Cluebase の計装の先読み込みがありません (${probe.path} の ${probe.scripts.join(" / ")})。${PRELOAD_CONSEQUENCE}${PRELOAD_REMEDY}`,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
if (probe.status === "invalid_path") {
|
|
279
|
+
return {
|
|
280
|
+
id,
|
|
281
|
+
severity: "error",
|
|
282
|
+
passed: false,
|
|
283
|
+
error: `× backend の package.json (${probe.path}) が repository boundary を満たさないため、計装の先読み込みを検証できませんでした。`,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
// package.json が見つからない / 起動 script が無い場合、起動方法は Docker CMD や
|
|
287
|
+
// 外部の supervisor 側にある。誤った赤にも誤った緑にもせず、未検証と伝える。
|
|
288
|
+
const reason =
|
|
289
|
+
probe.status === "package_json_missing"
|
|
290
|
+
? `backend の package.json (${probe.path}) が見つかりません`
|
|
291
|
+
: `${probe.path} に start / dev script がありません`;
|
|
292
|
+
return {
|
|
293
|
+
id,
|
|
294
|
+
severity: "warning",
|
|
295
|
+
passed: false,
|
|
296
|
+
error: `WARN: 計装の先読み込みを検証できませんでした (${reason})。${PRELOAD_CONSEQUENCE}実際の起動コマンドに \`--import ${CLUEBASE_REGISTER_SPECIFIER}\` が入っているか確認してください。`,
|
|
297
|
+
};
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
export const backendSdkImportCheck = (probe) => {
|
|
301
|
+
const id = "backend_sdk_import";
|
|
302
|
+
if (probe.status === "invalid_path") {
|
|
303
|
+
return {
|
|
304
|
+
id,
|
|
305
|
+
severity: "error",
|
|
306
|
+
passed: false,
|
|
307
|
+
error:
|
|
308
|
+
"× backend SDK import 検証失敗: backend root または Python interpreter path が repository boundary を満たさないか、読み取れません。symlink を使わない環境は `python -m venv --copies .venv` で作成してください。",
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
if (probe.status === "ok") {
|
|
312
|
+
return { id, severity: "error", passed: true, python: probe.python };
|
|
313
|
+
}
|
|
314
|
+
if (probe.status === "python_unavailable") {
|
|
315
|
+
// Cannot run Python here (e.g. no interpreter on PATH). Do not assert a
|
|
316
|
+
// false pass or a false fail — warn that the real import was not verified.
|
|
317
|
+
return {
|
|
318
|
+
id,
|
|
319
|
+
// doctor aggregates overall pass from severity==="error" checks only, and
|
|
320
|
+
// counts severity==="warning" in its warning summary. "cannot verify" is
|
|
321
|
+
// neither a pass nor a hard fail.
|
|
322
|
+
severity: "warning",
|
|
323
|
+
passed: false,
|
|
324
|
+
error:
|
|
325
|
+
'WARN: backend SDK の実 import を検証できませんでした (Python 実行環境が見つからない)。backend を起動する環境で `python -c "from cluebase_backend_sdk import cluebase"` が通るか確認してください。',
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
const remediation =
|
|
329
|
+
probe.status === "not_installed"
|
|
330
|
+
? `cluebase-backend-sdk が未インストールです。backend の依存をインストールしてください (例: pip install -r requirements.txt)。`
|
|
331
|
+
: probe.status === "facade_missing"
|
|
332
|
+
? `インストール済み cluebase-backend-sdk が古く \`cluebase\` ファサードがありません (要 >= ${MIN_BACKEND_SDK_VERSION})。\`pip install -U cluebase-backend-sdk\` で更新してください。`
|
|
333
|
+
: `backend SDK の import 検証に失敗しました。backend の Python 環境で \`from cluebase_backend_sdk import cluebase\` が通るか確認してください。`;
|
|
334
|
+
return {
|
|
335
|
+
id,
|
|
336
|
+
severity: "error",
|
|
337
|
+
passed: false,
|
|
338
|
+
error: `× backend SDK import 検証失敗: ${remediation}`,
|
|
339
|
+
};
|
|
340
|
+
};
|