@clue-ai/cli 0.0.8 → 0.0.10
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 +37 -26
- package/bin/clue-cli.mjs +93 -54
- package/package.json +1 -1
- package/src/cli-invocation.mjs +34 -0
- package/src/command-spec.mjs +3 -0
- package/src/contracts.mjs +132 -127
- package/src/init-tool.mjs +2 -1
- package/src/lifecycle-guard.mjs +168 -103
- package/src/lifecycle-init.mjs +428 -17
- package/src/public-schema.cjs +4 -0
- package/src/semantic-agent-runner.mjs +3 -1
- package/src/setup-check.mjs +643 -47
- package/src/setup-help.mjs +69 -0
- package/src/setup-prepare.mjs +75 -15
- package/src/setup-tool.mjs +86 -40
package/src/contracts.mjs
CHANGED
|
@@ -2,163 +2,168 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
|
|
3
3
|
const require = createRequire(import.meta.url);
|
|
4
4
|
const schemaPackage = require("./public-schema.cjs");
|
|
5
|
+
|
|
5
6
|
const {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
clueInitToolRequestSchema,
|
|
8
|
+
clueInitToolReportSchema,
|
|
9
|
+
semanticSnapshotRequestSchema,
|
|
10
|
+
semanticSnapshotResponseSchema,
|
|
10
11
|
} = schemaPackage;
|
|
11
12
|
|
|
12
13
|
const formatSchemaError = (result, field) => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
const issues = result.error.issues.map((issue) => {
|
|
15
|
+
const path = issue.path.length ? issue.path.join(".") : field;
|
|
16
|
+
return `${path}: ${issue.message}`;
|
|
17
|
+
});
|
|
18
|
+
return `${field} is invalid: ${issues.join("; ")}`;
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
const parseWithSchema = (schema, input, field) => {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
const result = schema.safeParse(input);
|
|
23
|
+
if (!result.success) {
|
|
24
|
+
throw new Error(formatSchemaError(result, field));
|
|
25
|
+
}
|
|
26
|
+
return result.data;
|
|
26
27
|
};
|
|
27
28
|
|
|
28
29
|
const nonEmpty = (value, field) => {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
31
|
+
throw new Error(`${field} is required`);
|
|
32
|
+
}
|
|
33
|
+
return value.trim();
|
|
33
34
|
};
|
|
34
35
|
|
|
35
36
|
const optionalNonEmpty = (value) =>
|
|
36
|
-
|
|
37
|
+
typeof value === "string" && value.trim() ? value.trim() : null;
|
|
37
38
|
|
|
38
39
|
const stringArray = (value, field, { min = 0 } = {}) => {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
if (!Array.isArray(value)) {
|
|
41
|
+
throw new Error(`${field} must be an array`);
|
|
42
|
+
}
|
|
43
|
+
const result = value.map((entry) => nonEmpty(entry, field));
|
|
44
|
+
if (result.length < min) {
|
|
45
|
+
throw new Error(`${field} must include at least ${min} item(s)`);
|
|
46
|
+
}
|
|
47
|
+
return [...new Set(result)];
|
|
47
48
|
};
|
|
48
49
|
|
|
49
50
|
export const validateInitRequest = (input) => {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
51
|
+
const parsed = parseWithSchema(
|
|
52
|
+
clueInitToolRequestSchema,
|
|
53
|
+
input,
|
|
54
|
+
"init request",
|
|
55
|
+
);
|
|
56
|
+
return {
|
|
57
|
+
...parsed,
|
|
58
|
+
allowed_source_paths: [...new Set(parsed.allowed_source_paths)],
|
|
59
|
+
excluded_source_paths: [...new Set(parsed.excluded_source_paths ?? [])],
|
|
60
|
+
};
|
|
60
61
|
};
|
|
61
62
|
|
|
62
63
|
export const buildInitReport = ({
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
request,
|
|
65
|
+
lifecycleInsertions,
|
|
66
|
+
requiredVariables = [],
|
|
67
|
+
warnings = [],
|
|
67
68
|
}) =>
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
69
|
+
parseWithSchema(
|
|
70
|
+
clueInitToolReportSchema,
|
|
71
|
+
{
|
|
72
|
+
target_tool: request.target_tool,
|
|
73
|
+
ci_workflow_path: request.ci_workflow_path,
|
|
74
|
+
ci_workflow_added: true,
|
|
75
|
+
required_secrets: ["CLUE_API_KEY", "CLUE_AI_PROVIDER_API_KEY"],
|
|
76
|
+
required_variables: requiredVariables,
|
|
77
|
+
lifecycle_insertions: lifecycleInsertions,
|
|
78
|
+
semantic_generation_timing: "after_merge_ci",
|
|
79
|
+
semantic_preview_generated: false,
|
|
80
|
+
client_repo_generated_files_committed: false,
|
|
81
|
+
frontend_api_key_exposed: false,
|
|
82
|
+
browser_token_endpoint_required: true,
|
|
83
|
+
browser_token_provider_required: true,
|
|
84
|
+
browser_token_endpoint_path: "/api/v1/ingest/browser-tokens",
|
|
85
|
+
clue_track_inserted: false,
|
|
86
|
+
clue_dom_tag_inserted: false,
|
|
87
|
+
allowed_source_paths: request.allowed_source_paths,
|
|
88
|
+
excluded_source_paths: request.excluded_source_paths,
|
|
89
|
+
warnings,
|
|
90
|
+
},
|
|
91
|
+
"init report",
|
|
92
|
+
);
|
|
88
93
|
|
|
89
94
|
export const validateSemanticCiRequest = (input) => ({
|
|
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
|
-
|
|
95
|
+
project_key: nonEmpty(input.project_key, "project_key"),
|
|
96
|
+
environment: nonEmpty(input.environment, "environment"),
|
|
97
|
+
service_key: nonEmpty(input.service_key, "service_key"),
|
|
98
|
+
repository: {
|
|
99
|
+
provider: nonEmpty(input.repository?.provider, "repository.provider"),
|
|
100
|
+
repository_id: nonEmpty(
|
|
101
|
+
input.repository?.repository_id,
|
|
102
|
+
"repository.repository_id",
|
|
103
|
+
),
|
|
104
|
+
...(optionalNonEmpty(input.repository?.owner)
|
|
105
|
+
? { owner: optionalNonEmpty(input.repository.owner) }
|
|
106
|
+
: {}),
|
|
107
|
+
...(optionalNonEmpty(input.repository?.name)
|
|
108
|
+
? { name: optionalNonEmpty(input.repository.name) }
|
|
109
|
+
: {}),
|
|
110
|
+
...(optionalNonEmpty(input.repository?.default_branch)
|
|
111
|
+
? { default_branch: optionalNonEmpty(input.repository.default_branch) }
|
|
112
|
+
: {}),
|
|
113
|
+
merge_commit: nonEmpty(
|
|
114
|
+
input.repository?.merge_commit,
|
|
115
|
+
"repository.merge_commit",
|
|
116
|
+
),
|
|
117
|
+
...(optionalNonEmpty(input.repository?.merged_at)
|
|
118
|
+
? { merged_at: optionalNonEmpty(input.repository.merged_at) }
|
|
119
|
+
: {}),
|
|
120
|
+
...(input.repository?.pull_request_number === undefined ||
|
|
121
|
+
input.repository?.pull_request_number === null
|
|
122
|
+
? {}
|
|
123
|
+
: { pull_request_number: input.repository.pull_request_number }),
|
|
124
|
+
...(optionalNonEmpty(input.repository?.workflow_run_id)
|
|
125
|
+
? { workflow_run_id: optionalNonEmpty(input.repository.workflow_run_id) }
|
|
126
|
+
: {}),
|
|
127
|
+
},
|
|
128
|
+
service: {
|
|
129
|
+
service_key: nonEmpty(
|
|
130
|
+
input.service?.service_key ?? input.service_key,
|
|
131
|
+
"service.service_key",
|
|
132
|
+
),
|
|
133
|
+
root_path: input.service?.root_path ?? null,
|
|
134
|
+
framework: input.service?.framework ?? "fastapi",
|
|
135
|
+
language: input.service?.language ?? "python",
|
|
136
|
+
},
|
|
137
|
+
allowed_source_paths: stringArray(
|
|
138
|
+
input.allowed_source_paths,
|
|
139
|
+
"allowed_source_paths",
|
|
140
|
+
{ min: 1 },
|
|
141
|
+
),
|
|
142
|
+
excluded_source_paths: stringArray(
|
|
143
|
+
input.excluded_source_paths ?? [],
|
|
144
|
+
"excluded_source_paths",
|
|
145
|
+
),
|
|
146
|
+
clue_api_base_url: nonEmpty(input.clue_api_base_url, "clue_api_base_url"),
|
|
147
|
+
ai_provider_base_url: null,
|
|
148
|
+
ai_provider: optionalNonEmpty(input.ai_provider),
|
|
149
|
+
ai_model: optionalNonEmpty(input.ai_model),
|
|
145
150
|
});
|
|
146
151
|
|
|
147
152
|
export const buildOperationSourceKey = (method, pathTemplate) =>
|
|
148
|
-
|
|
153
|
+
`route.${method.toUpperCase()}.${pathTemplate}`;
|
|
149
154
|
|
|
150
155
|
export const validateSemanticSnapshotRequest = (input) => {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
+
return parseWithSchema(
|
|
157
|
+
semanticSnapshotRequestSchema,
|
|
158
|
+
input,
|
|
159
|
+
"semantic snapshot",
|
|
160
|
+
);
|
|
156
161
|
};
|
|
157
162
|
|
|
158
163
|
export const validateSemanticSnapshotResponse = (input) => {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
+
return parseWithSchema(
|
|
165
|
+
semanticSnapshotResponseSchema,
|
|
166
|
+
input,
|
|
167
|
+
"semantic snapshot response",
|
|
168
|
+
);
|
|
164
169
|
};
|
package/src/init-tool.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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 { SEMANTIC_GEN_WORKFLOW_COMMAND } from "./cli-invocation.mjs";
|
|
4
5
|
import {
|
|
5
6
|
applyLifecyclePlan,
|
|
6
7
|
planLifecycleInsertions,
|
|
@@ -158,7 +159,7 @@ jobs:
|
|
|
158
159
|
CLUE_SEMANTIC_REQUEST_JSON: |
|
|
159
160
|
${indentMultiline(workflowRequestPayload(request), 12)}
|
|
160
161
|
run: |
|
|
161
|
-
|
|
162
|
+
${SEMANTIC_GEN_WORKFLOW_COMMAND}
|
|
162
163
|
`;
|
|
163
164
|
|
|
164
165
|
export const writeSemanticWorkflow = async ({ repoRoot, request }) => {
|
package/src/lifecycle-guard.mjs
CHANGED
|
@@ -1,134 +1,197 @@
|
|
|
1
1
|
const LIFECYCLE_CALL_PATTERN =
|
|
2
|
-
/\b(ClueInit|ClueIdentify|ClueSetAccount|ClueLogout)\s*\(/g;
|
|
3
|
-
const SAFE_HELPER_PATTERN =
|
|
4
|
-
/\b(?:safeClue|safe_clue|safe_clue_call|safeClueCall|withClueGuard|with_clue_guard)\s*\(/g;
|
|
2
|
+
/\b(ClueInit|ClueIdentify|ClueSetAccount|ClueLogout|clue_init_fastapi|clue_init_django)\s*\(/g;
|
|
5
3
|
|
|
6
|
-
const
|
|
7
|
-
let
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
4
|
+
export const stripSourceNoise = (text, { stripStrings = false } = {}) => {
|
|
5
|
+
let output = "";
|
|
6
|
+
let index = 0;
|
|
7
|
+
while (index < text.length) {
|
|
8
|
+
const char = text[index];
|
|
9
|
+
const next = text[index + 1];
|
|
10
|
+
if (char === "/" && next === "/") {
|
|
11
|
+
while (index < text.length && text[index] !== "\n") index += 1;
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (char === "#") {
|
|
15
|
+
while (index < text.length && text[index] !== "\n") index += 1;
|
|
16
|
+
continue;
|
|
14
17
|
}
|
|
18
|
+
if (char === "/" && next === "*") {
|
|
19
|
+
index += 2;
|
|
20
|
+
while (
|
|
21
|
+
index < text.length &&
|
|
22
|
+
!(text[index] === "*" && text[index + 1] === "/")
|
|
23
|
+
) {
|
|
24
|
+
if (text[index] === "\n") output += "\n";
|
|
25
|
+
index += 1;
|
|
26
|
+
}
|
|
27
|
+
index += index < text.length ? 2 : 0;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
31
|
+
const quote = char;
|
|
32
|
+
const triple =
|
|
33
|
+
quote !== "`" && text.slice(index, index + 3) === quote.repeat(3);
|
|
34
|
+
const endToken = triple ? quote.repeat(3) : quote;
|
|
35
|
+
if (!stripStrings) {
|
|
36
|
+
output += triple ? endToken : quote;
|
|
37
|
+
} else {
|
|
38
|
+
output += quote === "`" ? "``" : `${quote}${quote}`;
|
|
39
|
+
}
|
|
40
|
+
index += triple ? 3 : 1;
|
|
41
|
+
while (index < text.length) {
|
|
42
|
+
if (!triple && text[index] === "\\") {
|
|
43
|
+
if (!stripStrings) output += text.slice(index, index + 2);
|
|
44
|
+
index += 2;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (text.slice(index, index + endToken.length) === endToken) {
|
|
48
|
+
if (!stripStrings) output += endToken;
|
|
49
|
+
index += endToken.length;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
if (text[index] === "\n") output += "\n";
|
|
53
|
+
if (!stripStrings && text[index] !== "\n") output += text[index];
|
|
54
|
+
index += 1;
|
|
55
|
+
}
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
output += char;
|
|
59
|
+
index += 1;
|
|
15
60
|
}
|
|
16
|
-
return
|
|
61
|
+
return output;
|
|
17
62
|
};
|
|
18
63
|
|
|
19
|
-
const
|
|
20
|
-
|
|
64
|
+
const isIdentifierCharacter = (character) =>
|
|
65
|
+
typeof character === "string" && /[A-Za-z0-9_$]/.test(character);
|
|
21
66
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
67
|
+
const startsKeyword = (text, index, keyword) =>
|
|
68
|
+
text.startsWith(keyword, index) &&
|
|
69
|
+
!isIdentifierCharacter(text[index - 1]) &&
|
|
70
|
+
!isIdentifierCharacter(text[index + keyword.length]);
|
|
26
71
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
72
|
+
const advanceQuotedString = (text, startIndex, { preserve = false } = {}) => {
|
|
73
|
+
const quote = text[startIndex];
|
|
74
|
+
const triple = quote !== "`" && text.slice(startIndex, startIndex + 3) === quote.repeat(3);
|
|
75
|
+
const endToken = triple ? quote.repeat(3) : quote;
|
|
76
|
+
let index = startIndex + (triple ? 3 : 1);
|
|
77
|
+
let value = preserve ? (triple ? endToken : quote) : "";
|
|
78
|
+
while (index < text.length) {
|
|
79
|
+
if (!triple && text[index] === "\\") {
|
|
80
|
+
if (preserve) value += text.slice(index, index + 2);
|
|
81
|
+
index += 2;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (text.slice(index, index + endToken.length) === endToken) {
|
|
85
|
+
if (preserve) value += endToken;
|
|
86
|
+
index += endToken.length;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
if (preserve) value += text[index];
|
|
90
|
+
index += 1;
|
|
39
91
|
}
|
|
40
|
-
return
|
|
92
|
+
return { index, value };
|
|
41
93
|
};
|
|
42
94
|
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
)
|
|
54
|
-
|
|
95
|
+
export const extractExecutableModuleStatements = (text) => {
|
|
96
|
+
const statements = [];
|
|
97
|
+
let index = 0;
|
|
98
|
+
while (index < text.length) {
|
|
99
|
+
const char = text[index];
|
|
100
|
+
const next = text[index + 1];
|
|
101
|
+
if (char === "/" && next === "/") {
|
|
102
|
+
while (index < text.length && text[index] !== "\n") index += 1;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (char === "#") {
|
|
106
|
+
while (index < text.length && text[index] !== "\n") index += 1;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (char === "/" && next === "*") {
|
|
110
|
+
index += 2;
|
|
111
|
+
while (
|
|
112
|
+
index < text.length &&
|
|
113
|
+
!(text[index] === "*" && text[index + 1] === "/")
|
|
114
|
+
) {
|
|
115
|
+
index += 1;
|
|
116
|
+
}
|
|
117
|
+
index += index < text.length ? 2 : 0;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
121
|
+
index = advanceQuotedString(text, index).index;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (!startsKeyword(text, index, "import") && !startsKeyword(text, index, "export")) {
|
|
125
|
+
index += 1;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
let statement = "";
|
|
129
|
+
let depth = 0;
|
|
130
|
+
while (index < text.length) {
|
|
131
|
+
const current = text[index];
|
|
132
|
+
const following = text[index + 1];
|
|
133
|
+
if (current === "/" && following === "/") {
|
|
134
|
+
while (index < text.length && text[index] !== "\n") index += 1;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (current === "/" && following === "*") {
|
|
138
|
+
index += 2;
|
|
139
|
+
while (
|
|
140
|
+
index < text.length &&
|
|
141
|
+
!(text[index] === "*" && text[index + 1] === "/")
|
|
142
|
+
) {
|
|
143
|
+
index += 1;
|
|
144
|
+
}
|
|
145
|
+
index += index < text.length ? 2 : 0;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (current === "'" || current === '"' || current === "`") {
|
|
149
|
+
const quoted = advanceQuotedString(text, index, { preserve: true });
|
|
150
|
+
statement += quoted.value;
|
|
151
|
+
index = quoted.index;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
statement += current;
|
|
155
|
+
if (current === "{" || current === "(" || current === "[") depth += 1;
|
|
156
|
+
if (current === "}" || current === ")" || current === "]") {
|
|
157
|
+
depth = Math.max(0, depth - 1);
|
|
158
|
+
}
|
|
159
|
+
index += 1;
|
|
160
|
+
if (current === ";" && depth === 0) break;
|
|
161
|
+
if (current === "\n" && depth === 0) break;
|
|
162
|
+
}
|
|
163
|
+
const trimmed = statement.trim();
|
|
164
|
+
if (trimmed) statements.push(trimmed);
|
|
55
165
|
}
|
|
56
|
-
return
|
|
166
|
+
return statements;
|
|
57
167
|
};
|
|
58
168
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
let start = 0;
|
|
62
|
-
for (const line of text.split("\n")) {
|
|
63
|
-
const indent = line.match(/^\s*/)?.[0].length ?? 0;
|
|
64
|
-
lines.push({
|
|
65
|
-
start,
|
|
66
|
-
end: start + line.length,
|
|
67
|
-
indent,
|
|
68
|
-
text: line,
|
|
69
|
-
});
|
|
70
|
-
start += line.length + 1;
|
|
71
|
-
}
|
|
72
|
-
return lines;
|
|
73
|
-
};
|
|
169
|
+
const lineNumberForIndex = (text, index) =>
|
|
170
|
+
text.slice(0, index).split("\n").length;
|
|
74
171
|
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
(line) => line.start <= callIndex && callIndex <= line.end,
|
|
79
|
-
);
|
|
80
|
-
if (callLineIndex < 0) return false;
|
|
81
|
-
const callLine = lines[callLineIndex];
|
|
82
|
-
for (let index = callLineIndex - 1; index >= 0; index -= 1) {
|
|
83
|
-
const candidate = lines[index];
|
|
84
|
-
if (!candidate.text.trim()) continue;
|
|
85
|
-
if (!/^\s*try\s*:\s*(?:#.*)?$/.test(candidate.text)) continue;
|
|
86
|
-
if (candidate.indent >= callLine.indent) continue;
|
|
87
|
-
const escaped = lines
|
|
88
|
-
.slice(index + 1, callLineIndex)
|
|
89
|
-
.some(
|
|
90
|
-
(line) =>
|
|
91
|
-
line.text.trim() &&
|
|
92
|
-
line.indent <= candidate.indent &&
|
|
93
|
-
!/^\s*(?:except|finally|else)\b/.test(line.text),
|
|
94
|
-
);
|
|
95
|
-
if (!escaped) return true;
|
|
96
|
-
}
|
|
97
|
-
return false;
|
|
172
|
+
const isAwaitedOnCallLine = (text, callIndex) => {
|
|
173
|
+
const lineStart = text.lastIndexOf("\n", callIndex - 1) + 1;
|
|
174
|
+
return /\bawait\b/.test(text.slice(lineStart, callIndex));
|
|
98
175
|
};
|
|
99
176
|
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return
|
|
177
|
+
const canonicalLifecycleApiName = (apiName) => {
|
|
178
|
+
if (apiName === "clue_init_fastapi" || apiName === "clue_init_django") {
|
|
179
|
+
return "ClueInit";
|
|
180
|
+
}
|
|
181
|
+
return apiName;
|
|
105
182
|
};
|
|
106
183
|
|
|
107
|
-
const isGuardedLifecycleCall = (text, callIndex) =>
|
|
108
|
-
hasCatchHandlerOnCall(text, callIndex) ||
|
|
109
|
-
isInsideSafeHelperCall(text, callIndex) ||
|
|
110
|
-
isInsideJsTryBlock(text, callIndex) ||
|
|
111
|
-
isInsidePythonTryBlock(text, callIndex);
|
|
112
|
-
|
|
113
184
|
export const findLifecycleGuardViolations = (text) => {
|
|
114
185
|
const violations = [];
|
|
115
186
|
for (const match of text.matchAll(LIFECYCLE_CALL_PATTERN)) {
|
|
116
187
|
const callIndex = match.index ?? 0;
|
|
117
|
-
const apiName = match[1];
|
|
188
|
+
const apiName = canonicalLifecycleApiName(match[1]);
|
|
118
189
|
if (isAwaitedOnCallLine(text, callIndex)) {
|
|
119
190
|
violations.push({
|
|
120
191
|
api_name: apiName,
|
|
121
192
|
line: lineNumberForIndex(text, callIndex),
|
|
122
193
|
reason: "awaited_lifecycle_call",
|
|
123
194
|
});
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
if (!isGuardedLifecycleCall(text, callIndex)) {
|
|
127
|
-
violations.push({
|
|
128
|
-
api_name: apiName,
|
|
129
|
-
line: lineNumberForIndex(text, callIndex),
|
|
130
|
-
reason: "unguarded_lifecycle_call",
|
|
131
|
-
});
|
|
132
195
|
}
|
|
133
196
|
}
|
|
134
197
|
return violations;
|
|
@@ -136,6 +199,8 @@ export const findLifecycleGuardViolations = (text) => {
|
|
|
136
199
|
|
|
137
200
|
export const findLifecycleCallApiNames = (text) => [
|
|
138
201
|
...new Set(
|
|
139
|
-
[...text.matchAll(LIFECYCLE_CALL_PATTERN)].map((match) =>
|
|
202
|
+
[...text.matchAll(LIFECYCLE_CALL_PATTERN)].map((match) =>
|
|
203
|
+
canonicalLifecycleApiName(match[1]),
|
|
204
|
+
),
|
|
140
205
|
),
|
|
141
206
|
];
|