@coreframe/scripts 0.0.0 → 0.1.0
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/bin/coreframe.js +35 -0
- package/config.test.ts +15 -0
- package/config.ts +9 -0
- package/coreframe.ts +272 -0
- package/db/client.ts +72 -0
- package/db/config.ts +41 -0
- package/db/drizzle-kit.test.ts +27 -0
- package/db/drizzle-kit.ts +39 -0
- package/db/ensure.ts +67 -0
- package/db/migrate-held.test.ts +81 -0
- package/db/migrate-held.ts +166 -0
- package/db/migrate.ts +39 -0
- package/db/project.ts +28 -0
- package/db/reset.ts +24 -0
- package/db/seed.ts +38 -0
- package/db/setup.ts +18 -0
- package/deploy/check-migrations.ts +31 -0
- package/deploy/checks.ts +38 -0
- package/deploy/config.test.ts +24 -0
- package/deploy/config.ts +68 -0
- package/deploy/migrations.test.ts +110 -0
- package/deploy/migrations.ts +317 -0
- package/deploy/pending-migrations.test.ts +93 -0
- package/deploy/pending-migrations.ts +45 -0
- package/deploy/tauri-release.test.ts +196 -0
- package/deploy/tauri-release.ts +816 -0
- package/deploy/version.test.ts +29 -0
- package/deploy/version.ts +65 -0
- package/deploy.test.ts +101 -0
- package/deploy.ts +346 -0
- package/dev.test.ts +153 -0
- package/dev.ts +155 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +3 -0
- package/dist/coreframe.d.ts +280 -0
- package/dist/coreframe.js +240 -0
- package/dist/db/client.d.ts +22 -0
- package/dist/db/client.js +29 -0
- package/dist/db/config.d.ts +21 -0
- package/dist/db/config.js +32 -0
- package/dist/db/drizzle-kit.d.ts +8 -0
- package/dist/db/drizzle-kit.js +22 -0
- package/dist/db/ensure.d.ts +2 -0
- package/dist/db/ensure.js +40 -0
- package/dist/db/migrate-held.d.ts +12 -0
- package/dist/db/migrate-held.js +119 -0
- package/dist/db/migrate.d.ts +2 -0
- package/dist/db/migrate.js +25 -0
- package/dist/db/project.d.ts +8 -0
- package/dist/db/project.js +9 -0
- package/dist/db/reset.d.ts +2 -0
- package/dist/db/reset.js +19 -0
- package/dist/db/seed.d.ts +3 -0
- package/dist/db/seed.js +23 -0
- package/dist/db/setup.d.ts +3 -0
- package/dist/db/setup.js +15 -0
- package/dist/deploy/check-migrations.d.ts +2 -0
- package/dist/deploy/check-migrations.js +18 -0
- package/dist/deploy/checks.d.ts +2 -0
- package/dist/deploy/checks.js +27 -0
- package/dist/deploy/config.d.ts +50 -0
- package/dist/deploy/config.js +9 -0
- package/dist/deploy/migrations.d.ts +21 -0
- package/dist/deploy/migrations.js +203 -0
- package/dist/deploy/pending-migrations.d.ts +5 -0
- package/dist/deploy/pending-migrations.js +30 -0
- package/dist/deploy/tauri-release.d.ts +85 -0
- package/dist/deploy/tauri-release.js +512 -0
- package/dist/deploy/version.d.ts +18 -0
- package/dist/deploy/version.js +41 -0
- package/dist/deploy.d.ts +17 -0
- package/dist/deploy.js +247 -0
- package/dist/dev.d.ts +13 -0
- package/dist/dev.js +96 -0
- package/dist/image-generator.d.ts +2 -0
- package/dist/image-generator.js +59 -0
- package/dist/project.d.ts +1 -0
- package/dist/project.js +13 -0
- package/dist/typecheck.d.ts +2 -0
- package/dist/typecheck.js +45 -0
- package/dist/upgrade/check-skill-current.d.ts +6 -0
- package/dist/upgrade/check-skill-current.js +88 -0
- package/dist/upgrade/ensure-template-remote.d.ts +6 -0
- package/dist/upgrade/ensure-template-remote.js +57 -0
- package/dist/upgrade/fast-forward-template-files.d.ts +7 -0
- package/dist/upgrade/fast-forward-template-files.js +280 -0
- package/image-generator.ts +84 -0
- package/package.json +48 -8
- package/project.ts +6 -0
- package/tsconfig.build.json +12 -0
- package/tsconfig.json +20 -0
- package/typecheck.ts +55 -0
- package/upgrade/check-skill-current.ts +112 -0
- package/upgrade/ensure-template-remote.ts +79 -0
- package/upgrade/fast-forward-template-files.ts +408 -0
- package/readme.md +0 -1
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync, } from "node:fs";
|
|
4
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
5
|
+
const projectOwnedPathPrefixes = ["drizzle/", "drizzle-hold/"];
|
|
6
|
+
const supportedModes = new Set(["100644", "100755", "120000"]);
|
|
7
|
+
function git(args, options = {}) {
|
|
8
|
+
return spawnSync("git", args, options);
|
|
9
|
+
}
|
|
10
|
+
function gitText(args) {
|
|
11
|
+
const result = git(args, { encoding: "utf8" });
|
|
12
|
+
if (result.status !== 0) {
|
|
13
|
+
const detail = result.stderr.toString().trim() || result.stdout.toString().trim();
|
|
14
|
+
throw new Error(`Git command failed: git ${args.join(" ")}${detail ? `\n${detail}` : ""}`);
|
|
15
|
+
}
|
|
16
|
+
return result.stdout.toString();
|
|
17
|
+
}
|
|
18
|
+
function gitBuffer(args) {
|
|
19
|
+
const result = git(args);
|
|
20
|
+
if (result.status !== 0) {
|
|
21
|
+
const detail = result.stderr.toString("utf8").trim() || result.stdout.toString("utf8").trim();
|
|
22
|
+
throw new Error(`Git command failed: git ${args.join(" ")}${detail ? `\n${detail}` : ""}`);
|
|
23
|
+
}
|
|
24
|
+
return result.stdout;
|
|
25
|
+
}
|
|
26
|
+
function getRepoRoot() {
|
|
27
|
+
return gitText(["rev-parse", "--show-toplevel"]).trim();
|
|
28
|
+
}
|
|
29
|
+
function readPackageCoreframeVersion(repoRoot) {
|
|
30
|
+
const packagePath = join(repoRoot, "package.json");
|
|
31
|
+
if (!existsSync(packagePath)) {
|
|
32
|
+
throw new Error("Missing --base <git-ref>, and package.json does not exist.");
|
|
33
|
+
}
|
|
34
|
+
const packageJson = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
35
|
+
const version = packageJson.engines?.coreframe;
|
|
36
|
+
if (typeof version !== "string" || version.length === 0) {
|
|
37
|
+
throw new Error("Missing --base <git-ref>, and package.json engines.coreframe is not set.");
|
|
38
|
+
}
|
|
39
|
+
return version;
|
|
40
|
+
}
|
|
41
|
+
function splitNullTerminated(buffer) {
|
|
42
|
+
const fields = [];
|
|
43
|
+
let start = 0;
|
|
44
|
+
for (let index = 0; index < buffer.length; index += 1) {
|
|
45
|
+
if (buffer[index] !== 0) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
fields.push(buffer.subarray(start, index).toString("utf8"));
|
|
49
|
+
start = index + 1;
|
|
50
|
+
}
|
|
51
|
+
return fields.filter((field) => field.length > 0);
|
|
52
|
+
}
|
|
53
|
+
function listChangedPaths(base, target) {
|
|
54
|
+
const output = gitBuffer(["diff", "--name-status", "-z", "--no-renames", base, target, "--"]);
|
|
55
|
+
const fields = splitNullTerminated(output);
|
|
56
|
+
const changes = [];
|
|
57
|
+
for (let index = 0; index < fields.length; index += 2) {
|
|
58
|
+
const status = fields[index];
|
|
59
|
+
const path = fields[index + 1];
|
|
60
|
+
if (!status || !path) {
|
|
61
|
+
throw new Error("Could not parse git diff --name-status output.");
|
|
62
|
+
}
|
|
63
|
+
changes.push({ path, status });
|
|
64
|
+
}
|
|
65
|
+
return changes;
|
|
66
|
+
}
|
|
67
|
+
function getGitEntry(ref, path) {
|
|
68
|
+
const output = gitBuffer(["ls-tree", "-z", ref, "--", path]);
|
|
69
|
+
if (output.length === 0) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const record = splitNullTerminated(output)[0];
|
|
73
|
+
const tabIndex = record.indexOf("\t");
|
|
74
|
+
if (tabIndex === -1) {
|
|
75
|
+
throw new Error(`Could not parse git ls-tree output for ${ref}:${path}.`);
|
|
76
|
+
}
|
|
77
|
+
const [mode, type, object] = record.slice(0, tabIndex).split(" ");
|
|
78
|
+
if (!mode || !type || !object) {
|
|
79
|
+
throw new Error(`Could not parse git ls-tree metadata for ${ref}:${path}.`);
|
|
80
|
+
}
|
|
81
|
+
return { mode, object, path, type };
|
|
82
|
+
}
|
|
83
|
+
function readGitBlob(entry) {
|
|
84
|
+
return gitBuffer(["cat-file", "blob", entry.object]);
|
|
85
|
+
}
|
|
86
|
+
function safeWorktreePath(repoRoot, path) {
|
|
87
|
+
const absolutePath = resolve(repoRoot, path);
|
|
88
|
+
const rootPrefix = repoRoot.endsWith(sep) ? repoRoot : `${repoRoot}${sep}`;
|
|
89
|
+
if (absolutePath !== repoRoot && !absolutePath.startsWith(rootPrefix)) {
|
|
90
|
+
throw new Error(`Refusing to access path outside repository: ${path}`);
|
|
91
|
+
}
|
|
92
|
+
return absolutePath;
|
|
93
|
+
}
|
|
94
|
+
function pathExists(path) {
|
|
95
|
+
try {
|
|
96
|
+
lstatSync(path);
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function isExecutable(stat) {
|
|
107
|
+
return (stat.mode & 0o111) !== 0;
|
|
108
|
+
}
|
|
109
|
+
function entryIsSupported(entry) {
|
|
110
|
+
return entry === null || (entry.type === "blob" && supportedModes.has(entry.mode));
|
|
111
|
+
}
|
|
112
|
+
function isProjectOwnedPath(path) {
|
|
113
|
+
return projectOwnedPathPrefixes.some((prefix) => path === prefix.slice(0, -1) || path.startsWith(prefix));
|
|
114
|
+
}
|
|
115
|
+
function currentMatchesEntry(repoRoot, path, entry) {
|
|
116
|
+
const worktreePath = safeWorktreePath(repoRoot, path);
|
|
117
|
+
if (entry === null) {
|
|
118
|
+
return { matches: !pathExists(worktreePath) };
|
|
119
|
+
}
|
|
120
|
+
if (!entryIsSupported(entry)) {
|
|
121
|
+
return { matches: false, reason: `unsupported template entry mode ${entry.mode} ${entry.type}` };
|
|
122
|
+
}
|
|
123
|
+
let stat;
|
|
124
|
+
try {
|
|
125
|
+
stat = lstatSync(worktreePath);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
129
|
+
return { matches: false, reason: "path is missing from worktree" };
|
|
130
|
+
}
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
const expected = readGitBlob(entry);
|
|
134
|
+
if (entry.mode === "120000") {
|
|
135
|
+
if (!stat.isSymbolicLink()) {
|
|
136
|
+
return { matches: false, reason: "worktree path is not a symlink" };
|
|
137
|
+
}
|
|
138
|
+
const linkTarget = Buffer.from(readlinkSync(worktreePath), "utf8");
|
|
139
|
+
return { matches: linkTarget.equals(expected) };
|
|
140
|
+
}
|
|
141
|
+
if (!stat.isFile()) {
|
|
142
|
+
return { matches: false, reason: "worktree path is not a regular file" };
|
|
143
|
+
}
|
|
144
|
+
const executableMatches = entry.mode === "100755" ? isExecutable(stat) : !isExecutable(stat);
|
|
145
|
+
if (!executableMatches) {
|
|
146
|
+
return { matches: false, reason: "worktree executable bit differs from template" };
|
|
147
|
+
}
|
|
148
|
+
return { matches: readFileSync(worktreePath).equals(expected) };
|
|
149
|
+
}
|
|
150
|
+
function removeExistingPath(worktreePath) {
|
|
151
|
+
if (!pathExists(worktreePath)) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
rmSync(worktreePath, { force: true, recursive: false });
|
|
155
|
+
}
|
|
156
|
+
function applyEntry(repoRoot, path, entry) {
|
|
157
|
+
const worktreePath = safeWorktreePath(repoRoot, path);
|
|
158
|
+
if (entry === null) {
|
|
159
|
+
removeExistingPath(worktreePath);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
mkdirSync(dirname(worktreePath), { recursive: true });
|
|
163
|
+
if (entry.mode === "120000") {
|
|
164
|
+
removeExistingPath(worktreePath);
|
|
165
|
+
symlinkSync(readGitBlob(entry).toString("utf8"), worktreePath);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const stat = pathExists(worktreePath) ? lstatSync(worktreePath) : null;
|
|
169
|
+
if (stat && !stat.isFile()) {
|
|
170
|
+
removeExistingPath(worktreePath);
|
|
171
|
+
}
|
|
172
|
+
writeFileSync(worktreePath, readGitBlob(entry));
|
|
173
|
+
chmodSync(worktreePath, entry.mode === "100755" ? 0o755 : 0o644);
|
|
174
|
+
}
|
|
175
|
+
function actionFor(baseEntry, targetEntry) {
|
|
176
|
+
if (baseEntry === null && targetEntry !== null) {
|
|
177
|
+
return "add";
|
|
178
|
+
}
|
|
179
|
+
if (baseEntry !== null && targetEntry === null) {
|
|
180
|
+
return "delete";
|
|
181
|
+
}
|
|
182
|
+
return "update";
|
|
183
|
+
}
|
|
184
|
+
function printEntries(title, entries) {
|
|
185
|
+
if (entries.length === 0) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
console.log(`${title}:`);
|
|
189
|
+
for (const entry of entries) {
|
|
190
|
+
const detail = entry.reason ? ` (${entry.reason})` : "";
|
|
191
|
+
console.log(` ${entry.action}: ${entry.path}${detail}`);
|
|
192
|
+
}
|
|
193
|
+
console.log("");
|
|
194
|
+
}
|
|
195
|
+
export function fastForwardTemplateFiles(args) {
|
|
196
|
+
const repoRoot = getRepoRoot();
|
|
197
|
+
const base = args.base ?? readPackageCoreframeVersion(repoRoot);
|
|
198
|
+
const target = args.target;
|
|
199
|
+
const changes = listChangedPaths(base, target);
|
|
200
|
+
const clean = [];
|
|
201
|
+
const ignored = [];
|
|
202
|
+
const skipped = [];
|
|
203
|
+
const unchanged = [];
|
|
204
|
+
for (const change of changes) {
|
|
205
|
+
if (isProjectOwnedPath(change.path)) {
|
|
206
|
+
ignored.push({
|
|
207
|
+
action: "ignored",
|
|
208
|
+
path: change.path,
|
|
209
|
+
reason: "project-owned migration history",
|
|
210
|
+
});
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const baseEntry = getGitEntry(base, change.path);
|
|
214
|
+
const targetEntry = getGitEntry(target, change.path);
|
|
215
|
+
const action = actionFor(baseEntry, targetEntry);
|
|
216
|
+
if (!entryIsSupported(baseEntry) || !entryIsSupported(targetEntry)) {
|
|
217
|
+
skipped.push({
|
|
218
|
+
action,
|
|
219
|
+
path: change.path,
|
|
220
|
+
reason: "unsupported template entry type",
|
|
221
|
+
});
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const targetMatch = currentMatchesEntry(repoRoot, change.path, targetEntry);
|
|
225
|
+
if (targetMatch.matches) {
|
|
226
|
+
unchanged.push({ action: "unchanged", path: change.path });
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const baseMatch = currentMatchesEntry(repoRoot, change.path, baseEntry);
|
|
230
|
+
if (!baseMatch.matches) {
|
|
231
|
+
skipped.push({
|
|
232
|
+
action,
|
|
233
|
+
path: change.path,
|
|
234
|
+
reason: baseMatch.reason ?? "downstream differs from base template",
|
|
235
|
+
});
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
clean.push({ action, path: change.path, targetEntry });
|
|
239
|
+
}
|
|
240
|
+
if (args.apply) {
|
|
241
|
+
for (const entry of clean) {
|
|
242
|
+
applyEntry(repoRoot, entry.path, entry.targetEntry ?? null);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
console.log(`Coreframe template fast-forward: ${base} -> ${target}`);
|
|
246
|
+
console.log("");
|
|
247
|
+
printEntries(args.apply ? "Applied clean template updates" : "Would apply clean template updates", clean);
|
|
248
|
+
printEntries("Ignored project-owned paths", ignored);
|
|
249
|
+
printEntries("Skipped for agent review", skipped);
|
|
250
|
+
if (unchanged.length > 0) {
|
|
251
|
+
console.log(`${unchanged.length} changed template path(s) already matched the target state.`);
|
|
252
|
+
}
|
|
253
|
+
if (changes.length === 0) {
|
|
254
|
+
console.log("No template file changes found in the requested range.");
|
|
255
|
+
}
|
|
256
|
+
if (!args.apply && clean.length > 0) {
|
|
257
|
+
console.log("");
|
|
258
|
+
console.log(`Run with --apply to write the ${clean.length} clean update(s).`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (import.meta.main) {
|
|
262
|
+
const targetIndex = process.argv.indexOf("--target");
|
|
263
|
+
const target = targetIndex === -1 ? null : process.argv[targetIndex + 1];
|
|
264
|
+
if (!target) {
|
|
265
|
+
console.error("Usage: coreframe upgrade fast-forward-template-files --target <git-ref> [--base <git-ref>] [--apply]");
|
|
266
|
+
process.exit(64);
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
const baseIndex = process.argv.indexOf("--base");
|
|
270
|
+
fastForwardTemplateFiles({
|
|
271
|
+
apply: process.argv.includes("--apply"),
|
|
272
|
+
base: baseIndex === -1 ? undefined : process.argv[baseIndex + 1],
|
|
273
|
+
target,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
278
|
+
process.exit(65);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import fs from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
|
|
4
|
+
import type sharp from "sharp"
|
|
5
|
+
import { glob } from "tinyglobby"
|
|
6
|
+
import type { Plugin } from "vite"
|
|
7
|
+
|
|
8
|
+
type SharpFactory = typeof sharp
|
|
9
|
+
|
|
10
|
+
export function imageGenerator(): Plugin {
|
|
11
|
+
return {
|
|
12
|
+
name: "image-generator",
|
|
13
|
+
async buildStart() {
|
|
14
|
+
const files = await glob(["src/**/*@3x.{png,webp}", "public/**/*@3x.{png,webp}"])
|
|
15
|
+
|
|
16
|
+
console.log(`[image-generator] Found ${files.length} @3x images.`)
|
|
17
|
+
|
|
18
|
+
await Promise.all(files.map((file) => generateDensityImages(file)))
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function generateDensityImages(file: string) {
|
|
24
|
+
const dir = path.dirname(file)
|
|
25
|
+
const ext = path.extname(file)
|
|
26
|
+
const base = path.basename(file, `@3x${ext}`)
|
|
27
|
+
const outputImages = [
|
|
28
|
+
{
|
|
29
|
+
outputPath: path.join(dir, `${base}@2x${ext}`),
|
|
30
|
+
scale: 2 / 3,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
outputPath: path.join(dir, `${base}@1x${ext}`),
|
|
34
|
+
scale: 1 / 3,
|
|
35
|
+
},
|
|
36
|
+
].filter((image) => !fs.existsSync(image.outputPath))
|
|
37
|
+
|
|
38
|
+
if (outputImages.length === 0) {
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const { default: sharp } = await import("sharp")
|
|
43
|
+
let width: number | undefined
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
width = (await sharp(file).metadata()).width
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error(`[image-generator] Failed to read metadata for ${file}`, error)
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!width) {
|
|
53
|
+
console.error(`[image-generator] Skipping ${file}; image width could not be read.`)
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
await Promise.all(
|
|
58
|
+
outputImages.map((image) =>
|
|
59
|
+
generateDensityImage({
|
|
60
|
+
file,
|
|
61
|
+
outputPath: image.outputPath,
|
|
62
|
+
sharp,
|
|
63
|
+
width: Math.round(width * image.scale),
|
|
64
|
+
}),
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function generateDensityImage(options: {
|
|
70
|
+
file: string
|
|
71
|
+
outputPath: string
|
|
72
|
+
sharp: SharpFactory
|
|
73
|
+
width: number
|
|
74
|
+
}) {
|
|
75
|
+
try {
|
|
76
|
+
console.log(`[image-generator] Generating ${options.outputPath} (${options.width}w)`)
|
|
77
|
+
await options.sharp(options.file).resize({ width: options.width }).toFile(options.outputPath)
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error(
|
|
80
|
+
`[image-generator] Failed to process ${options.file} for ${options.outputPath}`,
|
|
81
|
+
error,
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coreframe/scripts",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"bin": {
|
|
5
|
+
"coreframe": "./bin/coreframe.js"
|
|
6
|
+
},
|
|
7
|
+
"type": "module",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/airlock-labs/coreframe.git",
|
|
11
|
+
"directory": "scripts"
|
|
12
|
+
},
|
|
13
|
+
"exports": {
|
|
14
|
+
"./*": {
|
|
15
|
+
"types": "./dist/*.d.ts",
|
|
16
|
+
"default": "./dist/*.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
6
19
|
"scripts": {
|
|
7
|
-
"
|
|
20
|
+
"build": "tsc -p tsconfig.build.json",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
23
|
+
"format": "oxfmt",
|
|
24
|
+
"lint": "oxlint"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@clack/prompts": "^1.6.0",
|
|
28
|
+
"cmd-ts": "0.15.0",
|
|
29
|
+
"drizzle-orm": "1.0.0-rc.4",
|
|
30
|
+
"nano-spawn": "^2.1.0",
|
|
31
|
+
"procband": "^0.3.5",
|
|
32
|
+
"tinyglobby": "^0.2.17"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^26.1.0",
|
|
36
|
+
"sharp": "^0.34.5",
|
|
37
|
+
"typescript": "6.0.3",
|
|
38
|
+
"vite": "^8.1.0",
|
|
39
|
+
"vitest": "^4.1.9"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"sharp": "^0.34.5",
|
|
43
|
+
"vite": "^8.0.0"
|
|
8
44
|
},
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
45
|
+
"peerDependenciesMeta": {
|
|
46
|
+
"sharp": {
|
|
47
|
+
"optional": true
|
|
48
|
+
},
|
|
49
|
+
"vite": {
|
|
50
|
+
"optional": true
|
|
51
|
+
}
|
|
52
|
+
}
|
|
13
53
|
}
|
package/project.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"allowImportingTsExtensions": false,
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"emitDeclarationOnly": false,
|
|
7
|
+
"noEmit": false,
|
|
8
|
+
"outDir": "dist",
|
|
9
|
+
"rewriteRelativeImportExtensions": true
|
|
10
|
+
},
|
|
11
|
+
"exclude": ["**/*.test.ts", "**/*.test.tsx", "dist"]
|
|
12
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"allowImportingTsExtensions": true,
|
|
4
|
+
"allowSyntheticDefaultImports": true,
|
|
5
|
+
"esModuleInterop": true,
|
|
6
|
+
"forceConsistentCasingInFileNames": true,
|
|
7
|
+
"isolatedModules": true,
|
|
8
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
9
|
+
"module": "ESNext",
|
|
10
|
+
"moduleResolution": "Bundler",
|
|
11
|
+
"noEmit": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"rootDir": ".",
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"strict": true,
|
|
16
|
+
"target": "ES2022",
|
|
17
|
+
"types": ["node"]
|
|
18
|
+
},
|
|
19
|
+
"include": ["**/*.ts"]
|
|
20
|
+
}
|
package/typecheck.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { access } from "node:fs/promises"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
import process from "node:process"
|
|
5
|
+
|
|
6
|
+
import { supervise } from "procband"
|
|
7
|
+
|
|
8
|
+
const nodeModulesBinPath = path.resolve("node_modules", ".bin")
|
|
9
|
+
process.env.PATH = nodeModulesBinPath + path.delimiter + process.env.PATH
|
|
10
|
+
|
|
11
|
+
const tscCommand = process.platform === "win32" ? "tsc.cmd" : "tsc"
|
|
12
|
+
const projects = [
|
|
13
|
+
{ name: "scripts", path: "scripts/tsconfig.json" },
|
|
14
|
+
{ name: "shared", path: "src/shared/tsconfig.json" },
|
|
15
|
+
{ name: "api", path: "src/api/tsconfig.json" },
|
|
16
|
+
{ name: "worker", path: "src/api/worker/tsconfig.json" },
|
|
17
|
+
{ name: "web", path: "src/web/tsconfig.json" },
|
|
18
|
+
] as const
|
|
19
|
+
|
|
20
|
+
export async function typecheckProject() {
|
|
21
|
+
const existingProjects = []
|
|
22
|
+
|
|
23
|
+
for (const project of projects) {
|
|
24
|
+
try {
|
|
25
|
+
await access(project.path)
|
|
26
|
+
existingProjects.push(project)
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) {
|
|
29
|
+
throw error
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const checks = existingProjects.map((project) =>
|
|
35
|
+
supervise({
|
|
36
|
+
name: project.name,
|
|
37
|
+
command: tscCommand,
|
|
38
|
+
args: ["-p", project.path],
|
|
39
|
+
}),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const results = await Promise.all(checks.map((check) => check.wait()))
|
|
43
|
+
const failed = results.filter((result) => result.exitCode !== 0)
|
|
44
|
+
|
|
45
|
+
if (failed.length > 0) {
|
|
46
|
+
process.exitCode = failed[0].exitCode
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (import.meta.main) {
|
|
51
|
+
typecheckProject().catch((error) => {
|
|
52
|
+
console.error(error)
|
|
53
|
+
process.exit(1)
|
|
54
|
+
})
|
|
55
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process"
|
|
3
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"
|
|
4
|
+
import { tmpdir } from "node:os"
|
|
5
|
+
import { join } from "node:path"
|
|
6
|
+
|
|
7
|
+
const skillPath = ".agents/skills/coreframe-upgrade/SKILL.md"
|
|
8
|
+
|
|
9
|
+
export type CheckUpgradeSkillOptions = {
|
|
10
|
+
apply: boolean
|
|
11
|
+
target: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function git(args: string[], options: { encoding?: BufferEncoding } = {}) {
|
|
15
|
+
return spawnSync("git", args, options)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function errorDetail(result: ReturnType<typeof spawnSync>) {
|
|
19
|
+
return result.stderr.toString("utf8").trim() || result.stdout.toString("utf8").trim()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function readGitFile(ref: string, path: string) {
|
|
23
|
+
const result = git(["show", `${ref}:${path}`], { encoding: "utf8" })
|
|
24
|
+
|
|
25
|
+
if (result.status !== 0) {
|
|
26
|
+
const detail = errorDetail(result)
|
|
27
|
+
throw new Error(`Could not read ${path} at ${ref}.${detail ? ` ${detail}` : ""}`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return result.stdout.toString()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getRepoRoot() {
|
|
34
|
+
const result = git(["rev-parse", "--show-toplevel"], { encoding: "utf8" })
|
|
35
|
+
|
|
36
|
+
if (result.status !== 0) {
|
|
37
|
+
const detail = errorDetail(result)
|
|
38
|
+
throw new Error(`Could not find git repository root.${detail ? ` ${detail}` : ""}`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return result.stdout.toString().trim()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function printDiff(localPath: string, targetText: string) {
|
|
45
|
+
const dir = mkdtempSync(join(tmpdir(), "coreframe-upgrade-skill-"))
|
|
46
|
+
const targetPath = join(dir, "SKILL.md")
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
writeFileSync(targetPath, targetText)
|
|
50
|
+
const diff = git([
|
|
51
|
+
"--no-pager",
|
|
52
|
+
"diff",
|
|
53
|
+
"--no-index",
|
|
54
|
+
"--color=never",
|
|
55
|
+
"--",
|
|
56
|
+
localPath,
|
|
57
|
+
targetPath,
|
|
58
|
+
])
|
|
59
|
+
const output = errorDetail(diff)
|
|
60
|
+
|
|
61
|
+
if (output) {
|
|
62
|
+
console.log(output)
|
|
63
|
+
}
|
|
64
|
+
} finally {
|
|
65
|
+
rmSync(dir, { force: true, recursive: true })
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function checkUpgradeSkillCurrent({ apply, target }: CheckUpgradeSkillOptions) {
|
|
70
|
+
const repoRoot = getRepoRoot()
|
|
71
|
+
const localPath = join(repoRoot, skillPath)
|
|
72
|
+
|
|
73
|
+
if (!existsSync(localPath)) {
|
|
74
|
+
throw new Error(`Local ${skillPath} does not exist.`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const localText = readFileSync(localPath, "utf8")
|
|
78
|
+
const targetText = readGitFile(target, skillPath)
|
|
79
|
+
|
|
80
|
+
if (localText === targetText) {
|
|
81
|
+
console.log(`Coreframe upgrade skill matches ${target}:${skillPath}.`)
|
|
82
|
+
return 0
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (apply) {
|
|
86
|
+
writeFileSync(localPath, targetText)
|
|
87
|
+
console.error(`Updated ${skillPath} from ${target}. Reread it before continuing the upgrade.`)
|
|
88
|
+
return 20
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.error(`Coreframe upgrade skill differs from ${target}:${skillPath}.`)
|
|
92
|
+
console.error(`Run again with --apply to update it, then reread ${skillPath} before continuing.`)
|
|
93
|
+
printDiff(localPath, targetText)
|
|
94
|
+
return 20
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (import.meta.main) {
|
|
98
|
+
const targetIndex = process.argv.indexOf("--target")
|
|
99
|
+
const target = targetIndex === -1 ? null : process.argv[targetIndex + 1]
|
|
100
|
+
|
|
101
|
+
if (!target) {
|
|
102
|
+
console.error("Usage: coreframe upgrade check-skill-current --target <git-ref> [--apply]")
|
|
103
|
+
process.exit(64)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
process.exit(checkUpgradeSkillCurrent({ apply: process.argv.includes("--apply"), target }))
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error(error instanceof Error ? error.message : String(error))
|
|
110
|
+
process.exit(65)
|
|
111
|
+
}
|
|
112
|
+
}
|