@clue-ai/cli 0.0.5 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -3
- package/bin/clue-cli.mjs +805 -762
- package/commands/claude-code/clue-init.md +7 -1
- package/commands/codex/clue-init.md +7 -1
- package/package.json +1 -1
- package/src/ai-provider.mjs +146 -0
- package/src/command-spec.mjs +7 -7
- package/src/contracts.mjs +49 -15
- package/src/init-tool.mjs +158 -124
- package/src/lifecycle-init.mjs +180 -205
- package/src/public-schema.cjs +1 -1
- package/src/semantic-ci.mjs +122 -163
- package/src/setup-check.mjs +373 -372
- package/src/setup-prepare.mjs +266 -147
- package/src/setup-tool.mjs +231 -229
package/src/setup-prepare.mjs
CHANGED
|
@@ -1,170 +1,289 @@
|
|
|
1
1
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
buildSemanticWorkflowRequestFromFlags,
|
|
5
|
+
writeSemanticWorkflow,
|
|
6
6
|
} from "./init-tool.mjs";
|
|
7
7
|
import { runSetupDetect } from "./setup-detect.mjs";
|
|
8
8
|
|
|
9
9
|
const DEFAULT_SETUP_MANIFEST_PATH = ".clue/setup-manifest.json";
|
|
10
|
+
const BROWSER_INGEST_PATH = "/api/v1/ingest/browser";
|
|
11
|
+
const BACKEND_INGEST_PATH = "/api/v1/ingest/backend";
|
|
10
12
|
|
|
11
13
|
const writeJson = async ({ repoRoot, path, value }) => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
const absolutePath = join(resolve(repoRoot), path);
|
|
15
|
+
await mkdir(dirname(absolutePath), { recursive: true });
|
|
16
|
+
await writeFile(absolutePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
15
17
|
};
|
|
16
18
|
|
|
17
19
|
const firstCandidateOrBlocker = (detection) => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
20
|
+
if (!detection.detected || detection.candidates.length === 0) {
|
|
21
|
+
return {
|
|
22
|
+
candidate: null,
|
|
23
|
+
blockers: detection.blockers.length
|
|
24
|
+
? detection.blockers
|
|
25
|
+
: [
|
|
26
|
+
{
|
|
27
|
+
code: "NO_SETUP_CANDIDATE",
|
|
28
|
+
message: "No setup candidate was mechanically detected.",
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
candidate: detection.candidates[0],
|
|
35
|
+
blockers: [],
|
|
36
|
+
};
|
|
35
37
|
};
|
|
36
38
|
|
|
37
39
|
const DEFAULT_SETUP_LIFECYCLE = [
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
"init",
|
|
41
|
+
"identify",
|
|
42
|
+
"set-account",
|
|
43
|
+
"logout",
|
|
44
|
+
"event-sent",
|
|
43
45
|
];
|
|
44
46
|
|
|
45
47
|
const buildWatchTargets = (detection, fallbackCandidate) => {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
48
|
+
const frontendServices = Array.isArray(detection.services?.frontend)
|
|
49
|
+
? detection.services.frontend
|
|
50
|
+
: [];
|
|
51
|
+
const backendServices = Array.isArray(detection.services?.backend)
|
|
52
|
+
? detection.services.backend
|
|
53
|
+
: [
|
|
54
|
+
{
|
|
55
|
+
kind: "backend",
|
|
56
|
+
framework: fallbackCandidate.framework,
|
|
57
|
+
root_path: fallbackCandidate.backend_root_path,
|
|
58
|
+
service_key: fallbackCandidate.service_key,
|
|
59
|
+
local_url_candidates: [],
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
return [...frontendServices, ...backendServices].map((service) => ({
|
|
63
|
+
kind: service.kind,
|
|
64
|
+
framework: service.framework,
|
|
65
|
+
root_path: service.root_path,
|
|
66
|
+
service_key: service.service_key,
|
|
67
|
+
producer_id: service.service_key,
|
|
68
|
+
expected_lifecycle: DEFAULT_SETUP_LIFECYCLE,
|
|
69
|
+
local_url_candidates: service.local_url_candidates ?? [],
|
|
70
|
+
url_env_name:
|
|
71
|
+
service.kind === "frontend"
|
|
72
|
+
? `CLUE_LOCAL_${service.service_key.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_FRONTEND_URL`
|
|
73
|
+
: `CLUE_LOCAL_${service.service_key.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_BACKEND_URL`,
|
|
74
|
+
}));
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const optionalString = (value) =>
|
|
78
|
+
typeof value === "string" && value.trim() ? value.trim() : null;
|
|
79
|
+
|
|
80
|
+
const trimTrailingSlash = (value) => String(value).replace(/\/+$/, "");
|
|
81
|
+
|
|
82
|
+
const buildEndpoint = (baseUrl, path) => `${trimTrailingSlash(baseUrl)}${path}`;
|
|
83
|
+
|
|
84
|
+
const setupContextFromInput = (input = {}) => ({
|
|
85
|
+
clue_api_key: optionalString(input.clueApiKey),
|
|
86
|
+
clue_api_base_url: optionalString(input.clueApiBaseUrl),
|
|
87
|
+
project_key: optionalString(input.projectKey),
|
|
88
|
+
environment: optionalString(input.environment),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const envFileCandidates = (target) => {
|
|
92
|
+
if (target.kind === "frontend") {
|
|
93
|
+
return target.framework === "nextjs" ? [".env.local"] : [".env"];
|
|
94
|
+
}
|
|
95
|
+
return [".env"];
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const buildServiceEnvBlock = ({ target, setupContext }) => {
|
|
99
|
+
const ingestPath =
|
|
100
|
+
target.kind === "frontend" ? BROWSER_INGEST_PATH : BACKEND_INGEST_PATH;
|
|
101
|
+
const variables = [
|
|
102
|
+
{
|
|
103
|
+
name: "CLUE_INGEST_ENDPOINT",
|
|
104
|
+
value: buildEndpoint(setupContext.clue_api_base_url, ingestPath),
|
|
105
|
+
},
|
|
106
|
+
{ name: "CLUE_PROJECT_KEY", value: setupContext.project_key },
|
|
107
|
+
{ name: "CLUE_ENVIRONMENT", value: setupContext.environment },
|
|
108
|
+
{ name: "CLUE_SERVICE_KEY", value: target.service_key },
|
|
109
|
+
];
|
|
110
|
+
if (target.kind === "backend") {
|
|
111
|
+
variables.push({ name: "CLUE_API_KEY", value: setupContext.clue_api_key });
|
|
112
|
+
}
|
|
113
|
+
return variables.map(({ name, value }) => `${name}=${value}`).join("\n");
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const buildEnvironmentInstructions = ({ manifest, setupContext }) => {
|
|
117
|
+
const missingFlags = [
|
|
118
|
+
["clue_api_key", "--clue-api-key"],
|
|
119
|
+
["clue_api_base_url", "--clue-api-base-url"],
|
|
120
|
+
["project_key", "--project-key"],
|
|
121
|
+
["environment", "--environment"],
|
|
122
|
+
]
|
|
123
|
+
.filter(([key]) => !setupContext[key])
|
|
124
|
+
.map(([, flag]) => flag);
|
|
125
|
+
|
|
126
|
+
if (missingFlags.length > 0 || manifest.status !== "ready_for_ai") {
|
|
127
|
+
return {
|
|
128
|
+
status: "missing_setup_arguments",
|
|
129
|
+
required_flags: missingFlags,
|
|
130
|
+
message:
|
|
131
|
+
"Run setup with Clue values from the setup screen to print service-specific env blocks.",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const watchTargets = manifest.lifecycle_verification.watch_targets;
|
|
136
|
+
return {
|
|
137
|
+
status: "ready",
|
|
138
|
+
message: "各サービスの env ファイルに以下を設定してください。",
|
|
139
|
+
service_env_blocks: watchTargets.map((target) => ({
|
|
140
|
+
kind: target.kind,
|
|
141
|
+
framework: target.framework,
|
|
142
|
+
root_path: target.root_path,
|
|
143
|
+
service_key: target.service_key,
|
|
144
|
+
env_file_candidates: envFileCandidates(target),
|
|
145
|
+
env_block: buildServiceEnvBlock({ target, setupContext }),
|
|
146
|
+
})),
|
|
147
|
+
ci_github: {
|
|
148
|
+
secrets: [
|
|
149
|
+
{ name: "CLUE_API_KEY", value: setupContext.clue_api_key },
|
|
150
|
+
{
|
|
151
|
+
name: "CLUE_AI_PROVIDER_API_KEY",
|
|
152
|
+
value: "<openai-or-anthropic-api-key>",
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
variables: [
|
|
156
|
+
{ name: "CLUE_AI_PROVIDER", value: "<openai-or-anthropic>" },
|
|
157
|
+
{ name: "CLUE_AI_MODEL", value: "<selected-provider-model>" },
|
|
158
|
+
],
|
|
159
|
+
},
|
|
160
|
+
};
|
|
73
161
|
};
|
|
74
162
|
|
|
75
163
|
export const runSetupPrepare = async ({
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
164
|
+
repoRoot,
|
|
165
|
+
target,
|
|
166
|
+
skillRoot,
|
|
167
|
+
setupContext: setupContextInput,
|
|
168
|
+
setupManifestPath = DEFAULT_SETUP_MANIFEST_PATH,
|
|
80
169
|
}) => {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
+
const resolvedRepoRoot = resolve(repoRoot ?? ".");
|
|
171
|
+
const setupContext = setupContextFromInput(setupContextInput);
|
|
172
|
+
const detection = await runSetupDetect({ repoRoot: resolvedRepoRoot });
|
|
173
|
+
const { candidate, blockers } = firstCandidateOrBlocker(detection);
|
|
174
|
+
if (!candidate) {
|
|
175
|
+
const manifest = {
|
|
176
|
+
status: "blocked",
|
|
177
|
+
target,
|
|
178
|
+
skill_root: skillRoot,
|
|
179
|
+
blockers,
|
|
180
|
+
detection,
|
|
181
|
+
ai_next_scope: "blocked_until_backend_routes_are_detected",
|
|
182
|
+
machine_owned_artifacts: [],
|
|
183
|
+
ai_owned_workstreams: [
|
|
184
|
+
"sdk_lifecycle_implementation_after_blockers_are_resolved",
|
|
185
|
+
],
|
|
186
|
+
};
|
|
187
|
+
await writeJson({
|
|
188
|
+
repoRoot: resolvedRepoRoot,
|
|
189
|
+
path: setupManifestPath,
|
|
190
|
+
value: manifest,
|
|
191
|
+
});
|
|
192
|
+
return {
|
|
193
|
+
...manifest,
|
|
194
|
+
environment_instructions: buildEnvironmentInstructions({
|
|
195
|
+
manifest,
|
|
196
|
+
setupContext,
|
|
197
|
+
}),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const request = buildSemanticWorkflowRequestFromFlags({
|
|
202
|
+
framework: candidate.framework,
|
|
203
|
+
backendRootPath: candidate.backend_root_path,
|
|
204
|
+
serviceKey: candidate.service_key,
|
|
205
|
+
projectKey: setupContext.project_key,
|
|
206
|
+
environment: setupContext.environment,
|
|
207
|
+
clueApiBaseUrl: setupContext.clue_api_base_url,
|
|
208
|
+
});
|
|
209
|
+
const workflow = await writeSemanticWorkflow({
|
|
210
|
+
repoRoot: resolvedRepoRoot,
|
|
211
|
+
request,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const manifest = {
|
|
215
|
+
status: "ready_for_ai",
|
|
216
|
+
target,
|
|
217
|
+
skill_root: skillRoot,
|
|
218
|
+
detected: {
|
|
219
|
+
framework: candidate.framework,
|
|
220
|
+
backend_root_path: candidate.backend_root_path,
|
|
221
|
+
service_key: candidate.service_key,
|
|
222
|
+
},
|
|
223
|
+
service_identity: {
|
|
224
|
+
canonical_field: "service_key",
|
|
225
|
+
backend_env_name: "CLUE_SERVICE_KEY",
|
|
226
|
+
frontend_env_name: "CLUE_SERVICE_KEY",
|
|
227
|
+
producer_id_derivation: "producer_id defaults to service_key",
|
|
228
|
+
},
|
|
229
|
+
clue_context: {
|
|
230
|
+
project_key: setupContext.project_key,
|
|
231
|
+
environment: setupContext.environment,
|
|
232
|
+
clue_api_base_url: setupContext.clue_api_base_url,
|
|
233
|
+
ingest_endpoints: setupContext.clue_api_base_url
|
|
234
|
+
? {
|
|
235
|
+
browser: buildEndpoint(
|
|
236
|
+
setupContext.clue_api_base_url,
|
|
237
|
+
BROWSER_INGEST_PATH,
|
|
238
|
+
),
|
|
239
|
+
backend: buildEndpoint(
|
|
240
|
+
setupContext.clue_api_base_url,
|
|
241
|
+
BACKEND_INGEST_PATH,
|
|
242
|
+
),
|
|
243
|
+
}
|
|
244
|
+
: null,
|
|
245
|
+
},
|
|
246
|
+
lifecycle_verification: {
|
|
247
|
+
watch_target_format:
|
|
248
|
+
"frontend:<service-key>[init,identify,set-account,logout,event-sent]=<frontend-url>,backend:<service-key>[init,identify,set-account,logout,event-sent]=<backend-url>",
|
|
249
|
+
rule: "setup-watch --local uses the structured watch_targets below. Lifecycle checks are fixed for setup verification and are evaluated per service_key.",
|
|
250
|
+
watch_targets: buildWatchTargets(detection, candidate),
|
|
251
|
+
},
|
|
252
|
+
artifacts: {
|
|
253
|
+
ci_workflow_path: workflow.ci_workflow_path,
|
|
254
|
+
setup_manifest_path: setupManifestPath,
|
|
255
|
+
runtime_request_committed: false,
|
|
256
|
+
},
|
|
257
|
+
machine_owned_artifacts: [workflow.ci_workflow_path, setupManifestPath],
|
|
258
|
+
ai_must_not_edit: [workflow.ci_workflow_path],
|
|
259
|
+
ai_owned_workstreams: ["sdk_lifecycle_implementation"],
|
|
260
|
+
required_final_check: {
|
|
261
|
+
command:
|
|
262
|
+
`npx @clue-ai/cli setup-check --framework ${candidate.framework} ` +
|
|
263
|
+
`--backend-root-path ${candidate.backend_root_path} --repo . --target ${target} --require-sdk-lifecycle`,
|
|
264
|
+
},
|
|
265
|
+
required_env_names: [
|
|
266
|
+
"CLUE_SERVICE_KEY",
|
|
267
|
+
"CLUE_API_KEY",
|
|
268
|
+
"CLUE_AI_PROVIDER",
|
|
269
|
+
"CLUE_AI_PROVIDER_API_KEY",
|
|
270
|
+
"CLUE_PROJECT_KEY",
|
|
271
|
+
"CLUE_ENVIRONMENT",
|
|
272
|
+
"CLUE_INGEST_ENDPOINT",
|
|
273
|
+
"CLUE_API_BASE_URL",
|
|
274
|
+
"CLUE_AI_MODEL",
|
|
275
|
+
],
|
|
276
|
+
};
|
|
277
|
+
await writeJson({
|
|
278
|
+
repoRoot: resolvedRepoRoot,
|
|
279
|
+
path: setupManifestPath,
|
|
280
|
+
value: manifest,
|
|
281
|
+
});
|
|
282
|
+
return {
|
|
283
|
+
...manifest,
|
|
284
|
+
environment_instructions: buildEnvironmentInstructions({
|
|
285
|
+
manifest,
|
|
286
|
+
setupContext,
|
|
287
|
+
}),
|
|
288
|
+
};
|
|
170
289
|
};
|