@clue-ai/cli 0.0.3 → 0.0.5
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 +43 -0
- package/bin/clue-cli.mjs +859 -0
- package/commands/claude-code/clue-init.md +8 -6
- package/commands/codex/clue-init.md +8 -6
- package/package.json +2 -2
- package/src/command-spec.mjs +21 -2
- package/src/contracts.mjs +5 -0
- package/src/fastapi-analyzer.mjs +150 -19
- package/src/init-tool.mjs +154 -55
- package/src/lifecycle-guard.mjs +141 -0
- package/src/lifecycle-init.mjs +210 -167
- package/src/path-policy.mjs +2 -0
- package/src/public-schema.cjs +26 -0
- package/src/semantic-ci.mjs +781 -32
- package/src/setup-check.mjs +435 -0
- package/src/setup-detect.mjs +198 -0
- package/src/setup-prepare.mjs +170 -0
- package/src/setup-tool.mjs +231 -114
- package/bin/clue-tool.mjs +0 -83
|
@@ -4,8 +4,7 @@ Run the Clue SDK initialization tool with structured inputs.
|
|
|
4
4
|
|
|
5
5
|
Required fields:
|
|
6
6
|
|
|
7
|
-
- `project_key`
|
|
8
|
-
- `service_key`
|
|
7
|
+
- `project_key` as an environment variable reference or placeholder, not a pasted secret value
|
|
9
8
|
- `framework`
|
|
10
9
|
- `backend_root_path`
|
|
11
10
|
- `environment`
|
|
@@ -19,9 +18,12 @@ Required secrets:
|
|
|
19
18
|
|
|
20
19
|
Behavior:
|
|
21
20
|
|
|
22
|
-
1.
|
|
23
|
-
2.
|
|
24
|
-
3.
|
|
25
|
-
4.
|
|
21
|
+
1. Follow the installed `clue-setup-orchestrator` skill and its monitoring gates.
|
|
22
|
+
2. Collect the required fields as structured input without asking for secret values.
|
|
23
|
+
3. Derive `service_key` from `backend_root_path` and repository structure; do not ask the user to type it.
|
|
24
|
+
4. Build the canonical init request.
|
|
25
|
+
5. Run `clue-ai init --request <generated-request.json> --repo .`.
|
|
26
|
+
6. Show the generated report and low-confidence review points.
|
|
26
27
|
|
|
27
28
|
Do not ask the user to write a free-form setup prompt.
|
|
29
|
+
Do not ask the user to paste project key, API key, service key, token, or environment values.
|
|
@@ -4,8 +4,7 @@ Run the Clue SDK initialization tool with structured inputs.
|
|
|
4
4
|
|
|
5
5
|
Required fields:
|
|
6
6
|
|
|
7
|
-
- `project_key`
|
|
8
|
-
- `service_key`
|
|
7
|
+
- `project_key` as an environment variable reference or placeholder, not a pasted secret value
|
|
9
8
|
- `framework`
|
|
10
9
|
- `backend_root_path`
|
|
11
10
|
- `environment`
|
|
@@ -19,9 +18,12 @@ Required secrets:
|
|
|
19
18
|
|
|
20
19
|
Behavior:
|
|
21
20
|
|
|
22
|
-
1.
|
|
23
|
-
2.
|
|
24
|
-
3.
|
|
25
|
-
4.
|
|
21
|
+
1. Follow the installed `clue-setup-orchestrator` skill and its monitoring gates.
|
|
22
|
+
2. Collect the required fields as structured input without asking for secret values.
|
|
23
|
+
3. Derive `service_key` from `backend_root_path` and repository structure; do not ask the user to type it.
|
|
24
|
+
4. Build the canonical init request.
|
|
25
|
+
5. Run `clue-ai init --request <generated-request.json> --repo .`.
|
|
26
|
+
6. Show the generated report and low-confidence review points.
|
|
26
27
|
|
|
27
28
|
Do not ask the user to write a free-form setup prompt.
|
|
29
|
+
Do not ask the user to paste project key, API key, service key, token, or environment values.
|
package/package.json
CHANGED
package/src/command-spec.mjs
CHANGED
|
@@ -7,7 +7,6 @@ export const REQUIRED_SECRET_NAMES = [
|
|
|
7
7
|
|
|
8
8
|
export const CLUE_INIT_COMMAND_FIELDS = [
|
|
9
9
|
"project_key",
|
|
10
|
-
"service_key",
|
|
11
10
|
"framework",
|
|
12
11
|
"backend_root_path",
|
|
13
12
|
"environment",
|
|
@@ -45,15 +44,35 @@ const normalizeStringArray = (value, fallback, field) => {
|
|
|
45
44
|
.map((entry) => entry.trim());
|
|
46
45
|
};
|
|
47
46
|
|
|
47
|
+
const deriveServiceKeyFromBackendRootPath = (backendRootPath) => {
|
|
48
|
+
const segment = backendRootPath
|
|
49
|
+
.split("/")
|
|
50
|
+
.map((part) => part.trim())
|
|
51
|
+
.filter(Boolean)
|
|
52
|
+
.at(-1);
|
|
53
|
+
const normalized = segment
|
|
54
|
+
?.toLowerCase()
|
|
55
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
56
|
+
.replace(/^-+|-+$/g, "");
|
|
57
|
+
if (!normalized) {
|
|
58
|
+
throw new Error(`service_key cannot be derived from backend_root_path for ${CLUE_INIT_COMMAND_NAME}`);
|
|
59
|
+
}
|
|
60
|
+
return normalized;
|
|
61
|
+
};
|
|
62
|
+
|
|
48
63
|
export const buildClueInitRequestFromCommandInput = ({
|
|
49
64
|
targetTool,
|
|
50
65
|
input,
|
|
51
66
|
}) => {
|
|
52
67
|
const backendRootPath = requireField(input, "backend_root_path");
|
|
68
|
+
const serviceKey =
|
|
69
|
+
typeof input?.service_key === "string" && input.service_key.trim()
|
|
70
|
+
? input.service_key.trim()
|
|
71
|
+
: deriveServiceKeyFromBackendRootPath(backendRootPath);
|
|
53
72
|
return {
|
|
54
73
|
target_tool: targetTool,
|
|
55
74
|
project_key: requireField(input, "project_key"),
|
|
56
|
-
service_key:
|
|
75
|
+
service_key: serviceKey,
|
|
57
76
|
framework: requireField(input, "framework"),
|
|
58
77
|
environment: requireField(input, "environment"),
|
|
59
78
|
allowed_source_paths: normalizeStringArray(
|
package/src/contracts.mjs
CHANGED
|
@@ -7,6 +7,7 @@ const {
|
|
|
7
7
|
clueInitToolRequestSchema,
|
|
8
8
|
clueInitToolReportSchema,
|
|
9
9
|
semanticSnapshotRequestSchema,
|
|
10
|
+
semanticSnapshotResponseSchema,
|
|
10
11
|
} = schemaPackage;
|
|
11
12
|
|
|
12
13
|
const formatSchemaError = (result, field) => {
|
|
@@ -122,3 +123,7 @@ export const buildOperationSourceKey = (method, pathTemplate) =>
|
|
|
122
123
|
export const validateSemanticSnapshotRequest = (input) => {
|
|
123
124
|
return parseWithSchema(semanticSnapshotRequestSchema, input, "semantic snapshot");
|
|
124
125
|
};
|
|
126
|
+
|
|
127
|
+
export const validateSemanticSnapshotResponse = (input) => {
|
|
128
|
+
return parseWithSchema(semanticSnapshotResponseSchema, input, "semantic snapshot response");
|
|
129
|
+
};
|
package/src/fastapi-analyzer.mjs
CHANGED
|
@@ -13,6 +13,7 @@ const FUNCTION_PATTERN = /(?:async\s+def|def)\s+(?<name>[A-Za-z_][A-Za-z0-9_]*)\
|
|
|
13
13
|
const ROUTER_ASSIGNMENT = /\b(?<name>[A-Za-z_][A-Za-z0-9_]*)\s*=\s*APIRouter\s*\(/g;
|
|
14
14
|
const INCLUDE_ROUTER_CALL = /\b(?<owner>[A-Za-z_][A-Za-z0-9_]*)\.include_router\s*\(/g;
|
|
15
15
|
const FROM_IMPORT = /^\s*from\s+(?<module>[A-Za-z0-9_.]+|\.+[A-Za-z0-9_.]*)\s+import\s+(?<names>[A-Za-z0-9_,\s]+)$/gm;
|
|
16
|
+
const IMPORT_MODULE = /^\s*import\s+(?<modules>[A-Za-z0-9_.,\s]+)$/gm;
|
|
16
17
|
|
|
17
18
|
const sha256 = (value) => `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
18
19
|
|
|
@@ -125,6 +126,41 @@ const resolveImportFile = ({ filesByRelativePath, currentRelativePath, moduleNam
|
|
|
125
126
|
return null;
|
|
126
127
|
};
|
|
127
128
|
|
|
129
|
+
const resolveFromImportEntry = ({
|
|
130
|
+
filesByRelativePath,
|
|
131
|
+
currentRelativePath,
|
|
132
|
+
moduleName,
|
|
133
|
+
importedName,
|
|
134
|
+
}) => {
|
|
135
|
+
const childModuleName =
|
|
136
|
+
moduleName.endsWith(".") || moduleName === "."
|
|
137
|
+
? `${moduleName}${importedName}`
|
|
138
|
+
: `${moduleName}.${importedName}`;
|
|
139
|
+
const childFile = resolveImportFile({
|
|
140
|
+
filesByRelativePath,
|
|
141
|
+
currentRelativePath,
|
|
142
|
+
moduleName: childModuleName,
|
|
143
|
+
});
|
|
144
|
+
if (childFile) {
|
|
145
|
+
return {
|
|
146
|
+
file: childFile,
|
|
147
|
+
name: undefined,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
const moduleFile = resolveImportFile({
|
|
151
|
+
filesByRelativePath,
|
|
152
|
+
currentRelativePath,
|
|
153
|
+
moduleName,
|
|
154
|
+
});
|
|
155
|
+
if (!moduleFile) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
file: moduleFile,
|
|
160
|
+
name: importedName,
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
|
|
128
164
|
const parseRouterAssignments = (source, relativePath) => {
|
|
129
165
|
const prefixes = new Map();
|
|
130
166
|
for (const match of source.matchAll(ROUTER_ASSIGNMENT)) {
|
|
@@ -141,44 +177,99 @@ const parseRouterAssignments = (source, relativePath) => {
|
|
|
141
177
|
const parseImports = ({ source, currentRelativePath, filesByRelativePath }) => {
|
|
142
178
|
const imports = new Map();
|
|
143
179
|
for (const match of source.matchAll(FROM_IMPORT)) {
|
|
144
|
-
const targetFile = resolveImportFile({
|
|
145
|
-
filesByRelativePath,
|
|
146
|
-
currentRelativePath,
|
|
147
|
-
moduleName: match.groups.module,
|
|
148
|
-
});
|
|
149
|
-
if (!targetFile) {
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
180
|
for (const entry of parseImportNames(match.groups.names)) {
|
|
181
|
+
const resolved = resolveFromImportEntry({
|
|
182
|
+
filesByRelativePath,
|
|
183
|
+
currentRelativePath,
|
|
184
|
+
moduleName: match.groups.module,
|
|
185
|
+
importedName: entry.importedName,
|
|
186
|
+
});
|
|
187
|
+
if (!resolved) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
153
190
|
imports.set(entry.localName, {
|
|
191
|
+
file: resolved.file,
|
|
192
|
+
name: resolved.name,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
for (const match of source.matchAll(IMPORT_MODULE)) {
|
|
197
|
+
for (const entry of parseImportNames(match.groups.modules)) {
|
|
198
|
+
const targetFile = resolveImportFile({
|
|
199
|
+
filesByRelativePath,
|
|
200
|
+
currentRelativePath,
|
|
201
|
+
moduleName: entry.importedName,
|
|
202
|
+
});
|
|
203
|
+
if (!targetFile) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
imports.set(entry.localName.split(".").at(-1), {
|
|
154
207
|
file: targetFile,
|
|
155
|
-
name:
|
|
208
|
+
name: undefined,
|
|
156
209
|
});
|
|
157
210
|
}
|
|
158
211
|
}
|
|
159
212
|
return imports;
|
|
160
213
|
};
|
|
161
214
|
|
|
215
|
+
const routerKey = (file, routerName) => `${file}::${routerName}`;
|
|
216
|
+
|
|
217
|
+
const resolveRouterTarget = ({ target, relativePath, imports }) => {
|
|
218
|
+
const parts = target.split(".");
|
|
219
|
+
if (parts.length === 1) {
|
|
220
|
+
const imported = imports.get(target);
|
|
221
|
+
return {
|
|
222
|
+
file: imported?.file ?? relativePath,
|
|
223
|
+
routerName: imported?.name ?? target,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
const imported = imports.get(parts[0]);
|
|
227
|
+
if (!imported?.file || imported.name) {
|
|
228
|
+
throw new Error(`${relativePath}: include_router dotted target cannot be analyzed safely`);
|
|
229
|
+
}
|
|
230
|
+
if (parts.length !== 2) {
|
|
231
|
+
throw new Error(`${relativePath}: include_router dotted target cannot be analyzed safely`);
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
file: imported.file,
|
|
235
|
+
routerName: parts[1],
|
|
236
|
+
};
|
|
237
|
+
};
|
|
238
|
+
|
|
162
239
|
const parseIncludeRouters = ({ source, relativePath, imports }) => {
|
|
163
240
|
const includes = [];
|
|
164
241
|
for (const match of source.matchAll(INCLUDE_ROUTER_CALL)) {
|
|
165
242
|
const openParenIndex = match.index + match[0].length - 1;
|
|
166
243
|
const args = readCallArgs(source, openParenIndex);
|
|
167
|
-
const routerMatch = /^\s*(?<router>[A-Za-z_][A-Za-z0-9_]*)/.exec(args);
|
|
244
|
+
const routerMatch = /^\s*(?<router>[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*)/.exec(args);
|
|
168
245
|
if (!routerMatch?.groups?.router) {
|
|
169
246
|
throw new Error(`${relativePath}: include_router target cannot be analyzed safely`);
|
|
170
247
|
}
|
|
171
|
-
const
|
|
172
|
-
|
|
248
|
+
const resolved = resolveRouterTarget({
|
|
249
|
+
target: routerMatch.groups.router,
|
|
250
|
+
relativePath,
|
|
251
|
+
imports,
|
|
252
|
+
});
|
|
173
253
|
includes.push({
|
|
174
|
-
|
|
175
|
-
|
|
254
|
+
ownerName: match.groups.owner,
|
|
255
|
+
file: resolved.file,
|
|
256
|
+
routerName: resolved.routerName,
|
|
176
257
|
prefix: extractPrefix({ args, relativePath, callName: "include_router" }),
|
|
177
258
|
});
|
|
178
259
|
}
|
|
179
260
|
return includes;
|
|
180
261
|
};
|
|
181
262
|
|
|
263
|
+
const addIncludePrefix = (prefixesByRouterKey, key, prefix) => {
|
|
264
|
+
const current = prefixesByRouterKey.get(key) ?? [];
|
|
265
|
+
if (!current.includes(prefix)) {
|
|
266
|
+
current.push(prefix);
|
|
267
|
+
prefixesByRouterKey.set(key, current);
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
return false;
|
|
271
|
+
};
|
|
272
|
+
|
|
182
273
|
const findNextFunction = (source, fromIndex) => {
|
|
183
274
|
FUNCTION_PATTERN.lastIndex = fromIndex;
|
|
184
275
|
const match = FUNCTION_PATTERN.exec(source);
|
|
@@ -270,6 +361,14 @@ export const analyzeFastApiRoutes = async ({ repoRoot, files }) => {
|
|
|
270
361
|
});
|
|
271
362
|
}
|
|
272
363
|
|
|
364
|
+
const routerPrefixesByKey = new Map();
|
|
365
|
+
for (const record of records) {
|
|
366
|
+
for (const [routerName, prefix] of record.routerPrefixes.entries()) {
|
|
367
|
+
routerPrefixesByKey.set(routerKey(record.relativePath, routerName), prefix);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const includeEdges = [];
|
|
273
372
|
const includePrefixes = new Map();
|
|
274
373
|
for (const record of records) {
|
|
275
374
|
for (const include of parseIncludeRouters({
|
|
@@ -277,10 +376,42 @@ export const analyzeFastApiRoutes = async ({ repoRoot, files }) => {
|
|
|
277
376
|
relativePath: record.relativePath,
|
|
278
377
|
imports: record.imports,
|
|
279
378
|
})) {
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
379
|
+
const parentKey = record.routerPrefixes.has(include.ownerName)
|
|
380
|
+
? routerKey(record.relativePath, include.ownerName)
|
|
381
|
+
: null;
|
|
382
|
+
includeEdges.push({
|
|
383
|
+
parentKey,
|
|
384
|
+
targetKey: routerKey(include.file, include.routerName),
|
|
385
|
+
prefix: include.prefix,
|
|
386
|
+
});
|
|
387
|
+
if (!parentKey) {
|
|
388
|
+
addIncludePrefix(
|
|
389
|
+
includePrefixes,
|
|
390
|
+
routerKey(include.file, include.routerName),
|
|
391
|
+
include.prefix,
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
let changed = true;
|
|
397
|
+
while (changed) {
|
|
398
|
+
changed = false;
|
|
399
|
+
for (const edge of includeEdges) {
|
|
400
|
+
if (!edge.parentKey) {
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
for (const parentPrefix of includePrefixes.get(edge.parentKey) ?? []) {
|
|
404
|
+
changed =
|
|
405
|
+
addIncludePrefix(
|
|
406
|
+
includePrefixes,
|
|
407
|
+
edge.targetKey,
|
|
408
|
+
combinePaths(
|
|
409
|
+
parentPrefix,
|
|
410
|
+
routerPrefixesByKey.get(edge.parentKey) ?? "",
|
|
411
|
+
edge.prefix,
|
|
412
|
+
),
|
|
413
|
+
) || changed;
|
|
414
|
+
}
|
|
284
415
|
}
|
|
285
416
|
}
|
|
286
417
|
|
|
@@ -289,7 +420,7 @@ export const analyzeFastApiRoutes = async ({ repoRoot, files }) => {
|
|
|
289
420
|
const buildForDecorator = ({ match, method, decoratorPath }) => {
|
|
290
421
|
const routerName = match.groups.router;
|
|
291
422
|
const functionInfo = findNextFunction(record.source, match.index + match[0].length);
|
|
292
|
-
const key =
|
|
423
|
+
const key = routerKey(record.relativePath, routerName);
|
|
293
424
|
const localRouterPrefix = record.routerPrefixes.get(routerName) ?? "";
|
|
294
425
|
const prefixes = includePrefixes.get(key) ?? [""];
|
|
295
426
|
for (const includePrefix of prefixes) {
|
package/src/init-tool.mjs
CHANGED
|
@@ -1,86 +1,185 @@
|
|
|
1
1
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { buildInitReport, validateInitRequest } from "./contracts.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
applyLifecyclePlan,
|
|
6
|
+
planLifecycleInsertions,
|
|
7
|
+
} from "./lifecycle-init.mjs";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_SEMANTIC_WORKFLOW_PATH =
|
|
10
|
+
".github/workflows/clue-semantic-snapshot.yml";
|
|
11
|
+
|
|
12
|
+
const nonEmpty = (value, field) => {
|
|
13
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
14
|
+
throw new Error(`${field} is required`);
|
|
15
|
+
}
|
|
16
|
+
return value.trim();
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const normalizeStringArray = (value, field, { min = 0 } = {}) => {
|
|
20
|
+
if (!Array.isArray(value)) {
|
|
21
|
+
throw new Error(`${field} must be an array`);
|
|
22
|
+
}
|
|
23
|
+
const result = value
|
|
24
|
+
.filter((entry) => typeof entry === "string" && entry.trim())
|
|
25
|
+
.map((entry) => entry.trim());
|
|
26
|
+
if (result.length < min) {
|
|
27
|
+
throw new Error(`${field} must include at least ${min} item(s)`);
|
|
28
|
+
}
|
|
29
|
+
return [...new Set(result)];
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const splitCsv = (value) =>
|
|
33
|
+
typeof value === "string"
|
|
34
|
+
? value
|
|
35
|
+
.split(",")
|
|
36
|
+
.map((entry) => entry.trim())
|
|
37
|
+
.filter(Boolean)
|
|
38
|
+
: [];
|
|
39
|
+
|
|
40
|
+
const deriveServiceKeyFromPath = (backendRootPath) => {
|
|
41
|
+
const segment = backendRootPath
|
|
42
|
+
.split("/")
|
|
43
|
+
.map((part) => part.trim())
|
|
44
|
+
.filter(Boolean)
|
|
45
|
+
.at(-1);
|
|
46
|
+
const normalized = segment
|
|
47
|
+
?.toLowerCase()
|
|
48
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
49
|
+
.replace(/^-+|-+$/g, "");
|
|
50
|
+
if (!normalized) {
|
|
51
|
+
throw new Error("service-key cannot be derived from backend-root-path");
|
|
52
|
+
}
|
|
53
|
+
return normalized;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const buildSemanticWorkflowRequestFromFlags = (flags) => {
|
|
57
|
+
const backendRootPath = nonEmpty(flags.backendRootPath, "backend-root-path");
|
|
58
|
+
const allowedSourcePaths = splitCsv(flags.allowedSourcePaths);
|
|
59
|
+
return {
|
|
60
|
+
ci_workflow_path:
|
|
61
|
+
typeof flags.workflowPath === "string" && flags.workflowPath.trim()
|
|
62
|
+
? flags.workflowPath.trim()
|
|
63
|
+
: DEFAULT_SEMANTIC_WORKFLOW_PATH,
|
|
64
|
+
service_key:
|
|
65
|
+
typeof flags.serviceKey === "string" && flags.serviceKey.trim()
|
|
66
|
+
? flags.serviceKey.trim()
|
|
67
|
+
: deriveServiceKeyFromPath(backendRootPath),
|
|
68
|
+
framework: nonEmpty(flags.framework, "framework"),
|
|
69
|
+
allowed_source_paths: normalizeStringArray(
|
|
70
|
+
allowedSourcePaths.length ? allowedSourcePaths : [backendRootPath],
|
|
71
|
+
"allowed-source-paths",
|
|
72
|
+
{ min: 1 },
|
|
73
|
+
),
|
|
74
|
+
excluded_source_paths: normalizeStringArray(
|
|
75
|
+
splitCsv(flags.excludedSourcePaths),
|
|
76
|
+
"excluded-source-paths",
|
|
77
|
+
),
|
|
78
|
+
};
|
|
79
|
+
};
|
|
5
80
|
|
|
6
81
|
const workflowRequestPayload = (request) =>
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
82
|
+
JSON.stringify(
|
|
83
|
+
{
|
|
84
|
+
project_key: "${{ vars.CLUE_PROJECT_KEY }}",
|
|
85
|
+
environment: "${{ vars.CLUE_ENVIRONMENT }}",
|
|
86
|
+
service_key: request.service_key,
|
|
87
|
+
repository: {
|
|
88
|
+
provider: "github",
|
|
89
|
+
repository_id: "${{ github.repository_id }}",
|
|
90
|
+
merge_commit: "${{ github.sha }}",
|
|
91
|
+
workflow_run_id: "${{ github.run_id }}",
|
|
92
|
+
},
|
|
93
|
+
service: {
|
|
94
|
+
service_key: request.service_key,
|
|
95
|
+
root_path: request.allowed_source_paths[0],
|
|
96
|
+
framework: request.framework,
|
|
97
|
+
language: "python",
|
|
98
|
+
},
|
|
99
|
+
allowed_source_paths: request.allowed_source_paths,
|
|
100
|
+
excluded_source_paths: request.excluded_source_paths,
|
|
101
|
+
clue_api_base_url: "${{ vars.CLUE_API_BASE_URL }}",
|
|
102
|
+
ai_model: "${{ vars.CLUE_AI_MODEL }}",
|
|
103
|
+
},
|
|
104
|
+
null,
|
|
105
|
+
10,
|
|
106
|
+
).trim();
|
|
107
|
+
|
|
108
|
+
const indentMultiline = (value, spaces) => {
|
|
109
|
+
const prefix = " ".repeat(spaces);
|
|
110
|
+
return value
|
|
111
|
+
.split("\n")
|
|
112
|
+
.map((line) => `${prefix}${line}`)
|
|
113
|
+
.join("\n");
|
|
114
|
+
};
|
|
35
115
|
|
|
36
116
|
const workflowTemplate = (request) => `name: Clue Semantic Snapshot
|
|
37
117
|
|
|
38
118
|
on:
|
|
39
119
|
push:
|
|
40
120
|
|
|
121
|
+
permissions:
|
|
122
|
+
contents: read
|
|
123
|
+
|
|
41
124
|
jobs:
|
|
42
125
|
semantic-snapshot:
|
|
43
126
|
if: github.ref_name == github.event.repository.default_branch
|
|
44
127
|
runs-on: ubuntu-latest
|
|
45
128
|
steps:
|
|
46
129
|
- uses: actions/checkout@v4
|
|
130
|
+
with:
|
|
131
|
+
persist-credentials: false
|
|
47
132
|
- uses: actions/setup-node@v4
|
|
48
133
|
with:
|
|
49
134
|
node-version: "20"
|
|
50
135
|
- name: Run Clue semantic generation
|
|
51
|
-
continue-on-error: true
|
|
52
136
|
env:
|
|
53
137
|
CLUE_API_KEY: \${{ secrets.CLUE_API_KEY }}
|
|
54
138
|
AI_PROVIDER_API_KEY: \${{ secrets.AI_PROVIDER_API_KEY }}
|
|
139
|
+
CLUE_SEMANTIC_REQUEST_JSON: |
|
|
140
|
+
${indentMultiline(workflowRequestPayload(request), 12)}
|
|
55
141
|
run: |
|
|
56
|
-
|
|
57
|
-
cat > .clue/semantic-request.runtime.json <<'JSON'
|
|
58
|
-
${workflowRequestPayload(request)}
|
|
59
|
-
JSON
|
|
60
|
-
npx @clue-ai/cli semantic-ci --request .clue/semantic-request.runtime.json --repo .
|
|
142
|
+
npx @clue-ai/cli semantic-ci --request-env CLUE_SEMANTIC_REQUEST_JSON --repo .
|
|
61
143
|
`;
|
|
62
144
|
|
|
145
|
+
export const writeSemanticWorkflow = async ({ repoRoot, request }) => {
|
|
146
|
+
const workflowPath = join(repoRoot, request.ci_workflow_path);
|
|
147
|
+
await mkdir(dirname(workflowPath), { recursive: true });
|
|
148
|
+
await writeFile(workflowPath, workflowTemplate(request), "utf8");
|
|
149
|
+
return {
|
|
150
|
+
ci_workflow_path: request.ci_workflow_path,
|
|
151
|
+
ci_workflow_added: true,
|
|
152
|
+
required_secrets: ["CLUE_API_KEY", "AI_PROVIDER_API_KEY"],
|
|
153
|
+
required_variables: [
|
|
154
|
+
"CLUE_PROJECT_KEY",
|
|
155
|
+
"CLUE_ENVIRONMENT",
|
|
156
|
+
"CLUE_API_BASE_URL",
|
|
157
|
+
"CLUE_AI_MODEL",
|
|
158
|
+
],
|
|
159
|
+
runtime_request_committed: false,
|
|
160
|
+
semantic_generation_timing: "after_merge_ci",
|
|
161
|
+
allowed_source_paths: request.allowed_source_paths,
|
|
162
|
+
excluded_source_paths: request.excluded_source_paths,
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
|
|
63
166
|
export const runInitTool = async ({
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
167
|
+
repoRoot,
|
|
168
|
+
request: rawRequest,
|
|
169
|
+
env = process.env,
|
|
170
|
+
lifecyclePlanner = planLifecycleInsertions,
|
|
68
171
|
}) => {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
plan: lifecyclePlan,
|
|
77
|
-
});
|
|
172
|
+
const request = validateInitRequest(rawRequest);
|
|
173
|
+
await writeSemanticWorkflow({ repoRoot, request });
|
|
174
|
+
const lifecyclePlan = await lifecyclePlanner({ repoRoot, request, env });
|
|
175
|
+
const lifecycleResult = await applyLifecyclePlan({
|
|
176
|
+
repoRoot,
|
|
177
|
+
plan: lifecyclePlan,
|
|
178
|
+
});
|
|
78
179
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
],
|
|
85
|
-
});
|
|
180
|
+
return buildInitReport({
|
|
181
|
+
request,
|
|
182
|
+
lifecycleInsertions: lifecycleResult.lifecycleInsertions,
|
|
183
|
+
warnings: [...lifecycleResult.warnings],
|
|
184
|
+
});
|
|
86
185
|
};
|