@wix/ditto-codegen-public 1.0.482 → 1.0.484
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/dist/opencode-tools/batch-read.ts +58 -10
- package/dist/opencode-tools/validate.ts +168 -5
- package/dist/out.js +2306 -2137
- package/package.json +2 -2
|
@@ -2,27 +2,75 @@ import { tool } from "@opencode-ai/plugin";
|
|
|
2
2
|
import { readFile } from "fs/promises";
|
|
3
3
|
import { join, isAbsolute } from "path";
|
|
4
4
|
|
|
5
|
+
// opencode cuts any tool output over 2,000 lines or 50 KB and saves the rest to
|
|
6
|
+
// a file the agent rarely reads, so every file after the cut is silently lost.
|
|
7
|
+
// Stay well under both limits and name the files that didn't fit instead.
|
|
8
|
+
export const OUTPUT_LIMIT = { bytes: 40 * 1024, lines: 1500 };
|
|
9
|
+
|
|
10
|
+
const SECTION_SEPARATOR = "\n\n";
|
|
11
|
+
|
|
12
|
+
async function readSection(
|
|
13
|
+
filePath: string,
|
|
14
|
+
directory: string,
|
|
15
|
+
): Promise<string> {
|
|
16
|
+
const fullPath = isAbsolute(filePath) ? filePath : join(directory, filePath);
|
|
17
|
+
try {
|
|
18
|
+
const content = await readFile(fullPath, "utf-8");
|
|
19
|
+
return `--- ${filePath} ---\n${content}`;
|
|
20
|
+
} catch (e) {
|
|
21
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
22
|
+
return `--- ${filePath} ---\nERROR: ${msg}`;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function notReadSection(paths: string[]): string {
|
|
27
|
+
return (
|
|
28
|
+
`--- NOT READ (output limit) ---\n` +
|
|
29
|
+
`These files were not read, to keep this output under the tool output limit. ` +
|
|
30
|
+
`Read them with another batch-read call:\n` +
|
|
31
|
+
paths.map((path) => `- ${path}`).join("\n")
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
5
35
|
export default tool({
|
|
6
36
|
description:
|
|
7
37
|
"Read multiple files at once and return their contents. " +
|
|
8
|
-
"Use this instead of calling read multiple times sequentially."
|
|
38
|
+
"Use this instead of calling read multiple times sequentially. " +
|
|
39
|
+
`Returns whole files, in order, up to about ${OUTPUT_LIMIT.bytes / 1024} KB and ${OUTPUT_LIMIT.lines} lines per call; ` +
|
|
40
|
+
"any files past that are listed as not read, so read them in another call.",
|
|
9
41
|
args: {
|
|
10
42
|
paths: tool.schema
|
|
11
43
|
.array(tool.schema.string())
|
|
12
44
|
.describe("Array of file paths (relative to project root) to read"),
|
|
13
45
|
},
|
|
14
46
|
async execute(args, context) {
|
|
15
|
-
const
|
|
47
|
+
const sections: string[] = [];
|
|
48
|
+
const notRead: string[] = [];
|
|
49
|
+
let bytes = 0;
|
|
50
|
+
let lines = 0;
|
|
16
51
|
for (const filePath of args.paths) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
52
|
+
if (notRead.length > 0) {
|
|
53
|
+
notRead.push(filePath);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const section = await readSection(filePath, context.directory);
|
|
57
|
+
const sectionBytes =
|
|
58
|
+
Buffer.byteLength(section) + SECTION_SEPARATOR.length;
|
|
59
|
+
const sectionLines = section.split("\n").length + 1;
|
|
60
|
+
// The first file always goes in: alone, it is no worse than a plain read.
|
|
61
|
+
const fits =
|
|
62
|
+
sections.length === 0 ||
|
|
63
|
+
(bytes + sectionBytes <= OUTPUT_LIMIT.bytes &&
|
|
64
|
+
lines + sectionLines <= OUTPUT_LIMIT.lines);
|
|
65
|
+
if (!fits) {
|
|
66
|
+
notRead.push(filePath);
|
|
67
|
+
continue;
|
|
24
68
|
}
|
|
69
|
+
sections.push(section);
|
|
70
|
+
bytes += sectionBytes;
|
|
71
|
+
lines += sectionLines;
|
|
25
72
|
}
|
|
26
|
-
|
|
73
|
+
if (notRead.length > 0) sections.push(notReadSection(notRead));
|
|
74
|
+
return sections.join(SECTION_SEPARATOR);
|
|
27
75
|
},
|
|
28
76
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { tool } from "@opencode-ai/plugin";
|
|
2
|
-
import { readdir } from "node:fs/promises";
|
|
2
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
|
|
5
6
|
const EDITOR_COMPONENTS_DIR = "src/extensions/site/components";
|
|
@@ -15,12 +16,173 @@ async function containsTypeScriptFile(directory: string): Promise<boolean> {
|
|
|
15
16
|
|
|
16
17
|
for (const entry of entries) {
|
|
17
18
|
const path = join(directory, entry.name);
|
|
18
|
-
if (entry.isDirectory() && (await containsTypeScriptFile(path)))
|
|
19
|
+
if (entry.isDirectory() && (await containsTypeScriptFile(path)))
|
|
20
|
+
return true;
|
|
19
21
|
if (entry.isFile() && /\.tsx?$/.test(entry.name)) return true;
|
|
20
22
|
}
|
|
21
23
|
return false;
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
const SAFE_SPECIFIER = /^[\w@./-]{1,100}$/;
|
|
27
|
+
|
|
28
|
+
const MISSING_SPECIFIER_FIX: Record<string, string> = {
|
|
29
|
+
contextProvider:
|
|
30
|
+
"add `moduleSpecifier: '<prefix><provider-kebab-case-name>'` to resources.contextSpecifier",
|
|
31
|
+
functionLibrary:
|
|
32
|
+
"add `moduleSpecifier: '<prefix><library-name>'` to resources",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type TypeScript = typeof import("typescript");
|
|
36
|
+
|
|
37
|
+
// The project's own TypeScript (the one its tsc step uses), else ours.
|
|
38
|
+
function loadTypeScript(cwd: string): TypeScript | undefined {
|
|
39
|
+
for (const base of [cwd, process.cwd()]) {
|
|
40
|
+
try {
|
|
41
|
+
return createRequire(join(base, "package.json"))(
|
|
42
|
+
"typescript",
|
|
43
|
+
) as TypeScript;
|
|
44
|
+
} catch {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Reads moduleSpecifier only from the object passed to contextProvider() or
|
|
52
|
+
// functionLibrary(), so comments, strings and other calls never count. A value
|
|
53
|
+
// is undefined unless it is a plain single-line quoted string.
|
|
54
|
+
function readModuleSpecifiers(
|
|
55
|
+
ts: TypeScript,
|
|
56
|
+
file: string,
|
|
57
|
+
source: string,
|
|
58
|
+
): { kind?: string; values: (string | undefined)[] } {
|
|
59
|
+
const found: { kind?: string; values: (string | undefined)[] } = {
|
|
60
|
+
values: [],
|
|
61
|
+
};
|
|
62
|
+
const visit = (node: import("typescript").Node, inCall: boolean): void => {
|
|
63
|
+
if (ts.isCallExpression(node)) {
|
|
64
|
+
const callee = node.expression;
|
|
65
|
+
const name = ts.isPropertyAccessExpression(callee)
|
|
66
|
+
? callee.name.text
|
|
67
|
+
: ts.isIdentifier(callee)
|
|
68
|
+
? callee.text
|
|
69
|
+
: undefined;
|
|
70
|
+
if (name === "contextProvider" || name === "functionLibrary") {
|
|
71
|
+
found.kind ??= name;
|
|
72
|
+
node.arguments.forEach((argument) => visit(argument, true));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (
|
|
77
|
+
inCall &&
|
|
78
|
+
(ts.isPropertyAssignment(node) || ts.isShorthandPropertyAssignment(node))
|
|
79
|
+
) {
|
|
80
|
+
const key =
|
|
81
|
+
ts.isIdentifier(node.name) || ts.isStringLiteral(node.name)
|
|
82
|
+
? node.name.text
|
|
83
|
+
: undefined;
|
|
84
|
+
if (key === "moduleSpecifier") {
|
|
85
|
+
const value =
|
|
86
|
+
ts.isPropertyAssignment(node) &&
|
|
87
|
+
ts.isStringLiteral(node.initializer) &&
|
|
88
|
+
!/['"`\r\n]/.test(node.initializer.text)
|
|
89
|
+
? node.initializer.text
|
|
90
|
+
: undefined;
|
|
91
|
+
found.values.push(value);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
ts.forEachChild(node, (child) => visit(child, inCall));
|
|
96
|
+
};
|
|
97
|
+
visit(ts.createSourceFile(file, source, ts.ScriptTarget.Latest), false);
|
|
98
|
+
return found;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function checkModuleSpecifiers(cwd: string): Promise<string | undefined> {
|
|
102
|
+
const prefix = process.env.MODULE_SPECIFIER_PREFIX;
|
|
103
|
+
if (!prefix) return undefined;
|
|
104
|
+
|
|
105
|
+
const gitEnv = { ...process.env, LC_ALL: "C" };
|
|
106
|
+
const listings = await Promise.all([
|
|
107
|
+
Bun.$`git ls-files --others --exclude-standard -- src`
|
|
108
|
+
.cwd(cwd)
|
|
109
|
+
.env(gitEnv)
|
|
110
|
+
.nothrow()
|
|
111
|
+
.quiet(),
|
|
112
|
+
Bun.$`git diff --cached --name-only --diff-filter=A --relative -- src`
|
|
113
|
+
.cwd(cwd)
|
|
114
|
+
.env(gitEnv)
|
|
115
|
+
.nothrow()
|
|
116
|
+
.quiet(),
|
|
117
|
+
]);
|
|
118
|
+
for (const listing of listings) {
|
|
119
|
+
const stderr = listing.stderr.toString();
|
|
120
|
+
if (listing.exitCode === 0) continue;
|
|
121
|
+
if (
|
|
122
|
+
listing.exitCode === 128 &&
|
|
123
|
+
/^fatal: not a git repository\b/m.test(stderr)
|
|
124
|
+
)
|
|
125
|
+
return undefined;
|
|
126
|
+
return `FAILED at moduleSpecifier prefix check: git exited ${listing.exitCode}:\n${stderr}`;
|
|
127
|
+
}
|
|
128
|
+
const newExtensionFiles = [
|
|
129
|
+
...new Set(
|
|
130
|
+
listings
|
|
131
|
+
.flatMap((listing) => listing.stdout.toString().split("\n"))
|
|
132
|
+
.map((file) => file.trim())
|
|
133
|
+
.filter((file) => file.endsWith(".extension.ts")),
|
|
134
|
+
),
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
const violations: string[] = [];
|
|
138
|
+
const ts = newExtensionFiles.length > 0 ? loadTypeScript(cwd) : undefined;
|
|
139
|
+
if (newExtensionFiles.length > 0 && !ts) {
|
|
140
|
+
violations.push(
|
|
141
|
+
"- cannot load the project's typescript package to read moduleSpecifier; run npm install",
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
for (const file of newExtensionFiles) {
|
|
145
|
+
if (!ts) break;
|
|
146
|
+
const source = await readFile(join(cwd, file), "utf8").catch(() => "");
|
|
147
|
+
const { kind, values } = readModuleSpecifiers(ts, file, source);
|
|
148
|
+
if (!kind) continue;
|
|
149
|
+
if (values.length === 0) {
|
|
150
|
+
violations.push(
|
|
151
|
+
`- ${file}: no moduleSpecifier; ${MISSING_SPECIFIER_FIX[kind].replace("<prefix>", prefix)}`,
|
|
152
|
+
);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
for (const specifier of values) {
|
|
156
|
+
if (specifier === undefined) {
|
|
157
|
+
violations.push(
|
|
158
|
+
`- ${file}: moduleSpecifier is not a plain string literal; write it as '${prefix}<name>'`,
|
|
159
|
+
);
|
|
160
|
+
} else if (specifier.length > 100) {
|
|
161
|
+
violations.push(
|
|
162
|
+
`- ${file}: moduleSpecifier is over 100 characters; shorten it to '${prefix}<name>'`,
|
|
163
|
+
);
|
|
164
|
+
} else if (!specifier.startsWith(prefix)) {
|
|
165
|
+
violations.push(
|
|
166
|
+
SAFE_SPECIFIER.test(specifier) &&
|
|
167
|
+
prefix.length + specifier.length <= 100
|
|
168
|
+
? `- ${file}: '${specifier}' -> '${prefix}${specifier}'`
|
|
169
|
+
: `- ${file}: moduleSpecifier must be '${prefix}<name>' (letters, digits, @ . _ / - only, at most 100 characters)`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (violations.length === 0) return undefined;
|
|
175
|
+
|
|
176
|
+
return [
|
|
177
|
+
"FAILED at moduleSpecifier prefix check:",
|
|
178
|
+
...violations,
|
|
179
|
+
`Every new context provider and function library must declare a moduleSpecifier that starts with '${prefix}'. ` +
|
|
180
|
+
"Module specifiers are unique across all of Wix, so an unprefixed one can belong to " +
|
|
181
|
+
'another app and fail the checkpoint with "Error during component specifier upsert". ' +
|
|
182
|
+
"Only new extensions are checked: never change the moduleSpecifier of an existing one.",
|
|
183
|
+
].join("\n");
|
|
184
|
+
}
|
|
185
|
+
|
|
24
186
|
export default tool({
|
|
25
187
|
description:
|
|
26
188
|
"Run project validation: TypeScript check by default, optionally a full build " +
|
|
@@ -74,6 +236,9 @@ export default tool({
|
|
|
74
236
|
}
|
|
75
237
|
steps.push("tsc --noEmit -p .: OK");
|
|
76
238
|
|
|
239
|
+
const specifierError = await checkModuleSpecifiers(cwd);
|
|
240
|
+
if (specifierError) return [...steps, specifierError].join("\n");
|
|
241
|
+
|
|
77
242
|
if (!args.runBuild) {
|
|
78
243
|
steps.push("wix build: skipped (pass runBuild: true to verify build)");
|
|
79
244
|
return steps.join("\n");
|
|
@@ -94,9 +259,7 @@ export default tool({
|
|
|
94
259
|
join(cwd, EDITOR_COMPONENTS_DIR),
|
|
95
260
|
);
|
|
96
261
|
if (!hasEditorComponents) {
|
|
97
|
-
steps.push(
|
|
98
|
-
"wix generate manifest: skipped (no editor components)",
|
|
99
|
-
);
|
|
262
|
+
steps.push("wix generate manifest: skipped (no editor components)");
|
|
100
263
|
return steps.join("\n");
|
|
101
264
|
}
|
|
102
265
|
|