@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,426 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { lstat, open, realpath } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { throwIfSetupDoctorDeadlineExceeded } from "./setup-doctor-deadline.mjs";
|
|
6
|
+
|
|
7
|
+
// Setup-doctor reads customer-controlled source/configuration files. Keep the
|
|
8
|
+
// cap local to this boundary so every setup-doctor file consumer is bounded by
|
|
9
|
+
// the same one-megabyte limit.
|
|
10
|
+
export const SETUP_DOCTOR_MAX_FILE_BYTES = 1024 * 1024;
|
|
11
|
+
|
|
12
|
+
const SAFE_OPEN_FLAGS =
|
|
13
|
+
constants.O_RDONLY |
|
|
14
|
+
(constants.O_NOFOLLOW ?? 0) |
|
|
15
|
+
(constants.O_NONBLOCK ?? 0);
|
|
16
|
+
export const SETUP_DOCTOR_FILE_BOUNDARY_ERROR_CODE =
|
|
17
|
+
"SETUP_DOCTOR_FILE_BOUNDARY";
|
|
18
|
+
export const SETUP_DOCTOR_REPOSITORY_ROOT_ERROR_CODE =
|
|
19
|
+
"SETUP_DOCTOR_REPOSITORY_ROOT";
|
|
20
|
+
|
|
21
|
+
const isInside = (root, candidate) => {
|
|
22
|
+
const relativePath = relative(root, candidate);
|
|
23
|
+
return (
|
|
24
|
+
relativePath === "" ||
|
|
25
|
+
(!relativePath.startsWith("..") && !isAbsolute(relativePath))
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const isPathLike = (value) =>
|
|
30
|
+
isAbsolute(value) || value.includes("/") || value.includes("\\");
|
|
31
|
+
|
|
32
|
+
const safeError = (
|
|
33
|
+
relativePath,
|
|
34
|
+
reason,
|
|
35
|
+
code = SETUP_DOCTOR_FILE_BOUNDARY_ERROR_CODE,
|
|
36
|
+
originalCode = null,
|
|
37
|
+
) => {
|
|
38
|
+
const error = new Error(
|
|
39
|
+
`setup-doctor repository file "${relativePath}": ${reason}.`,
|
|
40
|
+
);
|
|
41
|
+
error.code = code;
|
|
42
|
+
error.relativePath = relativePath;
|
|
43
|
+
if (originalCode) error.originalCode = originalCode;
|
|
44
|
+
return error;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const isSetupDoctorFileBoundaryError = (error) =>
|
|
48
|
+
error?.code === SETUP_DOCTOR_FILE_BOUNDARY_ERROR_CODE ||
|
|
49
|
+
error?.code === SETUP_DOCTOR_REPOSITORY_ROOT_ERROR_CODE;
|
|
50
|
+
|
|
51
|
+
const stableStatIdentity = (entry) => {
|
|
52
|
+
if (
|
|
53
|
+
entry?.dev === undefined ||
|
|
54
|
+
entry?.dev === null ||
|
|
55
|
+
entry?.ino === undefined ||
|
|
56
|
+
entry?.ino === null
|
|
57
|
+
) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return `${entry.dev}:${entry.ino}`;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const normalizePath = ({ repoRoot, path }) => {
|
|
64
|
+
const rootAbs = resolve(repoRoot);
|
|
65
|
+
const input = typeof path === "string" ? path : String(path ?? "");
|
|
66
|
+
if (!input.trim()) throw safeError("<invalid>", "path is empty");
|
|
67
|
+
|
|
68
|
+
const rawRelativePath = isAbsolute(input)
|
|
69
|
+
? relative(rootAbs, resolve(input))
|
|
70
|
+
: input;
|
|
71
|
+
const rawParts = rawRelativePath.split(/[\\/]+/).filter(Boolean);
|
|
72
|
+
if (rawParts.includes("..")) {
|
|
73
|
+
throw safeError("<outside-repo>", "path escapes the selected repository");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const absolutePath = resolve(rootAbs, rawRelativePath);
|
|
77
|
+
if (!isInside(rootAbs, absolutePath)) {
|
|
78
|
+
throw safeError("<outside-repo>", "path escapes the selected repository");
|
|
79
|
+
}
|
|
80
|
+
const relativePath = relative(rootAbs, absolutePath) || ".";
|
|
81
|
+
return { rootAbs, absolutePath, relativePath };
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const checkedRepository = async (rootAbs, signal) => {
|
|
85
|
+
try {
|
|
86
|
+
const rootEntry = await lstat(rootAbs);
|
|
87
|
+
if (rootEntry.isSymbolicLink()) {
|
|
88
|
+
throw safeError(".", "selected repository root is a symlink");
|
|
89
|
+
}
|
|
90
|
+
if (!rootEntry.isDirectory()) {
|
|
91
|
+
throw safeError(".", "selected repository root is not a directory");
|
|
92
|
+
}
|
|
93
|
+
const rootRealPath = await realpath(rootAbs);
|
|
94
|
+
return rootRealPath;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
throwIfSetupDoctorDeadlineExceeded(signal, error);
|
|
97
|
+
if (isSetupDoctorFileBoundaryError(error)) throw error;
|
|
98
|
+
if (error?.code === "ENOENT") {
|
|
99
|
+
throw safeError(
|
|
100
|
+
".",
|
|
101
|
+
"selected repository root cannot be accessed",
|
|
102
|
+
SETUP_DOCTOR_REPOSITORY_ROOT_ERROR_CODE,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
throw safeError(
|
|
106
|
+
".",
|
|
107
|
+
"selected repository root cannot be accessed",
|
|
108
|
+
SETUP_DOCTOR_REPOSITORY_ROOT_ERROR_CODE,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const checkPathComponents = async ({
|
|
114
|
+
rootAbs,
|
|
115
|
+
absolutePath,
|
|
116
|
+
relativePath,
|
|
117
|
+
signal,
|
|
118
|
+
}) => {
|
|
119
|
+
const rootRealPath = await checkedRepository(rootAbs, signal);
|
|
120
|
+
const parts = relativePath === "." ? [] : relativePath.split(/[\\/]+/);
|
|
121
|
+
let currentPath = rootAbs;
|
|
122
|
+
let targetEntry = null;
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
for (const [index, part] of parts.entries()) {
|
|
126
|
+
currentPath = join(currentPath, part);
|
|
127
|
+
const entry = await lstat(currentPath);
|
|
128
|
+
if (entry.isSymbolicLink()) {
|
|
129
|
+
throw safeError(relativePath, "symlink paths are not allowed");
|
|
130
|
+
}
|
|
131
|
+
if (index < parts.length - 1 && !entry.isDirectory()) {
|
|
132
|
+
throw safeError(relativePath, "a parent path is not a directory");
|
|
133
|
+
}
|
|
134
|
+
targetEntry = entry;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (targetEntry === null) targetEntry = await lstat(absolutePath);
|
|
138
|
+
if (targetEntry.isSymbolicLink()) {
|
|
139
|
+
throw safeError(relativePath, "symlink paths are not allowed");
|
|
140
|
+
}
|
|
141
|
+
const targetRealPath = await realpath(absolutePath);
|
|
142
|
+
if (!isInside(rootRealPath, targetRealPath)) {
|
|
143
|
+
throw safeError(
|
|
144
|
+
relativePath,
|
|
145
|
+
"resolved path escapes the selected repository",
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return { targetEntry, targetRealPath };
|
|
149
|
+
} catch (error) {
|
|
150
|
+
throwIfSetupDoctorDeadlineExceeded(signal, error);
|
|
151
|
+
if (isSetupDoctorFileBoundaryError(error)) throw error;
|
|
152
|
+
if (error?.code === "ENOENT") {
|
|
153
|
+
throw safeError(
|
|
154
|
+
relativePath,
|
|
155
|
+
"path cannot be accessed",
|
|
156
|
+
SETUP_DOCTOR_FILE_BOUNDARY_ERROR_CODE,
|
|
157
|
+
"ENOENT",
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
throw safeError(relativePath, "path cannot be accessed");
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const runBoundaryOperation = async ({
|
|
165
|
+
relativePath,
|
|
166
|
+
signal,
|
|
167
|
+
optional,
|
|
168
|
+
operation,
|
|
169
|
+
}) => {
|
|
170
|
+
try {
|
|
171
|
+
return await operation();
|
|
172
|
+
} catch (error) {
|
|
173
|
+
throwIfSetupDoctorDeadlineExceeded(signal, error);
|
|
174
|
+
if (
|
|
175
|
+
optional &&
|
|
176
|
+
(error?.code === "ENOENT" || error?.originalCode === "ENOENT")
|
|
177
|
+
) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
if (isSetupDoctorFileBoundaryError(error)) throw error;
|
|
181
|
+
throw safeError(relativePath, "read failed");
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
export const readSetupDoctorFile = async ({
|
|
186
|
+
repoRoot,
|
|
187
|
+
path,
|
|
188
|
+
signal,
|
|
189
|
+
optional = false,
|
|
190
|
+
}) => {
|
|
191
|
+
const normalized = normalizePath({ repoRoot, path });
|
|
192
|
+
return runBoundaryOperation({
|
|
193
|
+
relativePath: normalized.relativePath,
|
|
194
|
+
signal,
|
|
195
|
+
optional,
|
|
196
|
+
operation: async () => {
|
|
197
|
+
const { targetEntry } = await checkPathComponents({
|
|
198
|
+
...normalized,
|
|
199
|
+
signal,
|
|
200
|
+
});
|
|
201
|
+
const targetIdentity = stableStatIdentity(targetEntry);
|
|
202
|
+
if (!targetEntry.isFile()) {
|
|
203
|
+
throw safeError(
|
|
204
|
+
normalized.relativePath,
|
|
205
|
+
"target is not a regular file",
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
if (targetIdentity === null) {
|
|
209
|
+
throw safeError(
|
|
210
|
+
normalized.relativePath,
|
|
211
|
+
"target identity is unavailable",
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const handle = await open(normalized.absolutePath, SAFE_OPEN_FLAGS);
|
|
216
|
+
try {
|
|
217
|
+
const openedEntry = await handle.stat();
|
|
218
|
+
if (!openedEntry.isFile()) {
|
|
219
|
+
throw safeError(
|
|
220
|
+
normalized.relativePath,
|
|
221
|
+
"opened target is not a regular file",
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
const openedIdentity = stableStatIdentity(openedEntry);
|
|
225
|
+
if (openedIdentity === null) {
|
|
226
|
+
throw safeError(
|
|
227
|
+
normalized.relativePath,
|
|
228
|
+
"opened target identity is unavailable",
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
if (openedIdentity !== targetIdentity) {
|
|
232
|
+
throw safeError(
|
|
233
|
+
normalized.relativePath,
|
|
234
|
+
"target changed during read (device/inode mismatch)",
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
if (openedEntry.size > SETUP_DOCTOR_MAX_FILE_BYTES) {
|
|
238
|
+
throw safeError(
|
|
239
|
+
normalized.relativePath,
|
|
240
|
+
"file exceeds the size limit",
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const buffer = Buffer.allocUnsafe(SETUP_DOCTOR_MAX_FILE_BYTES + 1);
|
|
245
|
+
let totalBytes = 0;
|
|
246
|
+
while (totalBytes < buffer.length) {
|
|
247
|
+
throwIfSetupDoctorDeadlineExceeded(signal);
|
|
248
|
+
const { bytesRead } = await handle.read({
|
|
249
|
+
buffer,
|
|
250
|
+
offset: totalBytes,
|
|
251
|
+
length: buffer.length - totalBytes,
|
|
252
|
+
position: totalBytes,
|
|
253
|
+
signal,
|
|
254
|
+
});
|
|
255
|
+
totalBytes += bytesRead;
|
|
256
|
+
if (bytesRead === 0) break;
|
|
257
|
+
}
|
|
258
|
+
if (totalBytes > SETUP_DOCTOR_MAX_FILE_BYTES) {
|
|
259
|
+
throw safeError(
|
|
260
|
+
normalized.relativePath,
|
|
261
|
+
"file exceeds the size limit",
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
return buffer.subarray(0, totalBytes).toString("utf8");
|
|
265
|
+
} finally {
|
|
266
|
+
await handle.close();
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
export const readSetupDoctorJson = async ({
|
|
273
|
+
repoRoot,
|
|
274
|
+
path,
|
|
275
|
+
signal,
|
|
276
|
+
optional = false,
|
|
277
|
+
}) => {
|
|
278
|
+
const content = await readSetupDoctorFile({
|
|
279
|
+
repoRoot,
|
|
280
|
+
path,
|
|
281
|
+
signal,
|
|
282
|
+
optional,
|
|
283
|
+
});
|
|
284
|
+
if (content === null) return null;
|
|
285
|
+
try {
|
|
286
|
+
return JSON.parse(content);
|
|
287
|
+
} catch {
|
|
288
|
+
const relativePath = normalizePath({ repoRoot, path }).relativePath;
|
|
289
|
+
throw safeError(relativePath, "file contains invalid JSON");
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
export const validateSetupDoctorDirectory = async ({
|
|
294
|
+
repoRoot,
|
|
295
|
+
path,
|
|
296
|
+
signal,
|
|
297
|
+
}) => {
|
|
298
|
+
const normalized = normalizePath({ repoRoot, path });
|
|
299
|
+
return runBoundaryOperation({
|
|
300
|
+
relativePath: normalized.relativePath,
|
|
301
|
+
signal,
|
|
302
|
+
optional: false,
|
|
303
|
+
operation: async () => {
|
|
304
|
+
const { targetEntry, targetRealPath } = await checkPathComponents({
|
|
305
|
+
...normalized,
|
|
306
|
+
signal,
|
|
307
|
+
});
|
|
308
|
+
if (!targetEntry.isDirectory()) {
|
|
309
|
+
throw safeError(normalized.relativePath, "target is not a directory");
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
absolutePath: normalized.absolutePath,
|
|
313
|
+
relativePath: normalized.relativePath,
|
|
314
|
+
realPath: targetRealPath,
|
|
315
|
+
};
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
export const validateSetupDoctorFile = async ({
|
|
321
|
+
repoRoot,
|
|
322
|
+
path,
|
|
323
|
+
signal,
|
|
324
|
+
optional = false,
|
|
325
|
+
}) => {
|
|
326
|
+
const normalized = normalizePath({ repoRoot, path });
|
|
327
|
+
return runBoundaryOperation({
|
|
328
|
+
relativePath: normalized.relativePath,
|
|
329
|
+
signal,
|
|
330
|
+
optional,
|
|
331
|
+
operation: async () => {
|
|
332
|
+
const { targetEntry, targetRealPath } = await checkPathComponents({
|
|
333
|
+
...normalized,
|
|
334
|
+
signal,
|
|
335
|
+
});
|
|
336
|
+
if (!targetEntry.isFile()) {
|
|
337
|
+
throw safeError(
|
|
338
|
+
normalized.relativePath,
|
|
339
|
+
"target is not a regular file",
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
const targetIdentity = stableStatIdentity(targetEntry);
|
|
343
|
+
if (targetIdentity === null) {
|
|
344
|
+
throw safeError(
|
|
345
|
+
normalized.relativePath,
|
|
346
|
+
"target identity is unavailable",
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
const handle = await open(normalized.absolutePath, SAFE_OPEN_FLAGS);
|
|
350
|
+
try {
|
|
351
|
+
const openedEntry = await handle.stat();
|
|
352
|
+
if (!openedEntry.isFile()) {
|
|
353
|
+
throw safeError(
|
|
354
|
+
normalized.relativePath,
|
|
355
|
+
"opened target is not a regular file",
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
const openedIdentity = stableStatIdentity(openedEntry);
|
|
359
|
+
if (openedIdentity === null) {
|
|
360
|
+
throw safeError(
|
|
361
|
+
normalized.relativePath,
|
|
362
|
+
"opened target identity is unavailable",
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
if (openedIdentity !== targetIdentity) {
|
|
366
|
+
throw safeError(
|
|
367
|
+
normalized.relativePath,
|
|
368
|
+
"target changed during validation",
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
} finally {
|
|
372
|
+
await handle.close();
|
|
373
|
+
}
|
|
374
|
+
return {
|
|
375
|
+
absolutePath: normalized.absolutePath,
|
|
376
|
+
relativePath: normalized.relativePath,
|
|
377
|
+
realPath: targetRealPath,
|
|
378
|
+
};
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
export const prepareSetupDoctorPythonProbe = async ({
|
|
384
|
+
repoRoot,
|
|
385
|
+
backendRootPath,
|
|
386
|
+
candidates,
|
|
387
|
+
createCandidates,
|
|
388
|
+
signal,
|
|
389
|
+
}) => {
|
|
390
|
+
const repository = await validateSetupDoctorDirectory({
|
|
391
|
+
repoRoot,
|
|
392
|
+
path: ".",
|
|
393
|
+
signal,
|
|
394
|
+
});
|
|
395
|
+
const backendRoot =
|
|
396
|
+
typeof backendRootPath === "string" && backendRootPath.trim()
|
|
397
|
+
? await validateSetupDoctorDirectory({
|
|
398
|
+
repoRoot: repository.realPath,
|
|
399
|
+
path: backendRootPath,
|
|
400
|
+
signal,
|
|
401
|
+
})
|
|
402
|
+
: repository;
|
|
403
|
+
const candidateList =
|
|
404
|
+
candidates ??
|
|
405
|
+
createCandidates({
|
|
406
|
+
repoRoot: repository.realPath,
|
|
407
|
+
backendRootPath: backendRoot.relativePath,
|
|
408
|
+
});
|
|
409
|
+
const validatedCandidates = [];
|
|
410
|
+
for (const candidate of candidateList) {
|
|
411
|
+
const validated = isPathLike(candidate)
|
|
412
|
+
? await validateSetupDoctorFile({
|
|
413
|
+
repoRoot: repository.realPath,
|
|
414
|
+
path: candidate,
|
|
415
|
+
signal,
|
|
416
|
+
optional: true,
|
|
417
|
+
})
|
|
418
|
+
: null;
|
|
419
|
+
if (isPathLike(candidate) && validated === null) continue;
|
|
420
|
+
validatedCandidates.push({
|
|
421
|
+
candidate,
|
|
422
|
+
executable: validated?.realPath ?? candidate,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
return { candidates: validatedCandidates };
|
|
426
|
+
};
|