@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/src/init-tool.mjs
CHANGED
|
@@ -2,115 +2,132 @@ import { mkdir, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { buildInitReport, validateInitRequest } from "./contracts.mjs";
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
applyLifecyclePlan,
|
|
6
|
+
planLifecycleInsertions,
|
|
7
7
|
} from "./lifecycle-init.mjs";
|
|
8
8
|
|
|
9
9
|
const DEFAULT_SEMANTIC_WORKFLOW_PATH =
|
|
10
|
-
|
|
10
|
+
".github/workflows/clue-semantic-snapshot.yml";
|
|
11
11
|
|
|
12
12
|
const nonEmpty = (value, field) => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
14
|
+
throw new Error(`${field} is required`);
|
|
15
|
+
}
|
|
16
|
+
return value.trim();
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
const normalizeStringArray = (value, field, { min = 0 } = {}) => {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
20
|
+
if (!Array.isArray(value)) {
|
|
21
|
+
throw new Error(`${field} must be an array`);
|
|
22
|
+
}
|
|
23
|
+
const result = value
|
|
24
|
+
.filter((entry) => typeof entry === "string" && entry.trim())
|
|
25
|
+
.map((entry) => entry.trim());
|
|
26
|
+
if (result.length < min) {
|
|
27
|
+
throw new Error(`${field} must include at least ${min} item(s)`);
|
|
28
|
+
}
|
|
29
|
+
return [...new Set(result)];
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
const splitCsv = (value) =>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
typeof value === "string"
|
|
34
|
+
? value
|
|
35
|
+
.split(",")
|
|
36
|
+
.map((entry) => entry.trim())
|
|
37
|
+
.filter(Boolean)
|
|
38
|
+
: [];
|
|
39
39
|
|
|
40
40
|
const deriveServiceKeyFromPath = (backendRootPath) => {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
41
|
+
const segment = backendRootPath
|
|
42
|
+
.split("/")
|
|
43
|
+
.map((part) => part.trim())
|
|
44
|
+
.filter(Boolean)
|
|
45
|
+
.at(-1);
|
|
46
|
+
const normalized = segment
|
|
47
|
+
?.toLowerCase()
|
|
48
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
49
|
+
.replace(/^-+|-+$/g, "");
|
|
50
|
+
if (!normalized) {
|
|
51
|
+
throw new Error("service-key cannot be derived from backend-root-path");
|
|
52
|
+
}
|
|
53
|
+
return normalized;
|
|
54
54
|
};
|
|
55
55
|
|
|
56
56
|
export const buildSemanticWorkflowRequestFromFlags = (flags) => {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
57
|
+
const backendRootPath = nonEmpty(flags.backendRootPath, "backend-root-path");
|
|
58
|
+
const allowedSourcePaths = splitCsv(flags.allowedSourcePaths);
|
|
59
|
+
const projectKey =
|
|
60
|
+
typeof flags.projectKey === "string" && flags.projectKey.trim()
|
|
61
|
+
? flags.projectKey.trim()
|
|
62
|
+
: "${{ vars.CLUE_PROJECT_KEY }}";
|
|
63
|
+
const environment =
|
|
64
|
+
typeof flags.environment === "string" && flags.environment.trim()
|
|
65
|
+
? flags.environment.trim()
|
|
66
|
+
: "${{ vars.CLUE_ENVIRONMENT }}";
|
|
67
|
+
const clueApiBaseUrl =
|
|
68
|
+
typeof flags.clueApiBaseUrl === "string" && flags.clueApiBaseUrl.trim()
|
|
69
|
+
? flags.clueApiBaseUrl.trim()
|
|
70
|
+
: "${{ vars.CLUE_API_BASE_URL }}";
|
|
71
|
+
return {
|
|
72
|
+
project_key: projectKey,
|
|
73
|
+
environment,
|
|
74
|
+
clue_api_base_url: clueApiBaseUrl,
|
|
75
|
+
ci_workflow_path:
|
|
76
|
+
typeof flags.workflowPath === "string" && flags.workflowPath.trim()
|
|
77
|
+
? flags.workflowPath.trim()
|
|
78
|
+
: DEFAULT_SEMANTIC_WORKFLOW_PATH,
|
|
79
|
+
service_key:
|
|
80
|
+
typeof flags.serviceKey === "string" && flags.serviceKey.trim()
|
|
81
|
+
? flags.serviceKey.trim()
|
|
82
|
+
: deriveServiceKeyFromPath(backendRootPath),
|
|
83
|
+
framework: nonEmpty(flags.framework, "framework"),
|
|
84
|
+
allowed_source_paths: normalizeStringArray(
|
|
85
|
+
allowedSourcePaths.length ? allowedSourcePaths : [backendRootPath],
|
|
86
|
+
"allowed-source-paths",
|
|
87
|
+
{ min: 1 },
|
|
88
|
+
),
|
|
89
|
+
excluded_source_paths: normalizeStringArray(
|
|
90
|
+
splitCsv(flags.excludedSourcePaths),
|
|
91
|
+
"excluded-source-paths",
|
|
92
|
+
),
|
|
93
|
+
};
|
|
79
94
|
};
|
|
80
95
|
|
|
96
|
+
const usesGithubVariable = (value) =>
|
|
97
|
+
typeof value === "string" && value.includes("${{ vars.");
|
|
98
|
+
|
|
81
99
|
const workflowRequestPayload = (request) =>
|
|
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
|
-
).trim();
|
|
100
|
+
JSON.stringify(
|
|
101
|
+
{
|
|
102
|
+
project_key: request.project_key ?? "${{ vars.CLUE_PROJECT_KEY }}",
|
|
103
|
+
environment: request.environment ?? "${{ vars.CLUE_ENVIRONMENT }}",
|
|
104
|
+
service_key: request.service_key,
|
|
105
|
+
repository: {
|
|
106
|
+
provider: "github",
|
|
107
|
+
repository_id: "${{ github.repository_id }}",
|
|
108
|
+
merge_commit: "${{ github.sha }}",
|
|
109
|
+
workflow_run_id: "${{ github.run_id }}",
|
|
110
|
+
},
|
|
111
|
+
service: {
|
|
112
|
+
service_key: request.service_key,
|
|
113
|
+
framework: request.framework,
|
|
114
|
+
language: "python",
|
|
115
|
+
},
|
|
116
|
+
allowed_source_paths: request.allowed_source_paths,
|
|
117
|
+
excluded_source_paths: request.excluded_source_paths,
|
|
118
|
+
clue_api_base_url:
|
|
119
|
+
request.clue_api_base_url ?? "${{ vars.CLUE_API_BASE_URL }}",
|
|
120
|
+
},
|
|
121
|
+
null,
|
|
122
|
+
10,
|
|
123
|
+
).trim();
|
|
107
124
|
|
|
108
125
|
const indentMultiline = (value, spaces) => {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
126
|
+
const prefix = " ".repeat(spaces);
|
|
127
|
+
return value
|
|
128
|
+
.split("\n")
|
|
129
|
+
.map((line) => `${prefix}${line}`)
|
|
130
|
+
.join("\n");
|
|
114
131
|
};
|
|
115
132
|
|
|
116
133
|
const workflowTemplate = (request) => `name: Clue Semantic Snapshot
|
|
@@ -135,51 +152,67 @@ jobs:
|
|
|
135
152
|
- name: Run Clue semantic generation
|
|
136
153
|
env:
|
|
137
154
|
CLUE_API_KEY: \${{ secrets.CLUE_API_KEY }}
|
|
138
|
-
|
|
155
|
+
CLUE_AI_PROVIDER_API_KEY: \${{ secrets.CLUE_AI_PROVIDER_API_KEY }}
|
|
156
|
+
CLUE_AI_PROVIDER: \${{ vars.CLUE_AI_PROVIDER }}
|
|
157
|
+
CLUE_AI_MODEL: \${{ vars.CLUE_AI_MODEL }}
|
|
139
158
|
CLUE_SEMANTIC_REQUEST_JSON: |
|
|
140
159
|
${indentMultiline(workflowRequestPayload(request), 12)}
|
|
141
160
|
run: |
|
|
142
|
-
npx @clue-ai/cli semantic-
|
|
161
|
+
npx @clue-ai/cli semantic-gen --request-env CLUE_SEMANTIC_REQUEST_JSON --repo .
|
|
143
162
|
`;
|
|
144
163
|
|
|
145
164
|
export const writeSemanticWorkflow = async ({ repoRoot, request }) => {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
165
|
+
const workflowPath = join(repoRoot, request.ci_workflow_path);
|
|
166
|
+
await mkdir(dirname(workflowPath), { recursive: true });
|
|
167
|
+
await writeFile(workflowPath, workflowTemplate(request), "utf8");
|
|
168
|
+
return {
|
|
169
|
+
ci_workflow_path: request.ci_workflow_path,
|
|
170
|
+
ci_workflow_added: true,
|
|
171
|
+
required_secrets: ["CLUE_API_KEY", "CLUE_AI_PROVIDER_API_KEY"],
|
|
172
|
+
required_variables: [
|
|
173
|
+
"CLUE_AI_PROVIDER",
|
|
174
|
+
"CLUE_AI_MODEL",
|
|
175
|
+
...(usesGithubVariable(
|
|
176
|
+
request.project_key ?? "${{ vars.CLUE_PROJECT_KEY }}",
|
|
177
|
+
)
|
|
178
|
+
? ["CLUE_PROJECT_KEY"]
|
|
179
|
+
: []),
|
|
180
|
+
...(usesGithubVariable(
|
|
181
|
+
request.environment ?? "${{ vars.CLUE_ENVIRONMENT }}",
|
|
182
|
+
)
|
|
183
|
+
? ["CLUE_ENVIRONMENT"]
|
|
184
|
+
: []),
|
|
185
|
+
...(usesGithubVariable(
|
|
186
|
+
request.clue_api_base_url ?? "${{ vars.CLUE_API_BASE_URL }}",
|
|
187
|
+
)
|
|
188
|
+
? ["CLUE_API_BASE_URL"]
|
|
189
|
+
: []),
|
|
190
|
+
],
|
|
191
|
+
runtime_request_committed: false,
|
|
192
|
+
semantic_generation_timing: "after_merge_ci",
|
|
193
|
+
allowed_source_paths: request.allowed_source_paths,
|
|
194
|
+
excluded_source_paths: request.excluded_source_paths,
|
|
195
|
+
};
|
|
164
196
|
};
|
|
165
197
|
|
|
166
198
|
export const runInitTool = async ({
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
199
|
+
repoRoot,
|
|
200
|
+
request: rawRequest,
|
|
201
|
+
env = process.env,
|
|
202
|
+
lifecyclePlanner = planLifecycleInsertions,
|
|
171
203
|
}) => {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
204
|
+
const request = validateInitRequest(rawRequest);
|
|
205
|
+
const workflowResult = await writeSemanticWorkflow({ repoRoot, request });
|
|
206
|
+
const lifecyclePlan = await lifecyclePlanner({ repoRoot, request, env });
|
|
207
|
+
const lifecycleResult = await applyLifecyclePlan({
|
|
208
|
+
repoRoot,
|
|
209
|
+
plan: lifecyclePlan,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
return buildInitReport({
|
|
213
|
+
request,
|
|
214
|
+
lifecycleInsertions: lifecycleResult.lifecycleInsertions,
|
|
215
|
+
requiredVariables: workflowResult.required_variables,
|
|
216
|
+
warnings: [...lifecycleResult.warnings],
|
|
217
|
+
});
|
|
185
218
|
};
|