codegate-ai 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -9
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +115 -31
- package/dist/commands/clawhub-wrapper.d.ts +76 -0
- package/dist/commands/clawhub-wrapper.js +631 -0
- package/dist/commands/scan-command.d.ts +9 -0
- package/dist/commands/scan-command.js +228 -204
- package/dist/commands/skills-wrapper.d.ts +10 -1
- package/dist/commands/skills-wrapper.js +35 -16
- package/package.json +1 -1
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { createInterface } from "node:readline/promises";
|
|
6
|
+
import { OUTPUT_FORMATS, } from "../config.js";
|
|
7
|
+
import { resolveScanTarget } from "../scan-target.js";
|
|
8
|
+
import { runScanAnalysis } from "./scan-command.js";
|
|
9
|
+
import { renderByFormat, summarizeRequestedTargetFindings } from "./scan-command/helpers.js";
|
|
10
|
+
const CLAWHUB_GLOBAL_OPTIONS_WITH_VALUE = new Set(["--workdir", "--dir", "--site", "--registry"]);
|
|
11
|
+
const CLAWHUB_INSTALL_OPTIONS_WITH_VALUE = new Set(["--version"]);
|
|
12
|
+
const NPX_CLAWHUB_BASE_ARGS = ["--yes", "clawhub"];
|
|
13
|
+
function isRecord(value) {
|
|
14
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15
|
+
}
|
|
16
|
+
function parseWrapperOptionValue(args, index, flag) {
|
|
17
|
+
const current = args[index] ?? "";
|
|
18
|
+
const withEquals = `${flag}=`;
|
|
19
|
+
if (current.startsWith(withEquals)) {
|
|
20
|
+
const value = current.slice(withEquals.length).trim();
|
|
21
|
+
if (value.length === 0) {
|
|
22
|
+
throw new Error(`${flag} requires a value`);
|
|
23
|
+
}
|
|
24
|
+
return [value, index];
|
|
25
|
+
}
|
|
26
|
+
const nextValue = args[index + 1];
|
|
27
|
+
if (!nextValue ||
|
|
28
|
+
nextValue.trim().length === 0 ||
|
|
29
|
+
nextValue === "--" ||
|
|
30
|
+
nextValue.startsWith("-")) {
|
|
31
|
+
throw new Error(`${flag} requires a value`);
|
|
32
|
+
}
|
|
33
|
+
return [nextValue, index + 1];
|
|
34
|
+
}
|
|
35
|
+
function parseOutputFormat(value) {
|
|
36
|
+
const normalized = value.trim().toLowerCase();
|
|
37
|
+
const matched = OUTPUT_FORMATS.find((format) => format === normalized);
|
|
38
|
+
if (!matched) {
|
|
39
|
+
throw new Error(`Unsupported --cg-format value "${value}". Valid values: ${OUTPUT_FORMATS.join(", ")}.`);
|
|
40
|
+
}
|
|
41
|
+
return matched;
|
|
42
|
+
}
|
|
43
|
+
function isLikelyHttpUrl(value) {
|
|
44
|
+
return /^https?:\/\//iu.test(value);
|
|
45
|
+
}
|
|
46
|
+
function splitLongOption(token) {
|
|
47
|
+
const equalsIndex = token.indexOf("=");
|
|
48
|
+
if (equalsIndex < 0) {
|
|
49
|
+
return [token, null];
|
|
50
|
+
}
|
|
51
|
+
return [token.slice(0, equalsIndex), token.slice(equalsIndex + 1)];
|
|
52
|
+
}
|
|
53
|
+
function isValueOption(flag) {
|
|
54
|
+
return (CLAWHUB_GLOBAL_OPTIONS_WITH_VALUE.has(flag) || CLAWHUB_INSTALL_OPTIONS_WITH_VALUE.has(flag));
|
|
55
|
+
}
|
|
56
|
+
function firstPositionalToken(args) {
|
|
57
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
58
|
+
const token = args[index] ?? "";
|
|
59
|
+
if (token === "--") {
|
|
60
|
+
return [null, -1];
|
|
61
|
+
}
|
|
62
|
+
if (token.startsWith("--")) {
|
|
63
|
+
const [flag, inlineValue] = splitLongOption(token);
|
|
64
|
+
if (inlineValue === null && isValueOption(flag)) {
|
|
65
|
+
index += 1;
|
|
66
|
+
}
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (token.startsWith("-")) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
return [token.toLowerCase(), index];
|
|
73
|
+
}
|
|
74
|
+
return [null, -1];
|
|
75
|
+
}
|
|
76
|
+
function isLikelyLeadingGlobalOptionSequence(args, endExclusive) {
|
|
77
|
+
for (let index = 0; index < endExclusive; index += 1) {
|
|
78
|
+
const token = args[index] ?? "";
|
|
79
|
+
if (token === "--") {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
if (token.startsWith("--")) {
|
|
83
|
+
const [flag, inlineValue] = splitLongOption(token);
|
|
84
|
+
if (inlineValue === null && isValueOption(flag)) {
|
|
85
|
+
index += 1;
|
|
86
|
+
}
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (token.startsWith("-")) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
function looksLikeSourceToken(value, context) {
|
|
97
|
+
if (value.trim().length === 0 || value.startsWith("-")) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
if (isLikelyHttpUrl(value) ||
|
|
101
|
+
value.startsWith("./") ||
|
|
102
|
+
value.startsWith("../") ||
|
|
103
|
+
value.startsWith("/") ||
|
|
104
|
+
value.startsWith("~/") ||
|
|
105
|
+
value.startsWith("~\\")) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
if (context) {
|
|
109
|
+
const localCandidate = resolve(context.cwd, value);
|
|
110
|
+
if (context.pathExists(localCandidate)) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
function firstLikelySourceAfterInstall(args, installIndex, context) {
|
|
117
|
+
for (let index = installIndex + 1; index < args.length; index += 1) {
|
|
118
|
+
const token = args[index] ?? "";
|
|
119
|
+
if (token === "--") {
|
|
120
|
+
for (let tailIndex = index + 1; tailIndex < args.length; tailIndex += 1) {
|
|
121
|
+
const candidate = args[tailIndex] ?? "";
|
|
122
|
+
if (!candidate.startsWith("-")) {
|
|
123
|
+
return candidate;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
if (token.startsWith("--")) {
|
|
129
|
+
const [flag, inlineValue] = splitLongOption(token);
|
|
130
|
+
if (inlineValue === null && isValueOption(flag)) {
|
|
131
|
+
if (flag === "--version") {
|
|
132
|
+
index += 1;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
index += 1;
|
|
136
|
+
}
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (token.startsWith("-")) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (looksLikeSourceToken(token, context)) {
|
|
143
|
+
// Heuristic to preserve forward compatibility with new ClawHub options:
|
|
144
|
+
// if this token follows a flag and another source-like token follows,
|
|
145
|
+
// treat this one as the option value and keep scanning.
|
|
146
|
+
const previous = index > installIndex + 1 ? (args[index - 1] ?? "") : "";
|
|
147
|
+
const next = args[index + 1] ?? "";
|
|
148
|
+
if (previous.startsWith("-") && looksLikeSourceToken(next, context)) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
return token;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
function requestedVersionAfterInstall(args, installIndex) {
|
|
157
|
+
for (let index = installIndex + 1; index < args.length; index += 1) {
|
|
158
|
+
const token = args[index] ?? "";
|
|
159
|
+
if (token === "--") {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
if (!token.startsWith("--")) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const [flag, inlineValue] = splitLongOption(token);
|
|
166
|
+
if (flag !== "--version") {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (inlineValue !== null) {
|
|
170
|
+
const value = inlineValue.trim();
|
|
171
|
+
return value.length > 0 ? value : null;
|
|
172
|
+
}
|
|
173
|
+
const next = args[index + 1] ?? "";
|
|
174
|
+
if (next.trim().length === 0 || next.startsWith("-")) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
return next.trim();
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
function findInstallSubcommandIndex(args, context) {
|
|
182
|
+
const [subcommand, subcommandIndex] = firstPositionalToken(args);
|
|
183
|
+
if (subcommand === "install") {
|
|
184
|
+
return subcommandIndex;
|
|
185
|
+
}
|
|
186
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
187
|
+
const token = args[index]?.toLowerCase();
|
|
188
|
+
if (token !== "install") {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (isLikelyLeadingGlobalOptionSequence(args, index) &&
|
|
192
|
+
firstLikelySourceAfterInstall(args, index, context)) {
|
|
193
|
+
return index;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return -1;
|
|
197
|
+
}
|
|
198
|
+
function normalizeSlashes(value) {
|
|
199
|
+
return value.replaceAll("\\", "/");
|
|
200
|
+
}
|
|
201
|
+
function sanitizeSlugForPath(slug) {
|
|
202
|
+
const segments = normalizeSlashes(slug)
|
|
203
|
+
.split("/")
|
|
204
|
+
.filter((segment) => segment.length > 0)
|
|
205
|
+
.map((segment) => {
|
|
206
|
+
const sanitized = segment.replace(/[^a-z0-9._-]/giu, "-");
|
|
207
|
+
// Prevent "." and ".." from altering join() resolution semantics.
|
|
208
|
+
return sanitized === "." || sanitized === ".." ? "_" : sanitized;
|
|
209
|
+
})
|
|
210
|
+
.filter((segment) => segment.length > 0);
|
|
211
|
+
return segments.length > 0 ? segments.join("/") : "skill";
|
|
212
|
+
}
|
|
213
|
+
function sanitizeRelativeRemotePath(value) {
|
|
214
|
+
const normalized = normalizeSlashes(value).replace(/^\/+/, "");
|
|
215
|
+
if (normalized.length === 0 || normalized.includes("\u0000")) {
|
|
216
|
+
throw new Error(`Invalid file path returned by clawhub inspect: ${value}`);
|
|
217
|
+
}
|
|
218
|
+
const segments = normalized.split("/");
|
|
219
|
+
if (segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) {
|
|
220
|
+
throw new Error(`Unsafe file path returned by clawhub inspect: ${value}`);
|
|
221
|
+
}
|
|
222
|
+
return segments.join("/");
|
|
223
|
+
}
|
|
224
|
+
function parseJsonObjectFromCliOutput(raw, context) {
|
|
225
|
+
const trimmed = raw.trim();
|
|
226
|
+
const start = trimmed.indexOf("{");
|
|
227
|
+
if (start < 0) {
|
|
228
|
+
throw new Error(`clawhub ${context} did not produce JSON output`);
|
|
229
|
+
}
|
|
230
|
+
const jsonCandidate = trimmed.slice(start);
|
|
231
|
+
let parsed;
|
|
232
|
+
try {
|
|
233
|
+
parsed = JSON.parse(jsonCandidate);
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
237
|
+
throw new Error(`Failed to parse clawhub ${context} JSON output: ${message}`, {
|
|
238
|
+
cause: error,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
if (!isRecord(parsed)) {
|
|
242
|
+
throw new Error(`Unexpected clawhub ${context} JSON payload`);
|
|
243
|
+
}
|
|
244
|
+
return parsed;
|
|
245
|
+
}
|
|
246
|
+
function runClawhubCli(args, cwd) {
|
|
247
|
+
// --yes avoids interactive install prompts when npx needs to fetch clawhub.
|
|
248
|
+
const result = spawnSync("npx", [...NPX_CLAWHUB_BASE_ARGS, ...args], {
|
|
249
|
+
cwd,
|
|
250
|
+
encoding: "utf8",
|
|
251
|
+
});
|
|
252
|
+
if (result.error) {
|
|
253
|
+
throw result.error;
|
|
254
|
+
}
|
|
255
|
+
if (result.status !== 0) {
|
|
256
|
+
const stderr = (result.stderr ?? "").trim();
|
|
257
|
+
const stdout = (result.stdout ?? "").trim();
|
|
258
|
+
throw new Error(stderr.length > 0
|
|
259
|
+
? stderr
|
|
260
|
+
: stdout.length > 0
|
|
261
|
+
? stdout
|
|
262
|
+
: `npx clawhub ${args.join(" ")} failed`);
|
|
263
|
+
}
|
|
264
|
+
return result.stdout ?? "";
|
|
265
|
+
}
|
|
266
|
+
function extractInspectFiles(payload) {
|
|
267
|
+
const version = payload.version;
|
|
268
|
+
if (!isRecord(version)) {
|
|
269
|
+
return [];
|
|
270
|
+
}
|
|
271
|
+
const files = version.files;
|
|
272
|
+
if (!Array.isArray(files)) {
|
|
273
|
+
return [];
|
|
274
|
+
}
|
|
275
|
+
return files
|
|
276
|
+
.filter((item) => isRecord(item))
|
|
277
|
+
.map((item) => item.path)
|
|
278
|
+
.filter((path) => typeof path === "string" && path.trim().length > 0);
|
|
279
|
+
}
|
|
280
|
+
function extractInspectFileContent(payload, fallbackPath) {
|
|
281
|
+
const file = payload.file;
|
|
282
|
+
if (!isRecord(file)) {
|
|
283
|
+
throw new Error(`clawhub inspect --file ${fallbackPath} did not return file metadata`);
|
|
284
|
+
}
|
|
285
|
+
const content = file.content;
|
|
286
|
+
if (typeof content !== "string") {
|
|
287
|
+
throw new Error(`clawhub inspect --file ${fallbackPath} did not return textual file content`);
|
|
288
|
+
}
|
|
289
|
+
return content;
|
|
290
|
+
}
|
|
291
|
+
function canonicalClawhubUrlFromSlug(slug) {
|
|
292
|
+
const normalized = normalizeSlashes(slug).replace(/^\/+/, "").replace(/\/+$/, "");
|
|
293
|
+
return `https://clawhub.ai/${normalized}`;
|
|
294
|
+
}
|
|
295
|
+
function isLikelyLocalPathLike(value, context) {
|
|
296
|
+
if (value.startsWith("./") ||
|
|
297
|
+
value.startsWith("../") ||
|
|
298
|
+
value.startsWith("/") ||
|
|
299
|
+
value.startsWith("~/") ||
|
|
300
|
+
value.startsWith("~\\") ||
|
|
301
|
+
value.includes("\\")) {
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
return context.pathExists(resolve(context.cwd, value));
|
|
305
|
+
}
|
|
306
|
+
function extractClawhubSlugFromSource(source, context) {
|
|
307
|
+
const trimmed = source.trim();
|
|
308
|
+
if (trimmed.length === 0 || trimmed.startsWith("-")) {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
if (isLikelyHttpUrl(trimmed)) {
|
|
312
|
+
try {
|
|
313
|
+
const url = new URL(trimmed);
|
|
314
|
+
if (!url.hostname.toLowerCase().endsWith("clawhub.ai")) {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
const normalizedPath = url.pathname.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
318
|
+
return normalizedPath.length > 0 ? decodeURIComponent(normalizedPath) : null;
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (isLikelyLocalPathLike(trimmed, context)) {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
return trimmed;
|
|
328
|
+
}
|
|
329
|
+
async function stageClawhubSkillFromInspect(input) {
|
|
330
|
+
const versionArgs = input.version ? ["--version", input.version] : [];
|
|
331
|
+
const filesPayload = parseJsonObjectFromCliOutput(runClawhubCli(["inspect", "--json", "--files", ...versionArgs, input.slug], input.cwd), "inspect --files");
|
|
332
|
+
const filePaths = extractInspectFiles(filesPayload);
|
|
333
|
+
if (filePaths.length === 0) {
|
|
334
|
+
throw new Error(`clawhub inspect returned no files for ${input.slug}`);
|
|
335
|
+
}
|
|
336
|
+
const uniqueSortedFilePaths = [...new Set(filePaths)].sort((left, right) => left.localeCompare(right));
|
|
337
|
+
const tempRoot = mkdtempSync(join(tmpdir(), "codegate-scan-clawhub-"));
|
|
338
|
+
const stageRoot = join(tempRoot, "staged");
|
|
339
|
+
const stageSkillRoot = join(stageRoot, "skills", sanitizeSlugForPath(input.slug));
|
|
340
|
+
try {
|
|
341
|
+
for (const path of uniqueSortedFilePaths) {
|
|
342
|
+
const filePayload = parseJsonObjectFromCliOutput(runClawhubCli(["inspect", "--json", "--file", path, ...versionArgs, input.slug], input.cwd), `inspect --file ${path}`);
|
|
343
|
+
const content = extractInspectFileContent(filePayload, path);
|
|
344
|
+
const safeRelativePath = sanitizeRelativeRemotePath(path);
|
|
345
|
+
const destination = join(stageSkillRoot, safeRelativePath);
|
|
346
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
347
|
+
writeFileSync(destination, content, "utf8");
|
|
348
|
+
}
|
|
349
|
+
return {
|
|
350
|
+
scanTarget: stageRoot,
|
|
351
|
+
displayTarget: input.displayTarget,
|
|
352
|
+
cleanup: () => rmSync(tempRoot, { recursive: true, force: true }),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
rmSync(tempRoot, { recursive: true, force: true });
|
|
357
|
+
throw error;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
async function stageClawhubTargetDefault(input, deps) {
|
|
361
|
+
const sourceContext = {
|
|
362
|
+
cwd: input.cwd,
|
|
363
|
+
pathExists: deps.pathExists,
|
|
364
|
+
};
|
|
365
|
+
const slug = extractClawhubSlugFromSource(input.sourceTarget, sourceContext);
|
|
366
|
+
if (slug) {
|
|
367
|
+
return stageClawhubSkillFromInspect({
|
|
368
|
+
slug,
|
|
369
|
+
version: input.requestedVersion,
|
|
370
|
+
cwd: input.cwd,
|
|
371
|
+
displayTarget: isLikelyHttpUrl(input.sourceTarget)
|
|
372
|
+
? input.sourceTarget
|
|
373
|
+
: canonicalClawhubUrlFromSlug(slug),
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
return await deps.resolveScanTarget({
|
|
377
|
+
rawTarget: input.sourceTarget,
|
|
378
|
+
cwd: input.cwd,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
export function parseClawhubInvocation(rawArgs, context) {
|
|
382
|
+
const wrapper = {
|
|
383
|
+
force: false,
|
|
384
|
+
deep: false,
|
|
385
|
+
noTui: false,
|
|
386
|
+
includeUserScope: false,
|
|
387
|
+
format: undefined,
|
|
388
|
+
configPath: undefined,
|
|
389
|
+
};
|
|
390
|
+
const passthroughArgs = [];
|
|
391
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
392
|
+
const token = rawArgs[index] ?? "";
|
|
393
|
+
if (token === "--") {
|
|
394
|
+
passthroughArgs.push("--");
|
|
395
|
+
for (let tailIndex = index + 1; tailIndex < rawArgs.length; tailIndex += 1) {
|
|
396
|
+
passthroughArgs.push(rawArgs[tailIndex] ?? "");
|
|
397
|
+
}
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
if (token === "--cg-force") {
|
|
401
|
+
wrapper.force = true;
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
if (token === "--cg-deep") {
|
|
405
|
+
wrapper.deep = true;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
if (token === "--cg-no-tui") {
|
|
409
|
+
wrapper.noTui = true;
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
if (token === "--cg-include-user-scope") {
|
|
413
|
+
wrapper.includeUserScope = true;
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
if (token === "--cg-format" || token.startsWith("--cg-format=")) {
|
|
417
|
+
const [value, consumedIndex] = parseWrapperOptionValue(rawArgs, index, "--cg-format");
|
|
418
|
+
wrapper.format = parseOutputFormat(value);
|
|
419
|
+
index = consumedIndex;
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
if (token === "--cg-config" || token.startsWith("--cg-config=")) {
|
|
423
|
+
const [value, consumedIndex] = parseWrapperOptionValue(rawArgs, index, "--cg-config");
|
|
424
|
+
wrapper.configPath = value;
|
|
425
|
+
index = consumedIndex;
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (token.startsWith("--cg-")) {
|
|
429
|
+
throw new Error(`Unknown CodeGate wrapper option: ${token}`);
|
|
430
|
+
}
|
|
431
|
+
passthroughArgs.push(token);
|
|
432
|
+
}
|
|
433
|
+
const installSubcommandIndex = findInstallSubcommandIndex(passthroughArgs, context);
|
|
434
|
+
const subcommand = installSubcommandIndex >= 0 ? "install" : firstPositionalToken(passthroughArgs)[0];
|
|
435
|
+
const sourceTarget = installSubcommandIndex >= 0
|
|
436
|
+
? firstLikelySourceAfterInstall(passthroughArgs, installSubcommandIndex, context)
|
|
437
|
+
: null;
|
|
438
|
+
const requestedVersion = installSubcommandIndex >= 0
|
|
439
|
+
? requestedVersionAfterInstall(passthroughArgs, installSubcommandIndex)
|
|
440
|
+
: null;
|
|
441
|
+
return {
|
|
442
|
+
passthroughArgs,
|
|
443
|
+
wrapper,
|
|
444
|
+
subcommand,
|
|
445
|
+
sourceTarget,
|
|
446
|
+
requestedVersion,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
async function promptWarningProceed(context) {
|
|
450
|
+
const rl = createInterface({
|
|
451
|
+
input: process.stdin,
|
|
452
|
+
output: process.stdout,
|
|
453
|
+
});
|
|
454
|
+
const prompt = [
|
|
455
|
+
`Warning findings detected for ${context.target}.`,
|
|
456
|
+
`Findings: ${context.report.summary.total}`,
|
|
457
|
+
"Proceed with clawhub install? [y/N]: ",
|
|
458
|
+
].join("\n");
|
|
459
|
+
try {
|
|
460
|
+
const answer = await rl.question(prompt);
|
|
461
|
+
return /^y(es)?$/iu.test(answer.trim());
|
|
462
|
+
}
|
|
463
|
+
finally {
|
|
464
|
+
rl.close();
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
function finalizeLaunch(result, deps) {
|
|
468
|
+
if (result.error) {
|
|
469
|
+
deps.stderr(`Failed to run npx clawhub: ${result.error.message}`);
|
|
470
|
+
deps.setExitCode(3);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
deps.setExitCode(result.status ?? 1);
|
|
474
|
+
}
|
|
475
|
+
function shouldPromptForWarning(report, config, force) {
|
|
476
|
+
return (report.summary.exit_code === 1 &&
|
|
477
|
+
report.findings.length > 0 &&
|
|
478
|
+
force !== true &&
|
|
479
|
+
config.auto_proceed_below_threshold !== true);
|
|
480
|
+
}
|
|
481
|
+
export function launchClawhubPassthrough(args, cwd) {
|
|
482
|
+
const result = spawnSync("npx", [...NPX_CLAWHUB_BASE_ARGS, ...args], {
|
|
483
|
+
cwd,
|
|
484
|
+
stdio: "inherit",
|
|
485
|
+
});
|
|
486
|
+
return {
|
|
487
|
+
status: result.status,
|
|
488
|
+
error: result.error ?? undefined,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
export async function executeClawhubWrapper(input, deps) {
|
|
492
|
+
const cwd = deps.cwd();
|
|
493
|
+
const isTTY = deps.isTTY();
|
|
494
|
+
const pathExists = deps.pathExists ?? ((path) => existsSync(path));
|
|
495
|
+
const sourceDetectionContext = { cwd, pathExists };
|
|
496
|
+
const parsed = parseClawhubInvocation(input.clawhubArgs, sourceDetectionContext);
|
|
497
|
+
if (parsed.subcommand !== "install") {
|
|
498
|
+
finalizeLaunch(deps.launchClawhub(parsed.passthroughArgs, cwd), deps);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
const resolvedSourceTarget = parsed.sourceTarget;
|
|
502
|
+
if (!resolvedSourceTarget) {
|
|
503
|
+
deps.stderr("Could not determine the source target for `clawhub install`. Provide a skill slug or source target after `install`.");
|
|
504
|
+
deps.setExitCode(3);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
let resolvedTarget;
|
|
508
|
+
const interactivePromptsEnabled = isTTY && parsed.wrapper.noTui !== true;
|
|
509
|
+
try {
|
|
510
|
+
const resolveTarget = deps.resolveScanTarget ??
|
|
511
|
+
((resolverInput) => resolveScanTarget(resolverInput));
|
|
512
|
+
const stageClawhubTarget = deps.stageClawhubTarget ??
|
|
513
|
+
((stageInput) => stageClawhubTargetDefault(stageInput, {
|
|
514
|
+
pathExists,
|
|
515
|
+
resolveScanTarget: resolveTarget,
|
|
516
|
+
}));
|
|
517
|
+
resolvedTarget = await stageClawhubTarget({
|
|
518
|
+
sourceTarget: resolvedSourceTarget,
|
|
519
|
+
requestedVersion: parsed.requestedVersion ?? undefined,
|
|
520
|
+
cwd,
|
|
521
|
+
});
|
|
522
|
+
const noTui = parsed.wrapper.noTui === true || !isTTY;
|
|
523
|
+
const cliConfig = {
|
|
524
|
+
format: parsed.wrapper.format,
|
|
525
|
+
configPath: parsed.wrapper.configPath,
|
|
526
|
+
noTui,
|
|
527
|
+
};
|
|
528
|
+
const baseConfig = deps.resolveConfig({
|
|
529
|
+
scanTarget: resolvedTarget.scanTarget,
|
|
530
|
+
cli: cliConfig,
|
|
531
|
+
});
|
|
532
|
+
const config = parsed.wrapper.includeUserScope
|
|
533
|
+
? { ...baseConfig, scan_user_scope: true }
|
|
534
|
+
: baseConfig;
|
|
535
|
+
const { report, deepScanNotes } = await runScanAnalysis({
|
|
536
|
+
version: input.version,
|
|
537
|
+
scanTarget: resolvedTarget.scanTarget,
|
|
538
|
+
displayTarget: resolvedTarget.displayTarget,
|
|
539
|
+
explicitCandidates: resolvedTarget.explicitCandidates,
|
|
540
|
+
config,
|
|
541
|
+
options: {
|
|
542
|
+
noTui,
|
|
543
|
+
format: parsed.wrapper.format,
|
|
544
|
+
force: parsed.wrapper.force,
|
|
545
|
+
includeUserScope: parsed.wrapper.includeUserScope,
|
|
546
|
+
deep: parsed.wrapper.deep,
|
|
547
|
+
},
|
|
548
|
+
}, {
|
|
549
|
+
isTTY: deps.isTTY,
|
|
550
|
+
runScan: deps.runScan,
|
|
551
|
+
prepareScanDiscovery: deps.prepareScanDiscovery,
|
|
552
|
+
discoverDeepResources: deps.discoverDeepResources,
|
|
553
|
+
discoverLocalTextTargets: deps.discoverLocalTextTargets,
|
|
554
|
+
requestDeepScanConsent: interactivePromptsEnabled ? deps.requestDeepScanConsent : undefined,
|
|
555
|
+
requestDeepAgentSelection: interactivePromptsEnabled
|
|
556
|
+
? deps.requestDeepAgentSelection
|
|
557
|
+
: undefined,
|
|
558
|
+
requestMetaAgentCommandConsent: interactivePromptsEnabled
|
|
559
|
+
? deps.requestMetaAgentCommandConsent
|
|
560
|
+
: undefined,
|
|
561
|
+
runMetaAgentCommand: deps.runMetaAgentCommand,
|
|
562
|
+
executeDeepResource: deps.executeDeepResource,
|
|
563
|
+
});
|
|
564
|
+
const shouldUseTui = config.tui.enabled && isTTY && deps.renderTui !== undefined && noTui !== true;
|
|
565
|
+
const targetSummaryNote = config.output_format === "terminal"
|
|
566
|
+
? summarizeRequestedTargetFindings(report, resolvedTarget.displayTarget)
|
|
567
|
+
: null;
|
|
568
|
+
const scanNotes = config.output_format === "terminal"
|
|
569
|
+
? targetSummaryNote
|
|
570
|
+
? [...deepScanNotes, targetSummaryNote]
|
|
571
|
+
: deepScanNotes
|
|
572
|
+
: [];
|
|
573
|
+
if (shouldUseTui) {
|
|
574
|
+
deps.renderTui?.({
|
|
575
|
+
view: "dashboard",
|
|
576
|
+
report,
|
|
577
|
+
notices: scanNotes.length > 0 ? scanNotes : undefined,
|
|
578
|
+
});
|
|
579
|
+
deps.renderTui?.({ view: "summary", report });
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
for (const note of scanNotes) {
|
|
583
|
+
deps.stdout(note);
|
|
584
|
+
}
|
|
585
|
+
deps.stdout(renderByFormat(config.output_format, report));
|
|
586
|
+
}
|
|
587
|
+
if (report.summary.exit_code === 2 && parsed.wrapper.force !== true) {
|
|
588
|
+
deps.stderr("Dangerous findings detected. Aborting `clawhub install` (fail-closed).");
|
|
589
|
+
deps.setExitCode(2);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
if (shouldPromptForWarning(report, config, parsed.wrapper.force)) {
|
|
593
|
+
if (!interactivePromptsEnabled) {
|
|
594
|
+
deps.stderr("Warning findings detected. Aborting `clawhub install` in non-interactive mode (fail-closed). Use --cg-force to override.");
|
|
595
|
+
deps.setExitCode(1);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
const requestProceed = deps.requestWarningProceed ?? promptWarningProceed;
|
|
599
|
+
const approved = await requestProceed({
|
|
600
|
+
target: resolvedSourceTarget,
|
|
601
|
+
report,
|
|
602
|
+
});
|
|
603
|
+
if (!approved) {
|
|
604
|
+
deps.stderr("Install cancelled by user.");
|
|
605
|
+
deps.setExitCode(1);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
catch (error) {
|
|
611
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
612
|
+
if (parsed.wrapper.force !== true) {
|
|
613
|
+
deps.stderr(`Preflight scan failed (fail-closed): ${message}`);
|
|
614
|
+
deps.setExitCode(3);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
deps.stderr(`Preflight scan failed, continuing due to --cg-force: ${message}`);
|
|
618
|
+
}
|
|
619
|
+
finally {
|
|
620
|
+
if (resolvedTarget?.cleanup) {
|
|
621
|
+
try {
|
|
622
|
+
await resolvedTarget.cleanup();
|
|
623
|
+
}
|
|
624
|
+
catch (error) {
|
|
625
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
626
|
+
deps.stderr(`Scan target cleanup failed: ${message}`);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
finalizeLaunch(deps.launchClawhub(parsed.passthroughArgs, cwd), deps);
|
|
631
|
+
}
|
|
@@ -88,4 +88,13 @@ export interface ExecuteScanCommandDeps {
|
|
|
88
88
|
notices?: string[];
|
|
89
89
|
}) => void;
|
|
90
90
|
}
|
|
91
|
+
type ScanAnalysisDepKeys = "isTTY" | "runScan" | "prepareScanDiscovery" | "discoverDeepResources" | "discoverLocalTextTargets" | "requestDeepScanConsent" | "requestDeepAgentSelection" | "requestMetaAgentCommandConsent" | "runMetaAgentCommand" | "executeDeepResource";
|
|
92
|
+
export type ScanAnalysisDeps = Pick<ExecuteScanCommandDeps, ScanAnalysisDepKeys>;
|
|
93
|
+
export type ScanAnalysisInput = Omit<ExecuteScanCommandInput, "cwd">;
|
|
94
|
+
export interface ScanAnalysisResult {
|
|
95
|
+
report: CodeGateReport;
|
|
96
|
+
deepScanNotes: string[];
|
|
97
|
+
}
|
|
98
|
+
export declare function runScanAnalysis(input: ScanAnalysisInput, deps: ScanAnalysisDeps): Promise<ScanAnalysisResult>;
|
|
91
99
|
export declare function executeScanCommand(input: ExecuteScanCommandInput, deps: ExecuteScanCommandDeps): Promise<void>;
|
|
100
|
+
export {};
|