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