@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/bin/clue-cli.mjs
CHANGED
|
@@ -5,9 +5,9 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
5
5
|
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
6
6
|
import { commandSpecs } from "../src/command-spec.mjs";
|
|
7
7
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
buildSemanticWorkflowRequestFromFlags,
|
|
9
|
+
runInitTool,
|
|
10
|
+
writeSemanticWorkflow,
|
|
11
11
|
} from "../src/init-tool.mjs";
|
|
12
12
|
import { applyLifecyclePlan } from "../src/lifecycle-init.mjs";
|
|
13
13
|
import { runSemanticCi, runSemanticInventory } from "../src/semantic-ci.mjs";
|
|
@@ -17,843 +17,886 @@ import { runSetupPrepare } from "../src/setup-prepare.mjs";
|
|
|
17
17
|
import { installSetupSkills } from "../src/setup-tool.mjs";
|
|
18
18
|
|
|
19
19
|
const parseArgs = (argv) => {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
20
|
+
const [command = "help", ...tokens] = argv;
|
|
21
|
+
const flags = new Map();
|
|
22
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
23
|
+
const token = tokens[index];
|
|
24
|
+
if (!token.startsWith("--")) continue;
|
|
25
|
+
const key = token.slice(2);
|
|
26
|
+
const next = tokens[index + 1];
|
|
27
|
+
if (next && !next.startsWith("--")) {
|
|
28
|
+
flags.set(key, next);
|
|
29
|
+
index += 1;
|
|
30
|
+
} else {
|
|
31
|
+
flags.set(key, true);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return { command, flags };
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
const readJson = async (path) => JSON.parse(await readFile(path, "utf8"));
|
|
38
38
|
|
|
39
39
|
const writeJsonIfRequested = async ({ repoRoot, outputPath, value }) => {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
40
|
+
if (typeof outputPath !== "string") return false;
|
|
41
|
+
const resolvedRepoRoot = resolve(repoRoot);
|
|
42
|
+
const resolvedOutputPath = resolve(resolvedRepoRoot, outputPath);
|
|
43
|
+
const relativeOutputPath = relative(resolvedRepoRoot, resolvedOutputPath);
|
|
44
|
+
if (relativeOutputPath.startsWith("..") || isAbsolute(relativeOutputPath)) {
|
|
45
|
+
throw new Error(`output path escapes repo root: ${outputPath}`);
|
|
46
|
+
}
|
|
47
|
+
await mkdir(dirname(resolvedOutputPath), { recursive: true });
|
|
48
|
+
await writeFile(
|
|
49
|
+
resolvedOutputPath,
|
|
50
|
+
`${JSON.stringify(value, null, 2)}\n`,
|
|
51
|
+
"utf8",
|
|
52
|
+
);
|
|
53
|
+
return true;
|
|
54
54
|
};
|
|
55
55
|
|
|
56
56
|
const sleep = (ms) =>
|
|
57
|
-
|
|
57
|
+
new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
58
58
|
|
|
59
59
|
const splitCsv = (value) =>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
60
|
+
typeof value === "string"
|
|
61
|
+
? value
|
|
62
|
+
.split(",")
|
|
63
|
+
.map((entry) => entry.trim())
|
|
64
|
+
.filter(Boolean)
|
|
65
|
+
: [];
|
|
66
66
|
|
|
67
67
|
const splitWatchTargetEntries = (value) => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
68
|
+
if (typeof value !== "string") return [];
|
|
69
|
+
const entries = [];
|
|
70
|
+
let current = "";
|
|
71
|
+
let bracketDepth = 0;
|
|
72
|
+
for (const char of value) {
|
|
73
|
+
if (char === "[") bracketDepth += 1;
|
|
74
|
+
if (char === "]") bracketDepth = Math.max(0, bracketDepth - 1);
|
|
75
|
+
if (char === "," && bracketDepth === 0) {
|
|
76
|
+
if (current.trim()) entries.push(current.trim());
|
|
77
|
+
current = "";
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
current += char;
|
|
81
|
+
}
|
|
82
|
+
if (current.trim()) entries.push(current.trim());
|
|
83
|
+
return entries;
|
|
84
84
|
};
|
|
85
85
|
|
|
86
86
|
const WATCH_LIFECYCLE_ALIASES = new Map([
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
87
|
+
["clue-init", "init"],
|
|
88
|
+
["init", "init"],
|
|
89
|
+
["sdk-initialized", "init"],
|
|
90
|
+
["clue-identify", "identify"],
|
|
91
|
+
["identify", "identify"],
|
|
92
|
+
["identified", "identify"],
|
|
93
|
+
["clue-set-account", "set-account"],
|
|
94
|
+
["set-account", "set-account"],
|
|
95
|
+
["account", "set-account"],
|
|
96
|
+
["clue-logout", "logout"],
|
|
97
|
+
["logout", "logout"],
|
|
98
|
+
["event-sent", "event-sent"],
|
|
99
|
+
["event", "event-sent"],
|
|
100
|
+
["log", "event-sent"],
|
|
101
101
|
]);
|
|
102
102
|
|
|
103
103
|
const DEFAULT_WATCH_LIFECYCLE = [
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
104
|
+
"init",
|
|
105
|
+
"identify",
|
|
106
|
+
"set-account",
|
|
107
|
+
"logout",
|
|
108
|
+
"event-sent",
|
|
109
109
|
];
|
|
110
110
|
const DEFAULT_SETUP_MANIFEST_PATH = ".clue/setup-manifest.json";
|
|
111
111
|
|
|
112
112
|
const parseExpectedLifecycle = (value) => {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
113
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
114
|
+
return DEFAULT_WATCH_LIFECYCLE;
|
|
115
|
+
}
|
|
116
|
+
const entries = value
|
|
117
|
+
.split(/[,+]/)
|
|
118
|
+
.map((entry) => entry.trim().toLowerCase())
|
|
119
|
+
.filter(Boolean);
|
|
120
|
+
const normalized = entries.map((entry) => {
|
|
121
|
+
const lifecycle = WATCH_LIFECYCLE_ALIASES.get(entry);
|
|
122
|
+
if (!lifecycle) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`invalid lifecycle check: ${entry}; expected init, identify, set-account, logout, or event-sent`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
return lifecycle;
|
|
128
|
+
});
|
|
129
|
+
return [...new Set(normalized)];
|
|
130
130
|
};
|
|
131
131
|
|
|
132
132
|
const parseWatchTargets = (value) =>
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
133
|
+
splitWatchTargetEntries(value).map((entry) => {
|
|
134
|
+
const match =
|
|
135
|
+
/^(frontend|backend):([^[\]=\s]+)(?:\[([^\]]+)\])?(?:=(\S+))?$/.exec(
|
|
136
|
+
entry,
|
|
137
|
+
);
|
|
138
|
+
if (!match) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`invalid --watch-targets entry: ${entry}; expected frontend:<service-key>[init,identify,set-account,logout,event-sent][=<url>] or backend:<service-key>[...][=<url>]`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
const serviceKey = match[2];
|
|
144
|
+
return {
|
|
145
|
+
kind: match[1],
|
|
146
|
+
serviceKey,
|
|
147
|
+
producerId: serviceKey,
|
|
148
|
+
expectedLifecycle: parseExpectedLifecycle(match[3]),
|
|
149
|
+
url: match[4] ?? null,
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
152
|
|
|
153
153
|
const normalizeManifestWatchTarget = (target) => {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
154
|
+
if (!target || typeof target !== "object") {
|
|
155
|
+
throw new Error("invalid setup manifest watch target");
|
|
156
|
+
}
|
|
157
|
+
const kind = target.kind;
|
|
158
|
+
const serviceKey = target.service_key ?? target.serviceKey;
|
|
159
|
+
if (kind !== "frontend" && kind !== "backend") {
|
|
160
|
+
throw new Error(
|
|
161
|
+
"setup manifest watch target kind must be frontend or backend",
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
if (typeof serviceKey !== "string" || !serviceKey.trim()) {
|
|
165
|
+
throw new Error("setup manifest watch target service_key is required");
|
|
166
|
+
}
|
|
167
|
+
const localUrlCandidates = Array.isArray(target.local_url_candidates)
|
|
168
|
+
? target.local_url_candidates
|
|
169
|
+
.map((entry) => String(entry).trim())
|
|
170
|
+
.filter(Boolean)
|
|
171
|
+
: [];
|
|
172
|
+
const urlEnvName =
|
|
173
|
+
typeof target.url_env_name === "string" && target.url_env_name.trim()
|
|
174
|
+
? target.url_env_name.trim()
|
|
175
|
+
: null;
|
|
176
|
+
const explicitUrl =
|
|
177
|
+
typeof target.url === "string" && target.url.trim()
|
|
178
|
+
? target.url.trim()
|
|
179
|
+
: null;
|
|
180
|
+
const expectedLifecycle = Array.isArray(target.expected_lifecycle)
|
|
181
|
+
? parseExpectedLifecycle(target.expected_lifecycle.join(","))
|
|
182
|
+
: DEFAULT_WATCH_LIFECYCLE;
|
|
183
|
+
return {
|
|
184
|
+
kind,
|
|
185
|
+
serviceKey: serviceKey.trim(),
|
|
186
|
+
producerId:
|
|
187
|
+
typeof target.producer_id === "string" && target.producer_id.trim()
|
|
188
|
+
? target.producer_id.trim()
|
|
189
|
+
: serviceKey.trim(),
|
|
190
|
+
expectedLifecycle,
|
|
191
|
+
url: explicitUrl,
|
|
192
|
+
urlEnvName,
|
|
193
|
+
localUrlCandidates,
|
|
194
|
+
};
|
|
193
195
|
};
|
|
194
196
|
|
|
195
197
|
const readSetupManifest = async ({ repoRoot, manifestPath }) => {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
+
const resolvedPath = resolve(repoRoot, manifestPath);
|
|
199
|
+
return readJson(resolvedPath);
|
|
198
200
|
};
|
|
199
201
|
|
|
200
202
|
const manifestWatchTargets = (manifest) => {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
203
|
+
const targets = manifest?.lifecycle_verification?.watch_targets;
|
|
204
|
+
if (!Array.isArray(targets) || targets.length === 0) {
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
return targets.map(normalizeManifestWatchTarget);
|
|
206
208
|
};
|
|
207
209
|
|
|
208
210
|
const resolveTargetUrlFromEnv = ({ target, env }) => {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
211
|
+
if (target.url) return target.url;
|
|
212
|
+
if (target.urlEnvName && typeof env[target.urlEnvName] === "string") {
|
|
213
|
+
const value = env[target.urlEnvName].trim();
|
|
214
|
+
if (value) return value;
|
|
215
|
+
}
|
|
216
|
+
return target.localUrlCandidates[0] ?? null;
|
|
215
217
|
};
|
|
216
218
|
|
|
217
219
|
const shouldAskQuestions = ({ flags }) =>
|
|
218
|
-
|
|
220
|
+
!flags.has("yes") && process.stdin.isTTY && process.stdout.isTTY;
|
|
219
221
|
|
|
220
222
|
const confirmTargetUrls = async ({ flags, watchTargets, env }) => {
|
|
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
|
-
|
|
223
|
+
const initialTargets = watchTargets.map((target) => ({
|
|
224
|
+
...target,
|
|
225
|
+
url: resolveTargetUrlFromEnv({ target, env }),
|
|
226
|
+
}));
|
|
227
|
+
if (!shouldAskQuestions({ flags })) {
|
|
228
|
+
return initialTargets;
|
|
229
|
+
}
|
|
230
|
+
const readline = createInterface({
|
|
231
|
+
input: process.stdin,
|
|
232
|
+
output: process.stdout,
|
|
233
|
+
});
|
|
234
|
+
try {
|
|
235
|
+
const confirmedTargets = [];
|
|
236
|
+
for (const target of initialTargets) {
|
|
237
|
+
const currentUrl = target.url ?? "";
|
|
238
|
+
const answer = await readline.question(
|
|
239
|
+
`${target.kind}のローカルURL(${target.serviceKey})はこちらで正しいですか? ${currentUrl || "(未設定)"}\nEnterで決定、違う場合はURLを入力: `,
|
|
240
|
+
);
|
|
241
|
+
confirmedTargets.push({
|
|
242
|
+
...target,
|
|
243
|
+
url: answer.trim() || currentUrl || null,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
return confirmedTargets;
|
|
247
|
+
} finally {
|
|
248
|
+
readline.close();
|
|
249
|
+
}
|
|
248
250
|
};
|
|
249
251
|
|
|
250
252
|
const uniqueCsv = (values) => [...new Set(values.filter(Boolean))].join(",");
|
|
251
253
|
|
|
252
254
|
const buildWatchProducerIds = ({ explicitProducerIds, watchTargets }) =>
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
255
|
+
uniqueCsv([
|
|
256
|
+
...splitCsv(explicitProducerIds),
|
|
257
|
+
...watchTargets.map((target) => target.producerId),
|
|
258
|
+
]);
|
|
257
259
|
|
|
258
260
|
const checkTargetUrl = async (url) => {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
261
|
+
if (!url) return { checked: false, reachable: true, status: null };
|
|
262
|
+
try {
|
|
263
|
+
const response = await fetch(url, { method: "GET" });
|
|
264
|
+
return {
|
|
265
|
+
checked: true,
|
|
266
|
+
reachable: response.status < 500,
|
|
267
|
+
status: response.status,
|
|
268
|
+
};
|
|
269
|
+
} catch {
|
|
270
|
+
return { checked: true, reachable: false, status: null };
|
|
271
|
+
}
|
|
270
272
|
};
|
|
271
273
|
|
|
272
274
|
const evaluateWatchTargets = async ({ latest, watchTargets }) => {
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
275
|
+
const producers = Array.isArray(latest?.producers) ? latest.producers : [];
|
|
276
|
+
return Promise.all(
|
|
277
|
+
watchTargets.map(async (target) => {
|
|
278
|
+
const producer = producers.find(
|
|
279
|
+
(entry) => entry.id === target.producerId,
|
|
280
|
+
);
|
|
281
|
+
const urlHealth = await checkTargetUrl(target.url);
|
|
282
|
+
const lifecycleStatus = {
|
|
283
|
+
init: Boolean(producer?.clueInit ?? producer?.sdkInitialized),
|
|
284
|
+
identify: Boolean(producer?.clueIdentify),
|
|
285
|
+
"set-account": Boolean(producer?.clueSetAccount),
|
|
286
|
+
logout: Boolean(producer?.clueLogout),
|
|
287
|
+
"event-sent": Boolean(producer?.logStored),
|
|
288
|
+
};
|
|
289
|
+
const expectedLifecycle = target.expectedLifecycle;
|
|
290
|
+
const unexpectedLifecycle = Object.entries(lifecycleStatus)
|
|
291
|
+
.filter(
|
|
292
|
+
([name, passed]) =>
|
|
293
|
+
passed &&
|
|
294
|
+
name !== "event-sent" &&
|
|
295
|
+
name !== "init" &&
|
|
296
|
+
!expectedLifecycle.includes(name),
|
|
297
|
+
)
|
|
298
|
+
.map(([name]) => name);
|
|
299
|
+
const producerPassed =
|
|
300
|
+
expectedLifecycle.every((name) => lifecycleStatus[name]) &&
|
|
301
|
+
unexpectedLifecycle.length === 0;
|
|
302
|
+
return {
|
|
303
|
+
...target,
|
|
304
|
+
expectedLifecycle,
|
|
305
|
+
eventCount: producer?.eventCount ?? 0,
|
|
306
|
+
latestOccurredAt: producer?.latestOccurredAt ?? null,
|
|
307
|
+
logStored: Boolean(producer?.logStored),
|
|
308
|
+
requestStored: Boolean(producer?.requestStored),
|
|
309
|
+
sdkInitialized: Boolean(producer?.sdkInitialized),
|
|
310
|
+
lifecycleStatus,
|
|
311
|
+
unexpectedLifecycle,
|
|
312
|
+
urlChecked: urlHealth.checked,
|
|
313
|
+
urlReachable: urlHealth.reachable,
|
|
314
|
+
urlStatus: urlHealth.status,
|
|
315
|
+
passed: producerPassed && urlHealth.reachable,
|
|
316
|
+
};
|
|
317
|
+
}),
|
|
318
|
+
);
|
|
317
319
|
};
|
|
318
320
|
|
|
319
321
|
const lifecycleStatusFromEvents = (events) => {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
322
|
+
const customEventNames = new Set(
|
|
323
|
+
events
|
|
324
|
+
.map((event) => event.custom_event_name ?? event.customEventName)
|
|
325
|
+
.filter((value) => typeof value === "string"),
|
|
326
|
+
);
|
|
327
|
+
return {
|
|
328
|
+
init: customEventNames.has("sdk_initialized"),
|
|
329
|
+
identify: customEventNames.has("identity_identified"),
|
|
330
|
+
"set-account": customEventNames.has("account_associated"),
|
|
331
|
+
logout: customEventNames.has("identity_logged_out"),
|
|
332
|
+
"event-sent": events.length > 0,
|
|
333
|
+
};
|
|
332
334
|
};
|
|
333
335
|
|
|
334
336
|
const localSetupCheckSnapshot = ({ receivedBatches }) => {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
337
|
+
const producerEvents = new Map();
|
|
338
|
+
for (const batch of receivedBatches) {
|
|
339
|
+
const producerId =
|
|
340
|
+
batch.producerId ||
|
|
341
|
+
batch.payload?.producer_metadata?.producer_id ||
|
|
342
|
+
batch.payload?.producer_id;
|
|
343
|
+
if (typeof producerId !== "string" || !producerId.trim()) continue;
|
|
344
|
+
const events = Array.isArray(batch.payload?.events)
|
|
345
|
+
? batch.payload.events
|
|
346
|
+
: [];
|
|
347
|
+
const existing = producerEvents.get(producerId) ?? [];
|
|
348
|
+
producerEvents.set(producerId, [...existing, ...events]);
|
|
349
|
+
}
|
|
350
|
+
const producers = [...producerEvents.entries()].map(([id, events]) => {
|
|
351
|
+
const lifecycleStatus = lifecycleStatusFromEvents(events);
|
|
352
|
+
return {
|
|
353
|
+
id,
|
|
354
|
+
eventCount: events.length,
|
|
355
|
+
clueInit: lifecycleStatus.init,
|
|
356
|
+
clueIdentify: lifecycleStatus.identify,
|
|
357
|
+
clueSetAccount: lifecycleStatus["set-account"],
|
|
358
|
+
clueLogout: lifecycleStatus.logout,
|
|
359
|
+
sdkInitialized: lifecycleStatus.init,
|
|
360
|
+
logStored: lifecycleStatus["event-sent"],
|
|
361
|
+
requestStored: events.some((event) => event.event_category === "request"),
|
|
362
|
+
internalFlowStored: events.some(
|
|
363
|
+
(event) => event.event_category === "internal_flow",
|
|
364
|
+
),
|
|
365
|
+
latestOccurredAt:
|
|
366
|
+
events
|
|
367
|
+
.map((event) => event.occurred_at)
|
|
368
|
+
.filter((value) => typeof value === "string")
|
|
369
|
+
.sort()
|
|
370
|
+
.at(-1) ?? null,
|
|
371
|
+
};
|
|
372
|
+
});
|
|
373
|
+
const lifecycleChecks = producers.flatMap((producer) => [
|
|
374
|
+
["frontend_sdk_initialized", producer.sdkInitialized],
|
|
375
|
+
["backend_sdk_initialized", producer.sdkInitialized],
|
|
376
|
+
["frontend_log_sent", producer.logStored],
|
|
377
|
+
["backend_log_sent", producer.logStored],
|
|
378
|
+
["identified_event", producer.clueIdentify],
|
|
379
|
+
["account_event", producer.clueSetAccount],
|
|
380
|
+
["logout_reset", producer.clueLogout],
|
|
381
|
+
]);
|
|
382
|
+
const checks = Object.fromEntries(
|
|
383
|
+
lifecycleChecks.map(([name, passed]) => [
|
|
384
|
+
name,
|
|
385
|
+
passed ? "passed" : "waiting",
|
|
386
|
+
]),
|
|
387
|
+
);
|
|
388
|
+
return { checks, producers };
|
|
387
389
|
};
|
|
388
390
|
|
|
389
391
|
const readRequestJson = async (request) => {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
392
|
+
const chunks = [];
|
|
393
|
+
for await (const chunk of request) {
|
|
394
|
+
chunks.push(chunk);
|
|
395
|
+
}
|
|
396
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
397
|
+
if (!body.trim()) return {};
|
|
398
|
+
return JSON.parse(body);
|
|
397
399
|
};
|
|
398
400
|
|
|
399
401
|
const startLocalSetupReceiver = async ({ host, port }) => {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
402
|
+
const receivedBatches = [];
|
|
403
|
+
const server = createServer(async (request, response) => {
|
|
404
|
+
response.setHeader("access-control-allow-origin", "*");
|
|
405
|
+
response.setHeader("access-control-allow-methods", "POST, OPTIONS");
|
|
406
|
+
response.setHeader("access-control-allow-headers", "*");
|
|
407
|
+
if (request.method === "OPTIONS") {
|
|
408
|
+
response.writeHead(204);
|
|
409
|
+
response.end();
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (request.method !== "POST") {
|
|
413
|
+
response.writeHead(404, { "content-type": "application/json" });
|
|
414
|
+
response.end(JSON.stringify({ accepted: false }));
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
try {
|
|
418
|
+
const payload = await readRequestJson(request);
|
|
419
|
+
receivedBatches.push({
|
|
420
|
+
payload,
|
|
421
|
+
producerId:
|
|
422
|
+
payload?.producer_metadata?.producer_id ??
|
|
423
|
+
payload?.producer_id ??
|
|
424
|
+
null,
|
|
425
|
+
path: request.url,
|
|
426
|
+
});
|
|
427
|
+
response.writeHead(202, { "content-type": "application/json" });
|
|
428
|
+
response.end(
|
|
429
|
+
JSON.stringify({
|
|
430
|
+
accepted: true,
|
|
431
|
+
status: "accepted",
|
|
432
|
+
duplicate: false,
|
|
433
|
+
eventCount: Array.isArray(payload?.events)
|
|
434
|
+
? payload.events.length
|
|
435
|
+
: 0,
|
|
436
|
+
}),
|
|
437
|
+
);
|
|
438
|
+
} catch (error) {
|
|
439
|
+
response.writeHead(400, { "content-type": "application/json" });
|
|
440
|
+
response.end(
|
|
441
|
+
JSON.stringify({
|
|
442
|
+
accepted: false,
|
|
443
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
444
|
+
}),
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
await new Promise((resolveListen, rejectListen) => {
|
|
449
|
+
server.once("error", rejectListen);
|
|
450
|
+
server.listen(port, host, () => {
|
|
451
|
+
server.off("error", rejectListen);
|
|
452
|
+
resolveListen();
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
const address = server.address();
|
|
456
|
+
if (!address || typeof address === "string") {
|
|
457
|
+
throw new Error("local setup receiver failed to bind");
|
|
458
|
+
}
|
|
459
|
+
const baseUrl = `http://${host}:${address.port}`;
|
|
460
|
+
return {
|
|
461
|
+
baseUrl,
|
|
462
|
+
receivedBatches,
|
|
463
|
+
close: () =>
|
|
464
|
+
new Promise((resolveClose) => {
|
|
465
|
+
server.close(() => resolveClose());
|
|
466
|
+
}),
|
|
467
|
+
};
|
|
462
468
|
};
|
|
463
469
|
|
|
464
470
|
const renderWatchTargets = (targetChecks) =>
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
471
|
+
targetChecks
|
|
472
|
+
.map((target) => {
|
|
473
|
+
const urlStatus = target.urlChecked
|
|
474
|
+
? ` url:${target.urlReachable ? target.urlStatus : "unreachable"}`
|
|
475
|
+
: "";
|
|
476
|
+
const lifecycle = target.expectedLifecycle
|
|
477
|
+
.map(
|
|
478
|
+
(name) =>
|
|
479
|
+
`${name}:${target.lifecycleStatus[name] ? "ok" : "waiting"}`,
|
|
480
|
+
)
|
|
481
|
+
.join(" ");
|
|
482
|
+
const unexpected = target.unexpectedLifecycle.length
|
|
483
|
+
? ` unexpected:${target.unexpectedLifecycle.join(",")}`
|
|
484
|
+
: "";
|
|
485
|
+
return `${target.passed ? "[x]" : "[ ]"} ${target.kind}:${target.serviceKey} ${lifecycle} events:${target.eventCount}${urlStatus}${unexpected}`;
|
|
486
|
+
})
|
|
487
|
+
.join("\n");
|
|
482
488
|
|
|
483
489
|
const setupCheckUrl = ({
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
490
|
+
clueApiBaseUrl,
|
|
491
|
+
environment,
|
|
492
|
+
limit,
|
|
493
|
+
producerIds,
|
|
494
|
+
projectId,
|
|
495
|
+
projectKey,
|
|
496
|
+
startedAt,
|
|
491
497
|
}) => {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
498
|
+
const normalizedBaseUrl = String(clueApiBaseUrl).replace(/\/+$/, "");
|
|
499
|
+
const url = new URL(`${normalizedBaseUrl}/api/v1/events/setup-check`);
|
|
500
|
+
url.searchParams.set("projectKey", projectKey);
|
|
501
|
+
url.searchParams.set("environment", environment);
|
|
502
|
+
url.searchParams.set("startedAt", startedAt);
|
|
503
|
+
url.searchParams.set("limit", String(limit));
|
|
504
|
+
if (typeof projectId === "string" && projectId.trim()) {
|
|
505
|
+
url.searchParams.set("project_id", projectId.trim());
|
|
506
|
+
}
|
|
507
|
+
if (typeof producerIds === "string" && producerIds.trim()) {
|
|
508
|
+
url.searchParams.set("producerIds", producerIds.trim());
|
|
509
|
+
}
|
|
510
|
+
return url;
|
|
505
511
|
};
|
|
506
512
|
|
|
507
513
|
const runSetupWatch = async ({ flags, repoRoot = ".", env = process.env }) => {
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
process.stdout.write(`${JSON.stringify(latest, null, 2)}\n`);
|
|
659
|
-
throw new Error("setup-watch timed out before all Clue setup checks passed");
|
|
514
|
+
const localMode = flags.has("local");
|
|
515
|
+
const manifestPath = String(
|
|
516
|
+
flags.get("manifest") || DEFAULT_SETUP_MANIFEST_PATH,
|
|
517
|
+
);
|
|
518
|
+
const manifest = localMode
|
|
519
|
+
? await readSetupManifest({ repoRoot, manifestPath })
|
|
520
|
+
: null;
|
|
521
|
+
if (localMode && manifest?.status !== "ready_for_ai") {
|
|
522
|
+
throw new Error(
|
|
523
|
+
`setup-watch --local requires a ready setup manifest at ${manifestPath}`,
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
const projectKey = String(
|
|
527
|
+
flags.get("project-key") ||
|
|
528
|
+
env.CLUE_PROJECT_KEY ||
|
|
529
|
+
manifest?.project_key ||
|
|
530
|
+
"",
|
|
531
|
+
).trim();
|
|
532
|
+
if (!projectKey && !localMode) {
|
|
533
|
+
throw new Error("--project-key or CLUE_PROJECT_KEY is required");
|
|
534
|
+
}
|
|
535
|
+
const environment = String(
|
|
536
|
+
flags.get("environment") ||
|
|
537
|
+
env.CLUE_ENVIRONMENT ||
|
|
538
|
+
manifest?.environment ||
|
|
539
|
+
"dev",
|
|
540
|
+
).trim();
|
|
541
|
+
const clueApiBaseUrl = String(
|
|
542
|
+
flags.get("clue-api-base-url") || env.CLUE_API_BASE_URL || "",
|
|
543
|
+
).trim();
|
|
544
|
+
if (!clueApiBaseUrl && !localMode) {
|
|
545
|
+
throw new Error("--clue-api-base-url or CLUE_API_BASE_URL is required");
|
|
546
|
+
}
|
|
547
|
+
const startedAt = String(
|
|
548
|
+
flags.get("started-at") || new Date().toISOString(),
|
|
549
|
+
).trim();
|
|
550
|
+
const timeoutMs = Number(flags.get("timeout-ms") || 120_000);
|
|
551
|
+
const pollIntervalMs = Number(flags.get("poll-interval-ms") || 3000);
|
|
552
|
+
const limit = Number(flags.get("limit") || 200);
|
|
553
|
+
const projectId = flags.get("project-id");
|
|
554
|
+
const explicitWatchTargets = parseWatchTargets(flags.get("watch-targets"));
|
|
555
|
+
const watchTargets = await confirmTargetUrls({
|
|
556
|
+
flags,
|
|
557
|
+
watchTargets:
|
|
558
|
+
explicitWatchTargets.length > 0
|
|
559
|
+
? explicitWatchTargets
|
|
560
|
+
: manifestWatchTargets(manifest),
|
|
561
|
+
env,
|
|
562
|
+
});
|
|
563
|
+
const producerIds = buildWatchProducerIds({
|
|
564
|
+
explicitProducerIds: flags.get("producer-ids"),
|
|
565
|
+
watchTargets,
|
|
566
|
+
});
|
|
567
|
+
const started = Date.now();
|
|
568
|
+
let latest = null;
|
|
569
|
+
|
|
570
|
+
if (localMode) {
|
|
571
|
+
const receiver = await startLocalSetupReceiver({
|
|
572
|
+
host: String(flags.get("host") || "127.0.0.1"),
|
|
573
|
+
port: Number(flags.get("port") || 0),
|
|
574
|
+
});
|
|
575
|
+
try {
|
|
576
|
+
process.stdout.write(`Local Clue setup receiver: ${receiver.baseUrl}\n`);
|
|
577
|
+
process.stdout.write(
|
|
578
|
+
`Frontend endpoint: ${receiver.baseUrl}/api/v1/ingest/browser\n`,
|
|
579
|
+
);
|
|
580
|
+
process.stdout.write(
|
|
581
|
+
`Backend endpoint: ${receiver.baseUrl}/api/v1/ingest/backend\n`,
|
|
582
|
+
);
|
|
583
|
+
if (watchTargets.length > 0) {
|
|
584
|
+
process.stdout.write(
|
|
585
|
+
`Watching targets: ${watchTargets.map((target) => `${target.kind}:${target.serviceKey}`).join(", ")}\n`,
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
while (Date.now() - started <= timeoutMs) {
|
|
589
|
+
latest = localSetupCheckSnapshot({
|
|
590
|
+
receivedBatches: receiver.receivedBatches,
|
|
591
|
+
});
|
|
592
|
+
const targetChecks = await evaluateWatchTargets({
|
|
593
|
+
latest,
|
|
594
|
+
watchTargets,
|
|
595
|
+
});
|
|
596
|
+
const targetChecksPassed =
|
|
597
|
+
targetChecks.length > 0 &&
|
|
598
|
+
targetChecks.every((target) => target.passed);
|
|
599
|
+
const renderedTargets = renderWatchTargets(targetChecks);
|
|
600
|
+
process.stdout.write(`${renderedTargets}\n\n`);
|
|
601
|
+
if (targetChecksPassed) {
|
|
602
|
+
process.stdout.write("Clue local setup checks passed.\n");
|
|
603
|
+
return { ...latest, local: true, watchTargets: targetChecks };
|
|
604
|
+
}
|
|
605
|
+
await sleep(pollIntervalMs);
|
|
606
|
+
}
|
|
607
|
+
process.stdout.write(`${JSON.stringify(latest, null, 2)}\n`);
|
|
608
|
+
throw new Error(
|
|
609
|
+
"setup-watch --local timed out before all Clue setup checks passed",
|
|
610
|
+
);
|
|
611
|
+
} finally {
|
|
612
|
+
await receiver.close();
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
process.stdout.write(
|
|
617
|
+
`Waiting for Clue setup checks since ${startedAt} (${environment})...\n`,
|
|
618
|
+
);
|
|
619
|
+
if (watchTargets.length > 0) {
|
|
620
|
+
process.stdout.write(
|
|
621
|
+
`Watching targets: ${watchTargets.map((target) => `${target.kind}:${target.serviceKey}`).join(", ")}\n`,
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
while (Date.now() - started <= timeoutMs) {
|
|
626
|
+
const response = await fetch(
|
|
627
|
+
setupCheckUrl({
|
|
628
|
+
clueApiBaseUrl,
|
|
629
|
+
environment,
|
|
630
|
+
limit,
|
|
631
|
+
producerIds,
|
|
632
|
+
projectId,
|
|
633
|
+
projectKey,
|
|
634
|
+
startedAt,
|
|
635
|
+
}),
|
|
636
|
+
);
|
|
637
|
+
if (!response.ok) {
|
|
638
|
+
throw new Error(`setup-check request failed: ${response.status}`);
|
|
639
|
+
}
|
|
640
|
+
latest = await response.json();
|
|
641
|
+
const checks = latest?.checks ?? {};
|
|
642
|
+
const entries = Object.entries(checks);
|
|
643
|
+
const setupChecksPassed =
|
|
644
|
+
entries.length > 0 && entries.every(([, status]) => status === "passed");
|
|
645
|
+
const targetChecks = await evaluateWatchTargets({ latest, watchTargets });
|
|
646
|
+
const targetChecksPassed = targetChecks.every((target) => target.passed);
|
|
647
|
+
const passed = setupChecksPassed && targetChecksPassed;
|
|
648
|
+
const rendered = entries
|
|
649
|
+
.map(([id, status]) => `${status === "passed" ? "[x]" : "[ ]"} ${id}`)
|
|
650
|
+
.join("\n");
|
|
651
|
+
const renderedTargets = renderWatchTargets(targetChecks);
|
|
652
|
+
process.stdout.write(
|
|
653
|
+
`${rendered}${renderedTargets ? `\n${renderedTargets}` : ""}\n\n`,
|
|
654
|
+
);
|
|
655
|
+
if (passed) {
|
|
656
|
+
process.stdout.write("Clue setup checks passed.\n");
|
|
657
|
+
return { ...latest, watchTargets: targetChecks };
|
|
658
|
+
}
|
|
659
|
+
await sleep(pollIntervalMs);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
process.stdout.write(`${JSON.stringify(latest, null, 2)}\n`);
|
|
663
|
+
throw new Error("setup-watch timed out before all Clue setup checks passed");
|
|
660
664
|
};
|
|
661
665
|
|
|
662
666
|
const usage = () =>
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
667
|
+
[
|
|
668
|
+
"Usage:",
|
|
669
|
+
" /clue-init",
|
|
670
|
+
" clue-ai setup --clue-api-key <key> --clue-api-base-url <url> --project-key <key> --environment dev",
|
|
671
|
+
" clue-ai setup-detect --repo .",
|
|
672
|
+
" clue-ai semantic-inventory --framework fastapi --backend-root-path backend --repo . --output .clue/semantic-routes.json",
|
|
673
|
+
" clue-ai semantic-workflow --framework fastapi --backend-root-path backend --repo .",
|
|
674
|
+
" clue-ai lifecycle-apply --plan clue-lifecycle-plan.json --repo .",
|
|
675
|
+
" clue-ai setup-check --framework fastapi --backend-root-path backend --repo .",
|
|
676
|
+
" clue-ai setup-watch --local",
|
|
677
|
+
" clue-ai setup-watch --project-key <key> --environment dev --clue-api-base-url <clue-api-base-url> --watch-targets frontend:web[init,identify,set-account,logout,event-sent]=<frontend-url>,backend:api[init,identify,set-account,logout,event-sent]=<backend-url>",
|
|
678
|
+
" clue-ai init --request clue-init-request.json --repo .",
|
|
679
|
+
" clue-ai semantic-ci --request clue-semantic-request.json --repo . [--previous-snapshot-file previous.json]",
|
|
680
|
+
" clue-ai semantic-ci --request-env CLUE_SEMANTIC_REQUEST_JSON --repo .",
|
|
681
|
+
].join("\n");
|
|
682
|
+
|
|
683
|
+
const renderEnvironmentInstructions = (instructions) => {
|
|
684
|
+
if (!instructions || instructions.status !== "ready") {
|
|
685
|
+
return "";
|
|
686
|
+
}
|
|
687
|
+
const lines = ["", instructions.message, ""];
|
|
688
|
+
for (const block of instructions.service_env_blocks) {
|
|
689
|
+
lines.push(
|
|
690
|
+
`[${block.kind}] ${block.root_path} (${block.env_file_candidates.join(" or ")})`,
|
|
691
|
+
block.env_block,
|
|
692
|
+
"",
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
lines.push(
|
|
696
|
+
"GitHub Secrets",
|
|
697
|
+
...instructions.ci_github.secrets.map(
|
|
698
|
+
(entry) => `${entry.name}=${entry.value}`,
|
|
699
|
+
),
|
|
700
|
+
"",
|
|
701
|
+
"GitHub Variables",
|
|
702
|
+
...instructions.ci_github.variables.map(
|
|
703
|
+
(entry) => `${entry.name}=${entry.value}`,
|
|
704
|
+
),
|
|
705
|
+
"",
|
|
706
|
+
);
|
|
707
|
+
return `${lines.join("\n")}\n`;
|
|
708
|
+
};
|
|
678
709
|
|
|
679
710
|
const main = async () => {
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
711
|
+
const { command, flags } = parseArgs(process.argv.slice(2));
|
|
712
|
+
if (command === "help" || flags.has("help")) {
|
|
713
|
+
process.stdout.write(`${usage()}\n`);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if (command === "commands") {
|
|
718
|
+
process.stdout.write(
|
|
719
|
+
`${JSON.stringify({ commands: commandSpecs }, null, 2)}\n`,
|
|
720
|
+
);
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const requestPath = flags.get("request");
|
|
725
|
+
const repoRoot = resolve(String(flags.get("repo") || "."));
|
|
726
|
+
|
|
727
|
+
if (command === "setup-watch") {
|
|
728
|
+
const report = await runSetupWatch({ flags, repoRoot });
|
|
729
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
if (command === "setup") {
|
|
734
|
+
const report = await installSetupSkills({
|
|
735
|
+
repoRoot,
|
|
736
|
+
target:
|
|
737
|
+
typeof flags.get("target") === "string"
|
|
738
|
+
? flags.get("target")
|
|
739
|
+
: undefined,
|
|
740
|
+
});
|
|
741
|
+
const preparation = flags.has("skills-only")
|
|
742
|
+
? {
|
|
743
|
+
status: "skipped",
|
|
744
|
+
reason: "skills-only flag was provided",
|
|
745
|
+
}
|
|
746
|
+
: await runSetupPrepare({
|
|
747
|
+
repoRoot,
|
|
748
|
+
target: report.target,
|
|
749
|
+
skillRoot: report.skill_root,
|
|
750
|
+
setupContext: {
|
|
751
|
+
clueApiKey: flags.get("clue-api-key"),
|
|
752
|
+
clueApiBaseUrl: flags.get("clue-api-base-url"),
|
|
753
|
+
projectKey: flags.get("project-key"),
|
|
754
|
+
environment: flags.get("environment"),
|
|
755
|
+
},
|
|
756
|
+
});
|
|
757
|
+
const environmentInstructions = renderEnvironmentInstructions(
|
|
758
|
+
preparation.environment_instructions,
|
|
759
|
+
);
|
|
760
|
+
if (environmentInstructions) {
|
|
761
|
+
process.stderr.write(environmentInstructions);
|
|
762
|
+
}
|
|
763
|
+
process.stdout.write(
|
|
764
|
+
`${JSON.stringify({ ...report, preparation }, null, 2)}\n`,
|
|
765
|
+
);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (command === "setup-detect") {
|
|
770
|
+
const report = await runSetupDetect({
|
|
771
|
+
repoRoot,
|
|
772
|
+
excludedSourcePaths:
|
|
773
|
+
typeof flags.get("excluded-source-paths") === "string"
|
|
774
|
+
? flags
|
|
775
|
+
.get("excluded-source-paths")
|
|
776
|
+
.split(",")
|
|
777
|
+
.map((entry) => entry.trim())
|
|
778
|
+
.filter(Boolean)
|
|
779
|
+
: [],
|
|
780
|
+
});
|
|
781
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
782
|
+
if (!report.detected) {
|
|
783
|
+
process.exitCode = 1;
|
|
784
|
+
}
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (command === "semantic-workflow") {
|
|
789
|
+
const request = buildSemanticWorkflowRequestFromFlags({
|
|
790
|
+
framework: flags.get("framework"),
|
|
791
|
+
backendRootPath: flags.get("backend-root-path"),
|
|
792
|
+
allowedSourcePaths: flags.get("allowed-source-paths"),
|
|
793
|
+
excludedSourcePaths: flags.get("excluded-source-paths"),
|
|
794
|
+
serviceKey: flags.get("service-key"),
|
|
795
|
+
workflowPath: flags.get("workflow-path"),
|
|
796
|
+
});
|
|
797
|
+
const report = await writeSemanticWorkflow({ repoRoot, request });
|
|
798
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
if (command === "semantic-inventory") {
|
|
803
|
+
const request = buildSemanticWorkflowRequestFromFlags({
|
|
804
|
+
framework: flags.get("framework"),
|
|
805
|
+
backendRootPath: flags.get("backend-root-path"),
|
|
806
|
+
allowedSourcePaths: flags.get("allowed-source-paths"),
|
|
807
|
+
excludedSourcePaths: flags.get("excluded-source-paths"),
|
|
808
|
+
serviceKey: flags.get("service-key"),
|
|
809
|
+
workflowPath: flags.get("workflow-path"),
|
|
810
|
+
});
|
|
811
|
+
const report = await runSemanticInventory({ repoRoot, request });
|
|
812
|
+
await writeJsonIfRequested({
|
|
813
|
+
repoRoot,
|
|
814
|
+
outputPath: flags.get("output"),
|
|
815
|
+
value: report,
|
|
816
|
+
});
|
|
817
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
if (command === "lifecycle-apply") {
|
|
822
|
+
const planPath = flags.get("plan");
|
|
823
|
+
if (typeof planPath !== "string") {
|
|
824
|
+
throw new Error("--plan is required");
|
|
825
|
+
}
|
|
826
|
+
const plan = await readJson(resolve(planPath));
|
|
827
|
+
const report = await applyLifecyclePlan({ repoRoot, plan });
|
|
828
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
if (command === "setup-check") {
|
|
833
|
+
const hasInventoryFlags =
|
|
834
|
+
typeof flags.get("framework") === "string" &&
|
|
835
|
+
typeof flags.get("backend-root-path") === "string";
|
|
836
|
+
const request = hasInventoryFlags
|
|
837
|
+
? buildSemanticWorkflowRequestFromFlags({
|
|
838
|
+
framework: flags.get("framework"),
|
|
839
|
+
backendRootPath: flags.get("backend-root-path"),
|
|
840
|
+
allowedSourcePaths: flags.get("allowed-source-paths"),
|
|
841
|
+
excludedSourcePaths: flags.get("excluded-source-paths"),
|
|
842
|
+
serviceKey: flags.get("service-key"),
|
|
843
|
+
workflowPath: flags.get("workflow-path"),
|
|
844
|
+
})
|
|
845
|
+
: undefined;
|
|
846
|
+
const report = await runSetupCheck({
|
|
847
|
+
repoRoot,
|
|
848
|
+
request,
|
|
849
|
+
target:
|
|
850
|
+
typeof flags.get("target") === "string"
|
|
851
|
+
? flags.get("target")
|
|
852
|
+
: undefined,
|
|
853
|
+
requireSdkLifecycle: flags.has("require-sdk-lifecycle"),
|
|
854
|
+
});
|
|
855
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
856
|
+
if (!report.passed) {
|
|
857
|
+
process.exitCode = 1;
|
|
858
|
+
}
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const requestEnvName = flags.get("request-env");
|
|
863
|
+
if (typeof requestPath !== "string" && typeof requestEnvName !== "string") {
|
|
864
|
+
throw new Error("--request is required");
|
|
865
|
+
}
|
|
866
|
+
const request =
|
|
867
|
+
typeof requestEnvName === "string"
|
|
868
|
+
? JSON.parse(process.env[requestEnvName] ?? "")
|
|
869
|
+
: await readJson(resolve(requestPath));
|
|
870
|
+
|
|
871
|
+
if (command === "init") {
|
|
872
|
+
const report = await runInitTool({ repoRoot, request });
|
|
873
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
if (command === "semantic-ci") {
|
|
878
|
+
const previousSnapshotPath =
|
|
879
|
+
flags.get("previous-snapshot") ?? flags.get("previous-snapshot-file");
|
|
880
|
+
const previousSnapshot =
|
|
881
|
+
typeof previousSnapshotPath === "string"
|
|
882
|
+
? await readJson(resolve(previousSnapshotPath))
|
|
883
|
+
: undefined;
|
|
884
|
+
const result = await runSemanticCi({
|
|
885
|
+
repoRoot,
|
|
886
|
+
request,
|
|
887
|
+
env: process.env,
|
|
888
|
+
previousSnapshot,
|
|
889
|
+
});
|
|
890
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
throw new Error(`Unknown command: ${command}\n${usage()}`);
|
|
852
895
|
};
|
|
853
896
|
|
|
854
897
|
main().catch((error) => {
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
898
|
+
process.stderr.write(
|
|
899
|
+
`${error instanceof Error ? error.message : String(error)}\n`,
|
|
900
|
+
);
|
|
901
|
+
process.exitCode = 1;
|
|
859
902
|
});
|