@clue-ai/cli 0.0.4 → 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 +59 -2
- package/bin/clue-cli.mjs +836 -17
- 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 +53 -14
- package/src/init-tool.mjs +153 -20
- package/src/lifecycle-guard.mjs +141 -0
- package/src/lifecycle-init.mjs +91 -73
- package/src/path-policy.mjs +2 -0
- package/src/public-schema.cjs +27 -1
- package/src/semantic-ci.mjs +771 -122
- package/src/setup-check.mjs +436 -0
- package/src/setup-detect.mjs +198 -0
- package/src/setup-prepare.mjs +289 -0
- package/src/setup-tool.mjs +94 -27
|
@@ -11,10 +11,16 @@ Required fields:
|
|
|
11
11
|
- `allowed_source_paths`
|
|
12
12
|
- `excluded_source_paths`
|
|
13
13
|
|
|
14
|
-
Required
|
|
14
|
+
Required environment:
|
|
15
15
|
|
|
16
16
|
- `CLUE_API_KEY`
|
|
17
|
+
- `AI_PROVIDER`
|
|
17
18
|
- `AI_PROVIDER_API_KEY`
|
|
19
|
+
- `CLUE_AI_MODEL`
|
|
20
|
+
|
|
21
|
+
`AI_PROVIDER` must be `openai` or `anthropic`. `AI_PROVIDER_API_KEY` must be the
|
|
22
|
+
API key for the selected provider. `CLUE_AI_MODEL` must be the model name for the
|
|
23
|
+
selected provider.
|
|
18
24
|
|
|
19
25
|
Behavior:
|
|
20
26
|
|
|
@@ -11,10 +11,16 @@ Required fields:
|
|
|
11
11
|
- `allowed_source_paths`
|
|
12
12
|
- `excluded_source_paths`
|
|
13
13
|
|
|
14
|
-
Required
|
|
14
|
+
Required environment:
|
|
15
15
|
|
|
16
16
|
- `CLUE_API_KEY`
|
|
17
|
+
- `AI_PROVIDER`
|
|
17
18
|
- `AI_PROVIDER_API_KEY`
|
|
19
|
+
- `CLUE_AI_MODEL`
|
|
20
|
+
|
|
21
|
+
`AI_PROVIDER` must be `openai` or `anthropic`. `AI_PROVIDER_API_KEY` must be the
|
|
22
|
+
API key for the selected provider. `CLUE_AI_MODEL` must be the model name for the
|
|
23
|
+
selected provider.
|
|
18
24
|
|
|
19
25
|
Behavior:
|
|
20
26
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
const SUPPORTED_AI_PROVIDERS = new Set(["openai", "anthropic"]);
|
|
2
|
+
|
|
3
|
+
const normalizeProvider = (value) => {
|
|
4
|
+
const provider = String(value || "openai")
|
|
5
|
+
.trim()
|
|
6
|
+
.toLowerCase();
|
|
7
|
+
if (!SUPPORTED_AI_PROVIDERS.has(provider)) {
|
|
8
|
+
throw new Error(
|
|
9
|
+
`CLUE_AI_PROVIDER must be one of: ${Array.from(SUPPORTED_AI_PROVIDERS).join(", ")}`,
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
return provider;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const trimTrailingSlash = (value) => String(value).replace(/\/+$/, "");
|
|
16
|
+
|
|
17
|
+
const providerBaseUrl = ({ provider, env = {}, request = {} }) => {
|
|
18
|
+
const explicit =
|
|
19
|
+
request.ai_provider_base_url || env.CLUE_AI_PROVIDER_BASE_URL || null;
|
|
20
|
+
if (explicit) {
|
|
21
|
+
return trimTrailingSlash(explicit);
|
|
22
|
+
}
|
|
23
|
+
return provider === "anthropic"
|
|
24
|
+
? "https://api.anthropic.com/v1"
|
|
25
|
+
: "https://api.openai.com/v1";
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const providerModel = ({ env = {}, request = {} }) => {
|
|
29
|
+
const configuredModel =
|
|
30
|
+
env.CLUE_INIT_AI_MODEL || env.CLUE_AI_MODEL || request.ai_model;
|
|
31
|
+
const model = String(configuredModel || "").trim();
|
|
32
|
+
if (!model) {
|
|
33
|
+
throw new Error("CLUE_AI_MODEL is required");
|
|
34
|
+
}
|
|
35
|
+
return model;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const resolveAiProviderConfig = ({ env = {}, request = {}, apiKey }) => {
|
|
39
|
+
const provider = normalizeProvider(
|
|
40
|
+
request.ai_provider || env.CLUE_AI_PROVIDER,
|
|
41
|
+
);
|
|
42
|
+
const resolvedApiKey = apiKey || env.CLUE_AI_PROVIDER_API_KEY;
|
|
43
|
+
if (!resolvedApiKey) {
|
|
44
|
+
throw new Error("CLUE_AI_PROVIDER_API_KEY is required");
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
provider,
|
|
48
|
+
apiKey: resolvedApiKey,
|
|
49
|
+
baseUrl: providerBaseUrl({ provider, env, request }),
|
|
50
|
+
model: providerModel({ env, request }),
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const parseOpenAiJson = async ({ response, emptyMessage }) => {
|
|
55
|
+
const body = await response.json();
|
|
56
|
+
const content = body?.choices?.[0]?.message?.content;
|
|
57
|
+
if (typeof content !== "string" || content.trim() === "") {
|
|
58
|
+
throw new Error(emptyMessage);
|
|
59
|
+
}
|
|
60
|
+
return JSON.parse(content);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const parseAnthropicJson = async ({ response, toolName, emptyMessage }) => {
|
|
64
|
+
const body = await response.json();
|
|
65
|
+
const toolUse = Array.isArray(body?.content)
|
|
66
|
+
? body.content.find(
|
|
67
|
+
(entry) => entry?.type === "tool_use" && entry.name === toolName,
|
|
68
|
+
)
|
|
69
|
+
: null;
|
|
70
|
+
if (toolUse?.input && typeof toolUse.input === "object") {
|
|
71
|
+
return toolUse.input;
|
|
72
|
+
}
|
|
73
|
+
const text = Array.isArray(body?.content)
|
|
74
|
+
? body.content
|
|
75
|
+
.filter(
|
|
76
|
+
(entry) => entry?.type === "text" && typeof entry.text === "string",
|
|
77
|
+
)
|
|
78
|
+
.map((entry) => entry.text)
|
|
79
|
+
.join("")
|
|
80
|
+
.trim()
|
|
81
|
+
: "";
|
|
82
|
+
if (!text) {
|
|
83
|
+
throw new Error(emptyMessage);
|
|
84
|
+
}
|
|
85
|
+
return JSON.parse(text);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const callJsonAiProvider = async ({
|
|
89
|
+
config,
|
|
90
|
+
system,
|
|
91
|
+
user,
|
|
92
|
+
toolName = "return_json",
|
|
93
|
+
toolDescription = "Return schema-valid JSON for this Clue task.",
|
|
94
|
+
failureMessage = "AI provider failed",
|
|
95
|
+
emptyMessage = "AI provider returned empty content",
|
|
96
|
+
}) => {
|
|
97
|
+
const response =
|
|
98
|
+
config.provider === "anthropic"
|
|
99
|
+
? await fetch(`${config.baseUrl}/messages`, {
|
|
100
|
+
method: "POST",
|
|
101
|
+
headers: {
|
|
102
|
+
"content-type": "application/json",
|
|
103
|
+
"x-api-key": config.apiKey,
|
|
104
|
+
"anthropic-version": "2023-06-01",
|
|
105
|
+
},
|
|
106
|
+
body: JSON.stringify({
|
|
107
|
+
model: config.model,
|
|
108
|
+
max_tokens: 4096,
|
|
109
|
+
system,
|
|
110
|
+
messages: [{ role: "user", content: user }],
|
|
111
|
+
tools: [
|
|
112
|
+
{
|
|
113
|
+
name: toolName,
|
|
114
|
+
description: toolDescription,
|
|
115
|
+
input_schema: {
|
|
116
|
+
type: "object",
|
|
117
|
+
additionalProperties: true,
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
tool_choice: { type: "tool", name: toolName },
|
|
122
|
+
}),
|
|
123
|
+
})
|
|
124
|
+
: await fetch(`${config.baseUrl}/chat/completions`, {
|
|
125
|
+
method: "POST",
|
|
126
|
+
headers: {
|
|
127
|
+
"content-type": "application/json",
|
|
128
|
+
authorization: `Bearer ${config.apiKey}`,
|
|
129
|
+
},
|
|
130
|
+
body: JSON.stringify({
|
|
131
|
+
model: config.model,
|
|
132
|
+
messages: [
|
|
133
|
+
{ role: "system", content: system },
|
|
134
|
+
{ role: "user", content: user },
|
|
135
|
+
],
|
|
136
|
+
response_format: { type: "json_object" },
|
|
137
|
+
}),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (!response.ok) {
|
|
141
|
+
throw new Error(`${failureMessage}: ${response.status}`);
|
|
142
|
+
}
|
|
143
|
+
return config.provider === "anthropic"
|
|
144
|
+
? parseAnthropicJson({ response, toolName, emptyMessage })
|
|
145
|
+
: parseOpenAiJson({ response, emptyMessage });
|
|
146
|
+
};
|
package/src/command-spec.mjs
CHANGED
|
@@ -2,7 +2,7 @@ export const CLUE_INIT_COMMAND_NAME = "/clue-init";
|
|
|
2
2
|
|
|
3
3
|
export const REQUIRED_SECRET_NAMES = [
|
|
4
4
|
"CLUE_API_KEY",
|
|
5
|
-
"
|
|
5
|
+
"CLUE_AI_PROVIDER_API_KEY",
|
|
6
6
|
];
|
|
7
7
|
|
|
8
8
|
export const CLUE_INIT_COMMAND_FIELDS = [
|
|
@@ -55,15 +55,14 @@ const deriveServiceKeyFromBackendRootPath = (backendRootPath) => {
|
|
|
55
55
|
.replace(/[^a-z0-9_-]+/g, "-")
|
|
56
56
|
.replace(/^-+|-+$/g, "");
|
|
57
57
|
if (!normalized) {
|
|
58
|
-
throw new Error(
|
|
58
|
+
throw new Error(
|
|
59
|
+
`service_key cannot be derived from backend_root_path for ${CLUE_INIT_COMMAND_NAME}`,
|
|
60
|
+
);
|
|
59
61
|
}
|
|
60
62
|
return normalized;
|
|
61
63
|
};
|
|
62
64
|
|
|
63
|
-
export const buildClueInitRequestFromCommandInput = ({
|
|
64
|
-
targetTool,
|
|
65
|
-
input,
|
|
66
|
-
}) => {
|
|
65
|
+
export const buildClueInitRequestFromCommandInput = ({ targetTool, input }) => {
|
|
67
66
|
const backendRootPath = requireField(input, "backend_root_path");
|
|
68
67
|
const serviceKey =
|
|
69
68
|
typeof input?.service_key === "string" && input.service_key.trim()
|
|
@@ -85,7 +84,8 @@ export const buildClueInitRequestFromCommandInput = ({
|
|
|
85
84
|
[],
|
|
86
85
|
"excluded_source_paths",
|
|
87
86
|
),
|
|
88
|
-
...(typeof input?.ci_workflow_path === "string" &&
|
|
87
|
+
...(typeof input?.ci_workflow_path === "string" &&
|
|
88
|
+
input.ci_workflow_path.trim()
|
|
89
89
|
? { ci_workflow_path: input.ci_workflow_path.trim() }
|
|
90
90
|
: {}),
|
|
91
91
|
};
|
package/src/contracts.mjs
CHANGED
|
@@ -7,6 +7,7 @@ const {
|
|
|
7
7
|
clueInitToolRequestSchema,
|
|
8
8
|
clueInitToolReportSchema,
|
|
9
9
|
semanticSnapshotRequestSchema,
|
|
10
|
+
semanticSnapshotResponseSchema,
|
|
10
11
|
} = schemaPackage;
|
|
11
12
|
|
|
12
13
|
const formatSchemaError = (result, field) => {
|
|
@@ -47,7 +48,11 @@ const stringArray = (value, field, { min = 0 } = {}) => {
|
|
|
47
48
|
};
|
|
48
49
|
|
|
49
50
|
export const validateInitRequest = (input) => {
|
|
50
|
-
const parsed = parseWithSchema(
|
|
51
|
+
const parsed = parseWithSchema(
|
|
52
|
+
clueInitToolRequestSchema,
|
|
53
|
+
input,
|
|
54
|
+
"init request",
|
|
55
|
+
);
|
|
51
56
|
return {
|
|
52
57
|
...parsed,
|
|
53
58
|
allowed_source_paths: [...new Set(parsed.allowed_source_paths)],
|
|
@@ -55,14 +60,18 @@ export const validateInitRequest = (input) => {
|
|
|
55
60
|
};
|
|
56
61
|
};
|
|
57
62
|
|
|
58
|
-
export const buildInitReport = ({
|
|
63
|
+
export const buildInitReport = ({
|
|
64
|
+
request,
|
|
65
|
+
lifecycleInsertions,
|
|
66
|
+
warnings = [],
|
|
67
|
+
}) =>
|
|
59
68
|
parseWithSchema(
|
|
60
69
|
clueInitToolReportSchema,
|
|
61
70
|
{
|
|
62
71
|
target_tool: request.target_tool,
|
|
63
72
|
ci_workflow_path: request.ci_workflow_path,
|
|
64
73
|
ci_workflow_added: true,
|
|
65
|
-
required_secrets: ["CLUE_API_KEY", "
|
|
74
|
+
required_secrets: ["CLUE_API_KEY", "CLUE_AI_PROVIDER_API_KEY"],
|
|
66
75
|
lifecycle_insertions: lifecycleInsertions,
|
|
67
76
|
semantic_generation_timing: "after_merge_ci",
|
|
68
77
|
semantic_preview_generated: false,
|
|
@@ -80,9 +89,13 @@ export const validateSemanticCiRequest = (input) => ({
|
|
|
80
89
|
project_key: nonEmpty(input.project_key, "project_key"),
|
|
81
90
|
environment: nonEmpty(input.environment, "environment"),
|
|
82
91
|
service_key: nonEmpty(input.service_key, "service_key"),
|
|
92
|
+
ai_provider: optionalNonEmpty(input.ai_provider) ?? "openai",
|
|
83
93
|
repository: {
|
|
84
94
|
provider: nonEmpty(input.repository?.provider, "repository.provider"),
|
|
85
|
-
repository_id: nonEmpty(
|
|
95
|
+
repository_id: nonEmpty(
|
|
96
|
+
input.repository?.repository_id,
|
|
97
|
+
"repository.repository_id",
|
|
98
|
+
),
|
|
86
99
|
...(optionalNonEmpty(input.repository?.owner)
|
|
87
100
|
? { owner: optionalNonEmpty(input.repository.owner) }
|
|
88
101
|
: {}),
|
|
@@ -92,11 +105,15 @@ export const validateSemanticCiRequest = (input) => ({
|
|
|
92
105
|
...(optionalNonEmpty(input.repository?.default_branch)
|
|
93
106
|
? { default_branch: optionalNonEmpty(input.repository.default_branch) }
|
|
94
107
|
: {}),
|
|
95
|
-
merge_commit: nonEmpty(
|
|
108
|
+
merge_commit: nonEmpty(
|
|
109
|
+
input.repository?.merge_commit,
|
|
110
|
+
"repository.merge_commit",
|
|
111
|
+
),
|
|
96
112
|
...(optionalNonEmpty(input.repository?.merged_at)
|
|
97
113
|
? { merged_at: optionalNonEmpty(input.repository.merged_at) }
|
|
98
114
|
: {}),
|
|
99
|
-
...(input.repository?.pull_request_number === undefined ||
|
|
115
|
+
...(input.repository?.pull_request_number === undefined ||
|
|
116
|
+
input.repository?.pull_request_number === null
|
|
100
117
|
? {}
|
|
101
118
|
: { pull_request_number: input.repository.pull_request_number }),
|
|
102
119
|
...(optionalNonEmpty(input.repository?.workflow_run_id)
|
|
@@ -104,21 +121,43 @@ export const validateSemanticCiRequest = (input) => ({
|
|
|
104
121
|
: {}),
|
|
105
122
|
},
|
|
106
123
|
service: {
|
|
107
|
-
service_key: nonEmpty(
|
|
124
|
+
service_key: nonEmpty(
|
|
125
|
+
input.service?.service_key ?? input.service_key,
|
|
126
|
+
"service.service_key",
|
|
127
|
+
),
|
|
108
128
|
root_path: input.service?.root_path ?? null,
|
|
109
129
|
framework: input.service?.framework ?? "fastapi",
|
|
110
130
|
language: input.service?.language ?? "python",
|
|
111
131
|
},
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
132
|
+
allowed_source_paths: stringArray(
|
|
133
|
+
input.allowed_source_paths,
|
|
134
|
+
"allowed_source_paths",
|
|
135
|
+
{ min: 1 },
|
|
136
|
+
),
|
|
137
|
+
excluded_source_paths: stringArray(
|
|
138
|
+
input.excluded_source_paths ?? [],
|
|
139
|
+
"excluded_source_paths",
|
|
140
|
+
),
|
|
141
|
+
clue_api_base_url: nonEmpty(input.clue_api_base_url, "clue_api_base_url"),
|
|
142
|
+
ai_provider_base_url: optionalNonEmpty(input.ai_provider_base_url) ?? null,
|
|
143
|
+
ai_model: nonEmpty(input.ai_model, "ai_model"),
|
|
144
|
+
});
|
|
118
145
|
|
|
119
146
|
export const buildOperationSourceKey = (method, pathTemplate) =>
|
|
120
147
|
`route.${method.toUpperCase()}.${pathTemplate}`;
|
|
121
148
|
|
|
122
149
|
export const validateSemanticSnapshotRequest = (input) => {
|
|
123
|
-
return parseWithSchema(
|
|
150
|
+
return parseWithSchema(
|
|
151
|
+
semanticSnapshotRequestSchema,
|
|
152
|
+
input,
|
|
153
|
+
"semantic snapshot",
|
|
154
|
+
);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export const validateSemanticSnapshotResponse = (input) => {
|
|
158
|
+
return parseWithSchema(
|
|
159
|
+
semanticSnapshotResponseSchema,
|
|
160
|
+
input,
|
|
161
|
+
"semantic snapshot response",
|
|
162
|
+
);
|
|
124
163
|
};
|
package/src/init-tool.mjs
CHANGED
|
@@ -1,20 +1,110 @@
|
|
|
1
1
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { buildInitReport, validateInitRequest } from "./contracts.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
applyLifecyclePlan,
|
|
6
|
+
planLifecycleInsertions,
|
|
7
|
+
} from "./lifecycle-init.mjs";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_SEMANTIC_WORKFLOW_PATH =
|
|
10
|
+
".github/workflows/clue-semantic-snapshot.yml";
|
|
11
|
+
|
|
12
|
+
const nonEmpty = (value, field) => {
|
|
13
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
14
|
+
throw new Error(`${field} is required`);
|
|
15
|
+
}
|
|
16
|
+
return value.trim();
|
|
17
|
+
};
|
|
18
|
+
|
|
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)];
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const splitCsv = (value) =>
|
|
33
|
+
typeof value === "string"
|
|
34
|
+
? value
|
|
35
|
+
.split(",")
|
|
36
|
+
.map((entry) => entry.trim())
|
|
37
|
+
.filter(Boolean)
|
|
38
|
+
: [];
|
|
39
|
+
|
|
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;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const buildSemanticWorkflowRequestFromFlags = (flags) => {
|
|
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
|
+
};
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const usesGithubVariable = (value) =>
|
|
97
|
+
typeof value === "string" && value.includes("${{ vars.");
|
|
5
98
|
|
|
6
99
|
const workflowRequestPayload = (request) =>
|
|
7
100
|
JSON.stringify(
|
|
8
101
|
{
|
|
9
|
-
project_key: "${{ vars.CLUE_PROJECT_KEY }}",
|
|
10
|
-
environment: "${{ vars.CLUE_ENVIRONMENT }}",
|
|
102
|
+
project_key: request.project_key ?? "${{ vars.CLUE_PROJECT_KEY }}",
|
|
103
|
+
environment: request.environment ?? "${{ vars.CLUE_ENVIRONMENT }}",
|
|
11
104
|
service_key: request.service_key,
|
|
12
105
|
repository: {
|
|
13
106
|
provider: "github",
|
|
14
107
|
repository_id: "${{ github.repository_id }}",
|
|
15
|
-
owner: "${{ github.repository_owner }}",
|
|
16
|
-
name: "${{ github.event.repository.name }}",
|
|
17
|
-
default_branch: "${{ github.event.repository.default_branch }}",
|
|
18
108
|
merge_commit: "${{ github.sha }}",
|
|
19
109
|
workflow_run_id: "${{ github.run_id }}",
|
|
20
110
|
},
|
|
@@ -26,40 +116,87 @@ const workflowRequestPayload = (request) =>
|
|
|
26
116
|
},
|
|
27
117
|
allowed_source_paths: request.allowed_source_paths,
|
|
28
118
|
excluded_source_paths: request.excluded_source_paths,
|
|
29
|
-
clue_api_base_url:
|
|
119
|
+
clue_api_base_url:
|
|
120
|
+
request.clue_api_base_url ?? "${{ vars.CLUE_API_BASE_URL }}",
|
|
121
|
+
ai_provider: "${{ vars.CLUE_AI_PROVIDER }}",
|
|
30
122
|
ai_model: "${{ vars.CLUE_AI_MODEL }}",
|
|
31
123
|
},
|
|
32
124
|
null,
|
|
33
125
|
10,
|
|
34
126
|
).trim();
|
|
35
127
|
|
|
128
|
+
const indentMultiline = (value, spaces) => {
|
|
129
|
+
const prefix = " ".repeat(spaces);
|
|
130
|
+
return value
|
|
131
|
+
.split("\n")
|
|
132
|
+
.map((line) => `${prefix}${line}`)
|
|
133
|
+
.join("\n");
|
|
134
|
+
};
|
|
135
|
+
|
|
36
136
|
const workflowTemplate = (request) => `name: Clue Semantic Snapshot
|
|
37
137
|
|
|
38
138
|
on:
|
|
39
139
|
push:
|
|
40
140
|
|
|
141
|
+
permissions:
|
|
142
|
+
contents: read
|
|
143
|
+
|
|
41
144
|
jobs:
|
|
42
145
|
semantic-snapshot:
|
|
43
146
|
if: github.ref_name == github.event.repository.default_branch
|
|
44
147
|
runs-on: ubuntu-latest
|
|
45
148
|
steps:
|
|
46
149
|
- uses: actions/checkout@v4
|
|
150
|
+
with:
|
|
151
|
+
persist-credentials: false
|
|
47
152
|
- uses: actions/setup-node@v4
|
|
48
153
|
with:
|
|
49
154
|
node-version: "20"
|
|
50
155
|
- name: Run Clue semantic generation
|
|
51
|
-
continue-on-error: true
|
|
52
156
|
env:
|
|
53
157
|
CLUE_API_KEY: \${{ secrets.CLUE_API_KEY }}
|
|
54
|
-
|
|
158
|
+
CLUE_AI_PROVIDER_API_KEY: \${{ secrets.CLUE_AI_PROVIDER_API_KEY }}
|
|
159
|
+
CLUE_AI_PROVIDER: \${{ vars.CLUE_AI_PROVIDER }}
|
|
160
|
+
CLUE_SEMANTIC_REQUEST_JSON: |
|
|
161
|
+
${indentMultiline(workflowRequestPayload(request), 12)}
|
|
55
162
|
run: |
|
|
56
|
-
|
|
57
|
-
cat > .clue/semantic-request.runtime.json <<'JSON'
|
|
58
|
-
${workflowRequestPayload(request)}
|
|
59
|
-
JSON
|
|
60
|
-
npx @clue-ai/cli semantic-ci --request .clue/semantic-request.runtime.json --repo .
|
|
163
|
+
npx @clue-ai/cli semantic-ci --request-env CLUE_SEMANTIC_REQUEST_JSON --repo .
|
|
61
164
|
`;
|
|
62
165
|
|
|
166
|
+
export const writeSemanticWorkflow = async ({ repoRoot, request }) => {
|
|
167
|
+
const workflowPath = join(repoRoot, request.ci_workflow_path);
|
|
168
|
+
await mkdir(dirname(workflowPath), { recursive: true });
|
|
169
|
+
await writeFile(workflowPath, workflowTemplate(request), "utf8");
|
|
170
|
+
return {
|
|
171
|
+
ci_workflow_path: request.ci_workflow_path,
|
|
172
|
+
ci_workflow_added: true,
|
|
173
|
+
required_secrets: ["CLUE_API_KEY", "CLUE_AI_PROVIDER_API_KEY"],
|
|
174
|
+
required_variables: [
|
|
175
|
+
"CLUE_AI_PROVIDER",
|
|
176
|
+
...(usesGithubVariable(
|
|
177
|
+
request.project_key ?? "${{ vars.CLUE_PROJECT_KEY }}",
|
|
178
|
+
)
|
|
179
|
+
? ["CLUE_PROJECT_KEY"]
|
|
180
|
+
: []),
|
|
181
|
+
...(usesGithubVariable(
|
|
182
|
+
request.environment ?? "${{ vars.CLUE_ENVIRONMENT }}",
|
|
183
|
+
)
|
|
184
|
+
? ["CLUE_ENVIRONMENT"]
|
|
185
|
+
: []),
|
|
186
|
+
...(usesGithubVariable(
|
|
187
|
+
request.clue_api_base_url ?? "${{ vars.CLUE_API_BASE_URL }}",
|
|
188
|
+
)
|
|
189
|
+
? ["CLUE_API_BASE_URL"]
|
|
190
|
+
: []),
|
|
191
|
+
"CLUE_AI_MODEL",
|
|
192
|
+
],
|
|
193
|
+
runtime_request_committed: false,
|
|
194
|
+
semantic_generation_timing: "after_merge_ci",
|
|
195
|
+
allowed_source_paths: request.allowed_source_paths,
|
|
196
|
+
excluded_source_paths: request.excluded_source_paths,
|
|
197
|
+
};
|
|
198
|
+
};
|
|
199
|
+
|
|
63
200
|
export const runInitTool = async ({
|
|
64
201
|
repoRoot,
|
|
65
202
|
request: rawRequest,
|
|
@@ -67,9 +204,7 @@ export const runInitTool = async ({
|
|
|
67
204
|
lifecyclePlanner = planLifecycleInsertions,
|
|
68
205
|
}) => {
|
|
69
206
|
const request = validateInitRequest(rawRequest);
|
|
70
|
-
|
|
71
|
-
await mkdir(dirname(workflowPath), { recursive: true });
|
|
72
|
-
await writeFile(workflowPath, workflowTemplate(request), "utf8");
|
|
207
|
+
await writeSemanticWorkflow({ repoRoot, request });
|
|
73
208
|
const lifecyclePlan = await lifecyclePlanner({ repoRoot, request, env });
|
|
74
209
|
const lifecycleResult = await applyLifecyclePlan({
|
|
75
210
|
repoRoot,
|
|
@@ -79,8 +214,6 @@ export const runInitTool = async ({
|
|
|
79
214
|
return buildInitReport({
|
|
80
215
|
request,
|
|
81
216
|
lifecycleInsertions: lifecycleResult.lifecycleInsertions,
|
|
82
|
-
warnings: [
|
|
83
|
-
...lifecycleResult.warnings,
|
|
84
|
-
],
|
|
217
|
+
warnings: [...lifecycleResult.warnings],
|
|
85
218
|
});
|
|
86
219
|
};
|