@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/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
- applyLifecyclePlan,
6
- planLifecycleInsertions,
5
+ applyLifecyclePlan,
6
+ planLifecycleInsertions,
7
7
  } from "./lifecycle-init.mjs";
8
8
 
9
9
  const DEFAULT_SEMANTIC_WORKFLOW_PATH =
10
- ".github/workflows/clue-semantic-snapshot.yml";
10
+ ".github/workflows/clue-semantic-snapshot.yml";
11
11
 
12
12
  const nonEmpty = (value, field) => {
13
- if (typeof value !== "string" || value.trim() === "") {
14
- throw new Error(`${field} is required`);
15
- }
16
- return value.trim();
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
- 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)];
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
- typeof value === "string"
34
- ? value
35
- .split(",")
36
- .map((entry) => entry.trim())
37
- .filter(Boolean)
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
- 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;
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
- const backendRootPath = nonEmpty(flags.backendRootPath, "backend-root-path");
58
- const allowedSourcePaths = splitCsv(flags.allowedSourcePaths);
59
- return {
60
- ci_workflow_path:
61
- typeof flags.workflowPath === "string" && flags.workflowPath.trim()
62
- ? flags.workflowPath.trim()
63
- : DEFAULT_SEMANTIC_WORKFLOW_PATH,
64
- service_key:
65
- typeof flags.serviceKey === "string" && flags.serviceKey.trim()
66
- ? flags.serviceKey.trim()
67
- : deriveServiceKeyFromPath(backendRootPath),
68
- framework: nonEmpty(flags.framework, "framework"),
69
- allowed_source_paths: normalizeStringArray(
70
- allowedSourcePaths.length ? allowedSourcePaths : [backendRootPath],
71
- "allowed-source-paths",
72
- { min: 1 },
73
- ),
74
- excluded_source_paths: normalizeStringArray(
75
- splitCsv(flags.excludedSourcePaths),
76
- "excluded-source-paths",
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
- JSON.stringify(
83
- {
84
- project_key: "${{ vars.CLUE_PROJECT_KEY }}",
85
- environment: "${{ vars.CLUE_ENVIRONMENT }}",
86
- service_key: request.service_key,
87
- repository: {
88
- provider: "github",
89
- repository_id: "${{ github.repository_id }}",
90
- merge_commit: "${{ github.sha }}",
91
- workflow_run_id: "${{ github.run_id }}",
92
- },
93
- service: {
94
- service_key: request.service_key,
95
- root_path: request.allowed_source_paths[0],
96
- framework: request.framework,
97
- language: "python",
98
- },
99
- allowed_source_paths: request.allowed_source_paths,
100
- excluded_source_paths: request.excluded_source_paths,
101
- clue_api_base_url: "${{ vars.CLUE_API_BASE_URL }}",
102
- ai_model: "${{ vars.CLUE_AI_MODEL }}",
103
- },
104
- null,
105
- 10,
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
- const prefix = " ".repeat(spaces);
110
- return value
111
- .split("\n")
112
- .map((line) => `${prefix}${line}`)
113
- .join("\n");
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
- AI_PROVIDER_API_KEY: \${{ secrets.AI_PROVIDER_API_KEY }}
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-ci --request-env CLUE_SEMANTIC_REQUEST_JSON --repo .
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
- const workflowPath = join(repoRoot, request.ci_workflow_path);
147
- await mkdir(dirname(workflowPath), { recursive: true });
148
- await writeFile(workflowPath, workflowTemplate(request), "utf8");
149
- return {
150
- ci_workflow_path: request.ci_workflow_path,
151
- ci_workflow_added: true,
152
- required_secrets: ["CLUE_API_KEY", "AI_PROVIDER_API_KEY"],
153
- required_variables: [
154
- "CLUE_PROJECT_KEY",
155
- "CLUE_ENVIRONMENT",
156
- "CLUE_API_BASE_URL",
157
- "CLUE_AI_MODEL",
158
- ],
159
- runtime_request_committed: false,
160
- semantic_generation_timing: "after_merge_ci",
161
- allowed_source_paths: request.allowed_source_paths,
162
- excluded_source_paths: request.excluded_source_paths,
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
- repoRoot,
168
- request: rawRequest,
169
- env = process.env,
170
- lifecyclePlanner = planLifecycleInsertions,
199
+ repoRoot,
200
+ request: rawRequest,
201
+ env = process.env,
202
+ lifecyclePlanner = planLifecycleInsertions,
171
203
  }) => {
172
- const request = validateInitRequest(rawRequest);
173
- await writeSemanticWorkflow({ repoRoot, request });
174
- const lifecyclePlan = await lifecyclePlanner({ repoRoot, request, env });
175
- const lifecycleResult = await applyLifecyclePlan({
176
- repoRoot,
177
- plan: lifecyclePlan,
178
- });
179
-
180
- return buildInitReport({
181
- request,
182
- lifecycleInsertions: lifecycleResult.lifecycleInsertions,
183
- warnings: [...lifecycleResult.warnings],
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
  };