@clue-ai/cli 0.0.5 → 0.0.6
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 +17 -3
- package/bin/clue-cli.mjs +805 -762
- package/commands/claude-code/clue-init.md +7 -1
- package/commands/codex/clue-init.md +7 -1
- package/package.json +1 -1
- package/src/ai-provider.mjs +146 -0
- package/src/command-spec.mjs +7 -7
- package/src/contracts.mjs +49 -15
- package/src/init-tool.mjs +158 -124
- package/src/lifecycle-init.mjs +180 -205
- package/src/public-schema.cjs +1 -1
- package/src/semantic-ci.mjs +122 -163
- package/src/setup-check.mjs +373 -372
- package/src/setup-prepare.mjs +266 -147
- package/src/setup-tool.mjs +231 -229
package/src/setup-check.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { access, readFile } from "node:fs/promises";
|
|
2
2
|
import { join, relative, resolve } from "node:path";
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
findLifecycleCallApiNames,
|
|
5
|
+
findLifecycleGuardViolations,
|
|
6
6
|
} from "./lifecycle-guard.mjs";
|
|
7
7
|
import { listAllowedSourceFiles } from "./path-policy.mjs";
|
|
8
8
|
import { runSemanticInventory } from "./semantic-ci.mjs";
|
|
@@ -10,426 +10,427 @@ import { runSemanticInventory } from "./semantic-ci.mjs";
|
|
|
10
10
|
const DEFAULT_WORKFLOW_PATH = ".github/workflows/clue-semantic-snapshot.yml";
|
|
11
11
|
|
|
12
12
|
const disallowedWorkflowMetadataPattern =
|
|
13
|
-
|
|
13
|
+
/github\.(actor|triggering_actor|repository_owner)|github\.event\.sender|github\.event\.repository\.name|"default_branch"\s*:/;
|
|
14
14
|
const SETUP_SKILLS = [
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
"clue-setup-orchestrator",
|
|
16
|
+
"clue-route-semantic-snapshot",
|
|
17
|
+
"clue-semantic-ci",
|
|
18
|
+
"clue-sdk-instrumentation",
|
|
19
|
+
"clue-setup-audit",
|
|
20
|
+
"clue-local-verification",
|
|
21
|
+
"clue-setup-report",
|
|
22
22
|
];
|
|
23
23
|
const TARGET_SKILL_ROOTS = {
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
codex: [".agents", "skills"],
|
|
25
|
+
claude_code: [".claude", "skills"],
|
|
26
26
|
};
|
|
27
27
|
const SOURCE_EXTENSIONS = [".py", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
|
|
28
28
|
const REQUIRED_LIFECYCLE_APIS = [
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
"ClueInit",
|
|
30
|
+
"ClueIdentify",
|
|
31
|
+
"ClueSetAccount",
|
|
32
|
+
"ClueLogout",
|
|
33
33
|
];
|
|
34
34
|
const BACKEND_SDK_MARKERS = [
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
"clue-fastapi-sdk",
|
|
36
|
+
"clue-django-sdk",
|
|
37
|
+
"clue-python-sdk-core",
|
|
38
|
+
"clue_fastapi_sdk",
|
|
39
|
+
"clue_django_sdk",
|
|
40
|
+
"clue_python_sdk_core",
|
|
41
41
|
];
|
|
42
42
|
const DEPENDENCY_FILE_CANDIDATES = [
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
43
|
+
"package.json",
|
|
44
|
+
"pnpm-lock.yaml",
|
|
45
|
+
"package-lock.json",
|
|
46
|
+
"yarn.lock",
|
|
47
|
+
"requirements.txt",
|
|
48
|
+
"requirements-dev.txt",
|
|
49
|
+
"pyproject.toml",
|
|
50
|
+
"poetry.lock",
|
|
51
|
+
"Pipfile",
|
|
52
52
|
];
|
|
53
53
|
const exists = async (path) => {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
try {
|
|
55
|
+
await access(path);
|
|
56
|
+
return true;
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
60
|
};
|
|
61
61
|
|
|
62
62
|
const normalizeTarget = (target) => {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
63
|
+
if (typeof target !== "string" || target.trim() === "") return undefined;
|
|
64
|
+
const normalized = target
|
|
65
|
+
.trim()
|
|
66
|
+
.toLowerCase()
|
|
67
|
+
.replace(/[\s-]+/g, "_");
|
|
68
|
+
if (!TARGET_SKILL_ROOTS[normalized]) {
|
|
69
|
+
throw new Error("target must be codex or claude_code");
|
|
70
|
+
}
|
|
71
|
+
return normalized;
|
|
72
72
|
};
|
|
73
73
|
|
|
74
74
|
const addCheck = (checks, id, passed, summary, details = {}) => {
|
|
75
|
-
|
|
75
|
+
checks.push({ id, passed, summary, ...details });
|
|
76
76
|
};
|
|
77
77
|
|
|
78
78
|
const readAllowedSourceText = async ({
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
repoRoot,
|
|
80
|
+
allowedSourcePaths,
|
|
81
|
+
excludedSourcePaths,
|
|
82
82
|
}) => {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
83
|
+
const files = await listAllowedSourceFiles({
|
|
84
|
+
repoRoot,
|
|
85
|
+
allowedSourcePaths,
|
|
86
|
+
excludedSourcePaths,
|
|
87
|
+
extensions: SOURCE_EXTENSIONS,
|
|
88
|
+
});
|
|
89
|
+
const sources = [];
|
|
90
|
+
for (const absolutePath of files) {
|
|
91
|
+
sources.push({
|
|
92
|
+
file_path: relative(resolve(repoRoot), absolutePath),
|
|
93
|
+
text: await readFile(absolutePath, "utf8"),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return sources;
|
|
97
97
|
};
|
|
98
98
|
|
|
99
99
|
const readDependencyText = async ({ repoRoot, roots }) => {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
100
|
+
const candidatePaths = [
|
|
101
|
+
...DEPENDENCY_FILE_CANDIDATES,
|
|
102
|
+
...roots.flatMap((root) =>
|
|
103
|
+
DEPENDENCY_FILE_CANDIDATES.map((file) => join(root, file)),
|
|
104
|
+
),
|
|
105
|
+
];
|
|
106
|
+
const sources = [];
|
|
107
|
+
for (const path of [...new Set(candidatePaths)]) {
|
|
108
|
+
const absolutePath = join(repoRoot, path);
|
|
109
|
+
if (!(await exists(absolutePath))) continue;
|
|
110
|
+
sources.push({
|
|
111
|
+
file_path: path,
|
|
112
|
+
text: await readFile(absolutePath, "utf8"),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return sources;
|
|
116
116
|
};
|
|
117
117
|
|
|
118
118
|
const setupSourcePaths = async ({ repoRoot, request, includeFrontend }) => {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
119
|
+
const requested = request?.allowed_source_paths?.length
|
|
120
|
+
? request.allowed_source_paths
|
|
121
|
+
: ["."];
|
|
122
|
+
if (!includeFrontend) return requested;
|
|
123
|
+
const candidates = [
|
|
124
|
+
...requested,
|
|
125
|
+
"frontend/src",
|
|
126
|
+
"src",
|
|
127
|
+
"app",
|
|
128
|
+
"apps/web/src",
|
|
129
|
+
"packages/frontend",
|
|
130
|
+
];
|
|
131
|
+
const existing = [];
|
|
132
|
+
for (const candidate of [...new Set(candidates)]) {
|
|
133
|
+
if (await exists(join(repoRoot, candidate))) {
|
|
134
|
+
existing.push(candidate);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return existing.length ? existing : requested;
|
|
138
138
|
};
|
|
139
139
|
|
|
140
140
|
const secretLeakPatterns = [
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
141
|
+
/pk_(live|test)_[A-Za-z0-9_-]+/,
|
|
142
|
+
/sk_(live|test)_[A-Za-z0-9_-]+/,
|
|
143
|
+
/npm_[A-Za-z0-9]{20,}/,
|
|
144
|
+
/CLUE_API_KEY\s*[:=]\s*["'][^"']+["']/,
|
|
145
|
+
/CLUE_AI_PROVIDER_API_KEY\s*[:=]\s*["'][^"']+["']/,
|
|
146
146
|
];
|
|
147
147
|
|
|
148
148
|
const findSecretLeaks = (sources) =>
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
149
|
+
sources.flatMap((source) =>
|
|
150
|
+
secretLeakPatterns.some((pattern) => pattern.test(source.text))
|
|
151
|
+
? [source.file_path]
|
|
152
|
+
: [],
|
|
153
|
+
);
|
|
154
154
|
|
|
155
155
|
const startsWithRoot = (filePath, root) =>
|
|
156
|
-
|
|
156
|
+
filePath === root || filePath.startsWith(`${root.replace(/\/+$/, "")}/`);
|
|
157
157
|
|
|
158
158
|
const hasAnyMarker = (text, markers) =>
|
|
159
|
-
|
|
159
|
+
markers.some((marker) => text.includes(marker));
|
|
160
160
|
|
|
161
161
|
const checkSdkLifecycle = ({
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
162
|
+
backendRootPaths = [],
|
|
163
|
+
dependencySources = [],
|
|
164
|
+
sources,
|
|
165
165
|
}) => {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
166
|
+
const combined = sources.map((source) => source.text).join("\n");
|
|
167
|
+
const backendSources = sources.filter((source) =>
|
|
168
|
+
backendRootPaths.some((root) => startsWithRoot(source.file_path, root)),
|
|
169
|
+
);
|
|
170
|
+
const backendCombined = backendSources
|
|
171
|
+
.map((source) => source.text)
|
|
172
|
+
.join("\n");
|
|
173
|
+
const dependencyCombined = dependencySources
|
|
174
|
+
.map((source) => source.text)
|
|
175
|
+
.join("\n");
|
|
176
|
+
const foundApiNames = findLifecycleCallApiNames(combined);
|
|
177
|
+
const backendFoundApiNames = findLifecycleCallApiNames(backendCombined);
|
|
178
|
+
const foundApis = REQUIRED_LIFECYCLE_APIS.filter((api) =>
|
|
179
|
+
foundApiNames.includes(api),
|
|
180
|
+
);
|
|
181
|
+
const missingApis = REQUIRED_LIFECYCLE_APIS.filter(
|
|
182
|
+
(api) => !foundApis.includes(api),
|
|
183
|
+
);
|
|
184
|
+
const noOpPattern =
|
|
185
|
+
/window\.Clue(?:Init|Identify|SetAccount|Logout)|(?:function|const|let|var)\s+Clue(?:Init|Identify|SetAccount|Logout)\b/;
|
|
186
|
+
const componentLifecycleInitFiles = sources
|
|
187
|
+
.filter((source) =>
|
|
188
|
+
/useEffect\s*\([\s\S]{0,1200}ClueInit/.test(source.text),
|
|
189
|
+
)
|
|
190
|
+
.map((source) => source.file_path);
|
|
191
|
+
const unguardedLifecycleCalls = sources.flatMap((source) =>
|
|
192
|
+
findLifecycleGuardViolations(source.text).map((violation) => ({
|
|
193
|
+
file_path: source.file_path,
|
|
194
|
+
...violation,
|
|
195
|
+
})),
|
|
196
|
+
);
|
|
197
|
+
const unguardedLifecycleFiles = [
|
|
198
|
+
...new Set(unguardedLifecycleCalls.map((violation) => violation.file_path)),
|
|
199
|
+
];
|
|
200
|
+
const backendPresent = backendSources.length > 0;
|
|
201
|
+
const backendSdkPresent =
|
|
202
|
+
!backendPresent ||
|
|
203
|
+
hasAnyMarker(
|
|
204
|
+
`${backendCombined}\n${dependencyCombined}`,
|
|
205
|
+
BACKEND_SDK_MARKERS,
|
|
206
|
+
);
|
|
207
|
+
const backendInitPresent =
|
|
208
|
+
!backendPresent ||
|
|
209
|
+
/clue_init_fastapi|clue_init_django|configure_settings|CluePythonBootstrapConfig/.test(
|
|
210
|
+
backendCombined,
|
|
211
|
+
);
|
|
212
|
+
const backendIdentityRequired =
|
|
213
|
+
backendPresent &&
|
|
214
|
+
/\b(login|signin|sign_in|auth|token|session)\b/i.test(backendCombined);
|
|
215
|
+
const backendLogoutRequired =
|
|
216
|
+
backendPresent && /\b(logout|signout|sign_out)\b/i.test(backendCombined);
|
|
217
|
+
const backendAccountRequired =
|
|
218
|
+
backendPresent &&
|
|
219
|
+
/\b(account|workspace|organization|tenant)\b/i.test(backendCombined);
|
|
220
|
+
const backendMissingApis = [
|
|
221
|
+
...(backendIdentityRequired &&
|
|
222
|
+
!backendFoundApiNames.includes("ClueIdentify")
|
|
223
|
+
? ["ClueIdentify"]
|
|
224
|
+
: []),
|
|
225
|
+
...(backendLogoutRequired && !backendFoundApiNames.includes("ClueLogout")
|
|
226
|
+
? ["ClueLogout"]
|
|
227
|
+
: []),
|
|
228
|
+
...(backendAccountRequired &&
|
|
229
|
+
!backendFoundApiNames.includes("ClueSetAccount")
|
|
230
|
+
? ["ClueSetAccount"]
|
|
231
|
+
: []),
|
|
232
|
+
];
|
|
233
|
+
return {
|
|
234
|
+
foundApis,
|
|
235
|
+
missingApis,
|
|
236
|
+
backend_lifecycle: {
|
|
237
|
+
backend_present: backendPresent,
|
|
238
|
+
backend_root_paths: backendRootPaths,
|
|
239
|
+
dependency_files: dependencySources.map((source) => source.file_path),
|
|
240
|
+
sdk_dependency_or_import_present: backendSdkPresent,
|
|
241
|
+
sdk_init_present: backendInitPresent,
|
|
242
|
+
required_apis: [
|
|
243
|
+
...(backendIdentityRequired ? ["ClueIdentify"] : []),
|
|
244
|
+
...(backendAccountRequired ? ["ClueSetAccount"] : []),
|
|
245
|
+
...(backendLogoutRequired ? ["ClueLogout"] : []),
|
|
246
|
+
],
|
|
247
|
+
missing_apis: backendMissingApis,
|
|
248
|
+
},
|
|
249
|
+
has_noop_wrapper: noOpPattern.test(combined),
|
|
250
|
+
component_lifecycle_init_files: componentLifecycleInitFiles,
|
|
251
|
+
unguarded_lifecycle_files: unguardedLifecycleFiles,
|
|
252
|
+
unguarded_lifecycle_calls: unguardedLifecycleCalls,
|
|
253
|
+
passed:
|
|
254
|
+
missingApis.length === 0 &&
|
|
255
|
+
backendSdkPresent &&
|
|
256
|
+
backendInitPresent &&
|
|
257
|
+
backendMissingApis.length === 0 &&
|
|
258
|
+
!noOpPattern.test(combined) &&
|
|
259
|
+
componentLifecycleInitFiles.length === 0 &&
|
|
260
|
+
unguardedLifecycleFiles.length === 0,
|
|
261
|
+
};
|
|
262
262
|
};
|
|
263
263
|
|
|
264
264
|
export const runSetupCheck = async ({
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
265
|
+
repoRoot,
|
|
266
|
+
target,
|
|
267
|
+
request,
|
|
268
|
+
requireSdkLifecycle = false,
|
|
269
269
|
}) => {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
270
|
+
const resolvedRepoRoot = resolve(repoRoot ?? ".");
|
|
271
|
+
const checks = [];
|
|
272
|
+
const normalizedTarget = normalizeTarget(target);
|
|
273
273
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
274
|
+
if (normalizedTarget) {
|
|
275
|
+
const skillRoot = join(
|
|
276
|
+
resolvedRepoRoot,
|
|
277
|
+
...TARGET_SKILL_ROOTS[normalizedTarget],
|
|
278
|
+
);
|
|
279
|
+
const missingSkills = [];
|
|
280
|
+
for (const skillName of SETUP_SKILLS) {
|
|
281
|
+
const skillPath = join(skillRoot, skillName, "SKILL.md");
|
|
282
|
+
if (!(await exists(skillPath))) {
|
|
283
|
+
missingSkills.push(
|
|
284
|
+
join(...TARGET_SKILL_ROOTS[normalizedTarget], skillName, "SKILL.md"),
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
addCheck(
|
|
289
|
+
checks,
|
|
290
|
+
"setup_skills",
|
|
291
|
+
missingSkills.length === 0,
|
|
292
|
+
missingSkills.length === 0
|
|
293
|
+
? "setup skills are installed"
|
|
294
|
+
: "setup skills are missing",
|
|
295
|
+
{ missing_files: missingSkills },
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
298
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
299
|
+
const workflowPath = request?.ci_workflow_path ?? DEFAULT_WORKFLOW_PATH;
|
|
300
|
+
const absoluteWorkflowPath = join(resolvedRepoRoot, workflowPath);
|
|
301
|
+
if (await exists(absoluteWorkflowPath)) {
|
|
302
|
+
const workflow = await readFile(absoluteWorkflowPath, "utf8");
|
|
303
|
+
addCheck(
|
|
304
|
+
checks,
|
|
305
|
+
"semantic_workflow",
|
|
306
|
+
workflow.includes(
|
|
307
|
+
"npx @clue-ai/cli semantic-ci --request-env CLUE_SEMANTIC_REQUEST_JSON --repo .",
|
|
308
|
+
) &&
|
|
309
|
+
workflow.includes("CLUE_SEMANTIC_REQUEST_JSON: |") &&
|
|
310
|
+
workflow.includes("CLUE_API_KEY: ${{ secrets.CLUE_API_KEY }}") &&
|
|
311
|
+
workflow.includes(
|
|
312
|
+
"CLUE_AI_PROVIDER_API_KEY: ${{ secrets.CLUE_AI_PROVIDER_API_KEY }}",
|
|
313
|
+
) &&
|
|
314
|
+
workflow.includes("CLUE_AI_PROVIDER: ${{ vars.CLUE_AI_PROVIDER }}") &&
|
|
315
|
+
workflow.includes("permissions:\n contents: read") &&
|
|
316
|
+
workflow.includes("persist-credentials: false") &&
|
|
317
|
+
!disallowedWorkflowMetadataPattern.test(workflow),
|
|
318
|
+
"semantic workflow uses the canonical env runtime request, least-privilege checkout, and privacy-minimized GitHub metadata",
|
|
319
|
+
{ workflow_path: workflowPath },
|
|
320
|
+
);
|
|
321
|
+
} else {
|
|
322
|
+
addCheck(
|
|
323
|
+
checks,
|
|
324
|
+
"semantic_workflow",
|
|
325
|
+
false,
|
|
326
|
+
"semantic workflow is missing",
|
|
327
|
+
{
|
|
328
|
+
workflow_path: workflowPath,
|
|
329
|
+
},
|
|
330
|
+
);
|
|
331
|
+
}
|
|
331
332
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
333
|
+
addCheck(
|
|
334
|
+
checks,
|
|
335
|
+
"runtime_request_not_committed",
|
|
336
|
+
!(await exists(
|
|
337
|
+
join(resolvedRepoRoot, ".clue", "semantic-request.runtime.json"),
|
|
338
|
+
)),
|
|
339
|
+
".clue/semantic-request.runtime.json is not present in the repository",
|
|
340
|
+
);
|
|
340
341
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
342
|
+
if (request) {
|
|
343
|
+
try {
|
|
344
|
+
const inventory = await runSemanticInventory({
|
|
345
|
+
repoRoot: resolvedRepoRoot,
|
|
346
|
+
request,
|
|
347
|
+
});
|
|
348
|
+
addCheck(
|
|
349
|
+
checks,
|
|
350
|
+
"semantic_inventory",
|
|
351
|
+
true,
|
|
352
|
+
"semantic inventory discovers routes",
|
|
353
|
+
{
|
|
354
|
+
route_count: inventory.route_count,
|
|
355
|
+
},
|
|
356
|
+
);
|
|
357
|
+
} catch (error) {
|
|
358
|
+
addCheck(
|
|
359
|
+
checks,
|
|
360
|
+
"semantic_inventory",
|
|
361
|
+
false,
|
|
362
|
+
"semantic inventory failed",
|
|
363
|
+
{
|
|
364
|
+
error: error instanceof Error ? error.message : String(error),
|
|
365
|
+
},
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
368
369
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
370
|
+
const sourcePaths = await setupSourcePaths({
|
|
371
|
+
repoRoot: resolvedRepoRoot,
|
|
372
|
+
request,
|
|
373
|
+
includeFrontend: requireSdkLifecycle,
|
|
374
|
+
});
|
|
375
|
+
const excludedPaths = request?.excluded_source_paths ?? [];
|
|
376
|
+
const sources = await readAllowedSourceText({
|
|
377
|
+
repoRoot: resolvedRepoRoot,
|
|
378
|
+
allowedSourcePaths: sourcePaths,
|
|
379
|
+
excludedSourcePaths: excludedPaths,
|
|
380
|
+
});
|
|
381
|
+
const dependencySources = await readDependencyText({
|
|
382
|
+
repoRoot: resolvedRepoRoot,
|
|
383
|
+
roots: request?.allowed_source_paths ?? [],
|
|
384
|
+
});
|
|
385
|
+
const secretLeaks = findSecretLeaks([
|
|
386
|
+
...sources,
|
|
387
|
+
...dependencySources,
|
|
388
|
+
...((await exists(absoluteWorkflowPath))
|
|
389
|
+
? [
|
|
390
|
+
{
|
|
391
|
+
file_path: workflowPath,
|
|
392
|
+
text: await readFile(absoluteWorkflowPath, "utf8"),
|
|
393
|
+
},
|
|
394
|
+
]
|
|
395
|
+
: []),
|
|
396
|
+
]);
|
|
397
|
+
addCheck(
|
|
398
|
+
checks,
|
|
399
|
+
"no_secret_values",
|
|
400
|
+
secretLeaks.length === 0,
|
|
401
|
+
"no obvious secret values found",
|
|
402
|
+
{
|
|
403
|
+
files: secretLeaks,
|
|
404
|
+
},
|
|
405
|
+
);
|
|
405
406
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
407
|
+
if (requireSdkLifecycle) {
|
|
408
|
+
const sdkLifecycle = checkSdkLifecycle({
|
|
409
|
+
backendRootPaths: request?.allowed_source_paths ?? [],
|
|
410
|
+
dependencySources,
|
|
411
|
+
sources,
|
|
412
|
+
});
|
|
413
|
+
addCheck(
|
|
414
|
+
checks,
|
|
415
|
+
"sdk_lifecycle",
|
|
416
|
+
sdkLifecycle.passed,
|
|
417
|
+
"SDK lifecycle calls resolve to real source code instead of no-op globals",
|
|
418
|
+
{
|
|
419
|
+
found_apis: sdkLifecycle.foundApis,
|
|
420
|
+
missing_apis: sdkLifecycle.missingApis,
|
|
421
|
+
backend_lifecycle: sdkLifecycle.backend_lifecycle,
|
|
422
|
+
has_noop_wrapper: sdkLifecycle.has_noop_wrapper,
|
|
423
|
+
component_lifecycle_init_files:
|
|
424
|
+
sdkLifecycle.component_lifecycle_init_files,
|
|
425
|
+
unguarded_lifecycle_files: sdkLifecycle.unguarded_lifecycle_files,
|
|
426
|
+
unguarded_lifecycle_calls: sdkLifecycle.unguarded_lifecycle_calls,
|
|
427
|
+
},
|
|
428
|
+
);
|
|
429
|
+
}
|
|
429
430
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
431
|
+
const passed = checks.every((check) => check.passed);
|
|
432
|
+
return {
|
|
433
|
+
passed,
|
|
434
|
+
checks,
|
|
435
|
+
};
|
|
435
436
|
};
|