@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.
Files changed (96) hide show
  1. package/bin/coreframe.js +35 -0
  2. package/config.test.ts +15 -0
  3. package/config.ts +9 -0
  4. package/coreframe.ts +272 -0
  5. package/db/client.ts +72 -0
  6. package/db/config.ts +41 -0
  7. package/db/drizzle-kit.test.ts +27 -0
  8. package/db/drizzle-kit.ts +39 -0
  9. package/db/ensure.ts +67 -0
  10. package/db/migrate-held.test.ts +81 -0
  11. package/db/migrate-held.ts +166 -0
  12. package/db/migrate.ts +39 -0
  13. package/db/project.ts +28 -0
  14. package/db/reset.ts +24 -0
  15. package/db/seed.ts +38 -0
  16. package/db/setup.ts +18 -0
  17. package/deploy/check-migrations.ts +31 -0
  18. package/deploy/checks.ts +38 -0
  19. package/deploy/config.test.ts +24 -0
  20. package/deploy/config.ts +68 -0
  21. package/deploy/migrations.test.ts +110 -0
  22. package/deploy/migrations.ts +317 -0
  23. package/deploy/pending-migrations.test.ts +93 -0
  24. package/deploy/pending-migrations.ts +45 -0
  25. package/deploy/tauri-release.test.ts +196 -0
  26. package/deploy/tauri-release.ts +816 -0
  27. package/deploy/version.test.ts +29 -0
  28. package/deploy/version.ts +65 -0
  29. package/deploy.test.ts +101 -0
  30. package/deploy.ts +346 -0
  31. package/dev.test.ts +153 -0
  32. package/dev.ts +155 -0
  33. package/dist/config.d.ts +5 -0
  34. package/dist/config.js +3 -0
  35. package/dist/coreframe.d.ts +280 -0
  36. package/dist/coreframe.js +240 -0
  37. package/dist/db/client.d.ts +22 -0
  38. package/dist/db/client.js +29 -0
  39. package/dist/db/config.d.ts +21 -0
  40. package/dist/db/config.js +32 -0
  41. package/dist/db/drizzle-kit.d.ts +8 -0
  42. package/dist/db/drizzle-kit.js +22 -0
  43. package/dist/db/ensure.d.ts +2 -0
  44. package/dist/db/ensure.js +40 -0
  45. package/dist/db/migrate-held.d.ts +12 -0
  46. package/dist/db/migrate-held.js +119 -0
  47. package/dist/db/migrate.d.ts +2 -0
  48. package/dist/db/migrate.js +25 -0
  49. package/dist/db/project.d.ts +8 -0
  50. package/dist/db/project.js +9 -0
  51. package/dist/db/reset.d.ts +2 -0
  52. package/dist/db/reset.js +19 -0
  53. package/dist/db/seed.d.ts +3 -0
  54. package/dist/db/seed.js +23 -0
  55. package/dist/db/setup.d.ts +3 -0
  56. package/dist/db/setup.js +15 -0
  57. package/dist/deploy/check-migrations.d.ts +2 -0
  58. package/dist/deploy/check-migrations.js +18 -0
  59. package/dist/deploy/checks.d.ts +2 -0
  60. package/dist/deploy/checks.js +27 -0
  61. package/dist/deploy/config.d.ts +50 -0
  62. package/dist/deploy/config.js +9 -0
  63. package/dist/deploy/migrations.d.ts +21 -0
  64. package/dist/deploy/migrations.js +203 -0
  65. package/dist/deploy/pending-migrations.d.ts +5 -0
  66. package/dist/deploy/pending-migrations.js +30 -0
  67. package/dist/deploy/tauri-release.d.ts +85 -0
  68. package/dist/deploy/tauri-release.js +512 -0
  69. package/dist/deploy/version.d.ts +18 -0
  70. package/dist/deploy/version.js +41 -0
  71. package/dist/deploy.d.ts +17 -0
  72. package/dist/deploy.js +247 -0
  73. package/dist/dev.d.ts +13 -0
  74. package/dist/dev.js +96 -0
  75. package/dist/image-generator.d.ts +2 -0
  76. package/dist/image-generator.js +59 -0
  77. package/dist/project.d.ts +1 -0
  78. package/dist/project.js +13 -0
  79. package/dist/typecheck.d.ts +2 -0
  80. package/dist/typecheck.js +45 -0
  81. package/dist/upgrade/check-skill-current.d.ts +6 -0
  82. package/dist/upgrade/check-skill-current.js +88 -0
  83. package/dist/upgrade/ensure-template-remote.d.ts +6 -0
  84. package/dist/upgrade/ensure-template-remote.js +57 -0
  85. package/dist/upgrade/fast-forward-template-files.d.ts +7 -0
  86. package/dist/upgrade/fast-forward-template-files.js +280 -0
  87. package/image-generator.ts +84 -0
  88. package/package.json +48 -8
  89. package/project.ts +6 -0
  90. package/tsconfig.build.json +12 -0
  91. package/tsconfig.json +20 -0
  92. package/typecheck.ts +55 -0
  93. package/upgrade/check-skill-current.ts +112 -0
  94. package/upgrade/ensure-template-remote.ts +79 -0
  95. package/upgrade/fast-forward-template-files.ts +408 -0
  96. 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.0.0",
4
- "description": "",
5
- "main": "index.js",
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
- "test": "echo \"Error: no test specified\" && exit 1"
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
- "keywords": [],
10
- "author": "",
11
- "license": "All rights reserved",
12
- "type": "commonjs"
45
+ "peerDependenciesMeta": {
46
+ "sharp": {
47
+ "optional": true
48
+ },
49
+ "vite": {
50
+ "optional": true
51
+ }
52
+ }
13
53
  }
package/project.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { resolve } from "node:path"
2
+ import { pathToFileURL } from "node:url"
3
+
4
+ export async function importProjectModule<T>(path: string): Promise<T> {
5
+ return (await import(pathToFileURL(resolve(process.cwd(), path)).href)) as T
6
+ }
@@ -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
+ }