@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,79 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process"
3
+
4
+ const defaultRemote = "template"
5
+ const defaultUrl = "https://github.com/airlock-labs/coreframe.git"
6
+
7
+ export type EnsureTemplateRemoteOptions = {
8
+ fetch: boolean
9
+ remote: string
10
+ }
11
+
12
+ function git(args: string[]) {
13
+ return spawnSync("git", args, { encoding: "utf8" })
14
+ }
15
+
16
+ function runGit(args: string[]) {
17
+ const result = git(args)
18
+
19
+ if (result.status !== 0) {
20
+ const detail = result.stderr.trim() || result.stdout.trim()
21
+ throw new Error(`Git command failed: git ${args.join(" ")}${detail ? `\n${detail}` : ""}`)
22
+ }
23
+
24
+ return result.stdout
25
+ }
26
+
27
+ function getRemoteUrl(remote: string) {
28
+ const result = git(["remote", "get-url", remote])
29
+
30
+ if (result.status === 2) {
31
+ return null
32
+ }
33
+
34
+ if (result.status !== 0) {
35
+ const detail = result.stderr.trim() || result.stdout.trim()
36
+ throw new Error(`Could not inspect remote ${remote}.${detail ? `\n${detail}` : ""}`)
37
+ }
38
+
39
+ return result.stdout.trim()
40
+ }
41
+
42
+ export function ensureTemplateRemote({
43
+ fetch = true,
44
+ remote = defaultRemote,
45
+ }: Partial<EnsureTemplateRemoteOptions> = {}) {
46
+ if (remote.length === 0) {
47
+ throw new Error("Remote name must not be empty.")
48
+ }
49
+
50
+ runGit(["rev-parse", "--show-toplevel"])
51
+
52
+ const existingUrl = getRemoteUrl(remote)
53
+
54
+ if (existingUrl === null) {
55
+ runGit(["remote", "add", remote, defaultUrl])
56
+ console.log(`Added ${remote} remote: ${defaultUrl}`)
57
+ } else if (existingUrl === defaultUrl) {
58
+ console.log(`${remote} remote already points at ${defaultUrl}`)
59
+ } else {
60
+ throw new Error(
61
+ `${remote} remote points at ${existingUrl}, not ${defaultUrl}.\n` +
62
+ "Choose the intended template source before continuing, then update or rename the remote manually.",
63
+ )
64
+ }
65
+
66
+ if (fetch) {
67
+ runGit(["fetch", remote, "--tags", "--prune"])
68
+ console.log(`Fetched ${remote}.`)
69
+ }
70
+ }
71
+
72
+ if (import.meta.main) {
73
+ try {
74
+ ensureTemplateRemote({ fetch: !process.argv.includes("--no-fetch") })
75
+ } catch (error) {
76
+ console.error(error instanceof Error ? error.message : String(error))
77
+ process.exit(65)
78
+ }
79
+ }
@@ -0,0 +1,408 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process"
3
+ import {
4
+ chmodSync,
5
+ existsSync,
6
+ lstatSync,
7
+ mkdirSync,
8
+ readFileSync,
9
+ readlinkSync,
10
+ rmSync,
11
+ symlinkSync,
12
+ writeFileSync,
13
+ } from "node:fs"
14
+ import type { Stats } from "node:fs"
15
+ import { dirname, join, resolve, sep } from "node:path"
16
+
17
+ const projectOwnedPathPrefixes = ["drizzle/", "drizzle-hold/"]
18
+ const supportedModes = new Set(["100644", "100755", "120000"])
19
+
20
+ type GitEntry = {
21
+ mode: string
22
+ object: string
23
+ path: string
24
+ type: string
25
+ }
26
+
27
+ type ChangedPath = {
28
+ path: string
29
+ status: string
30
+ }
31
+
32
+ type PendingEntry = {
33
+ action: string
34
+ path: string
35
+ reason?: string
36
+ targetEntry?: GitEntry | null
37
+ }
38
+
39
+ export type FastForwardTemplateFilesOptions = {
40
+ apply: boolean
41
+ base?: string
42
+ target: string
43
+ }
44
+
45
+ function git(args: string[], options = {}) {
46
+ return spawnSync("git", args, options)
47
+ }
48
+
49
+ function gitText(args: string[]) {
50
+ const result = git(args, { encoding: "utf8" })
51
+
52
+ if (result.status !== 0) {
53
+ const detail = result.stderr.toString().trim() || result.stdout.toString().trim()
54
+ throw new Error(`Git command failed: git ${args.join(" ")}${detail ? `\n${detail}` : ""}`)
55
+ }
56
+
57
+ return result.stdout.toString()
58
+ }
59
+
60
+ function gitBuffer(args: string[]) {
61
+ const result = git(args)
62
+
63
+ if (result.status !== 0) {
64
+ const detail = result.stderr.toString("utf8").trim() || result.stdout.toString("utf8").trim()
65
+ throw new Error(`Git command failed: git ${args.join(" ")}${detail ? `\n${detail}` : ""}`)
66
+ }
67
+
68
+ return result.stdout
69
+ }
70
+
71
+ function getRepoRoot() {
72
+ return gitText(["rev-parse", "--show-toplevel"]).trim()
73
+ }
74
+
75
+ function readPackageCoreframeVersion(repoRoot: string) {
76
+ const packagePath = join(repoRoot, "package.json")
77
+
78
+ if (!existsSync(packagePath)) {
79
+ throw new Error("Missing --base <git-ref>, and package.json does not exist.")
80
+ }
81
+
82
+ const packageJson = JSON.parse(readFileSync(packagePath, "utf8")) as {
83
+ engines?: { coreframe?: unknown }
84
+ }
85
+ const version = packageJson.engines?.coreframe
86
+
87
+ if (typeof version !== "string" || version.length === 0) {
88
+ throw new Error("Missing --base <git-ref>, and package.json engines.coreframe is not set.")
89
+ }
90
+
91
+ return version
92
+ }
93
+
94
+ function splitNullTerminated(buffer: Buffer) {
95
+ const fields = []
96
+ let start = 0
97
+
98
+ for (let index = 0; index < buffer.length; index += 1) {
99
+ if (buffer[index] !== 0) {
100
+ continue
101
+ }
102
+
103
+ fields.push(buffer.subarray(start, index).toString("utf8"))
104
+ start = index + 1
105
+ }
106
+
107
+ return fields.filter((field) => field.length > 0)
108
+ }
109
+
110
+ function listChangedPaths(base: string, target: string) {
111
+ const output = gitBuffer(["diff", "--name-status", "-z", "--no-renames", base, target, "--"])
112
+ const fields = splitNullTerminated(output)
113
+ const changes: ChangedPath[] = []
114
+
115
+ for (let index = 0; index < fields.length; index += 2) {
116
+ const status = fields[index]
117
+ const path = fields[index + 1]
118
+
119
+ if (!status || !path) {
120
+ throw new Error("Could not parse git diff --name-status output.")
121
+ }
122
+
123
+ changes.push({ path, status })
124
+ }
125
+
126
+ return changes
127
+ }
128
+
129
+ function getGitEntry(ref: string, path: string): GitEntry | null {
130
+ const output = gitBuffer(["ls-tree", "-z", ref, "--", path])
131
+
132
+ if (output.length === 0) {
133
+ return null
134
+ }
135
+
136
+ const record = splitNullTerminated(output)[0]
137
+ const tabIndex = record.indexOf("\t")
138
+
139
+ if (tabIndex === -1) {
140
+ throw new Error(`Could not parse git ls-tree output for ${ref}:${path}.`)
141
+ }
142
+
143
+ const [mode, type, object] = record.slice(0, tabIndex).split(" ")
144
+
145
+ if (!mode || !type || !object) {
146
+ throw new Error(`Could not parse git ls-tree metadata for ${ref}:${path}.`)
147
+ }
148
+
149
+ return { mode, object, path, type }
150
+ }
151
+
152
+ function readGitBlob(entry: GitEntry) {
153
+ return gitBuffer(["cat-file", "blob", entry.object])
154
+ }
155
+
156
+ function safeWorktreePath(repoRoot: string, path: string) {
157
+ const absolutePath = resolve(repoRoot, path)
158
+ const rootPrefix = repoRoot.endsWith(sep) ? repoRoot : `${repoRoot}${sep}`
159
+
160
+ if (absolutePath !== repoRoot && !absolutePath.startsWith(rootPrefix)) {
161
+ throw new Error(`Refusing to access path outside repository: ${path}`)
162
+ }
163
+
164
+ return absolutePath
165
+ }
166
+
167
+ function pathExists(path: string) {
168
+ try {
169
+ lstatSync(path)
170
+ return true
171
+ } catch (error) {
172
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
173
+ return false
174
+ }
175
+
176
+ throw error
177
+ }
178
+ }
179
+
180
+ function isExecutable(stat: Stats) {
181
+ return (stat.mode & 0o111) !== 0
182
+ }
183
+
184
+ function entryIsSupported(entry: GitEntry | null) {
185
+ return entry === null || (entry.type === "blob" && supportedModes.has(entry.mode))
186
+ }
187
+
188
+ function isProjectOwnedPath(path: string) {
189
+ return projectOwnedPathPrefixes.some(
190
+ (prefix) => path === prefix.slice(0, -1) || path.startsWith(prefix),
191
+ )
192
+ }
193
+
194
+ function currentMatchesEntry(repoRoot: string, path: string, entry: GitEntry | null) {
195
+ const worktreePath = safeWorktreePath(repoRoot, path)
196
+
197
+ if (entry === null) {
198
+ return { matches: !pathExists(worktreePath) }
199
+ }
200
+
201
+ if (!entryIsSupported(entry)) {
202
+ return { matches: false, reason: `unsupported template entry mode ${entry.mode} ${entry.type}` }
203
+ }
204
+
205
+ let stat: Stats
206
+
207
+ try {
208
+ stat = lstatSync(worktreePath)
209
+ } catch (error) {
210
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
211
+ return { matches: false, reason: "path is missing from worktree" }
212
+ }
213
+
214
+ throw error
215
+ }
216
+
217
+ const expected = readGitBlob(entry)
218
+
219
+ if (entry.mode === "120000") {
220
+ if (!stat.isSymbolicLink()) {
221
+ return { matches: false, reason: "worktree path is not a symlink" }
222
+ }
223
+
224
+ const linkTarget = Buffer.from(readlinkSync(worktreePath), "utf8")
225
+ return { matches: linkTarget.equals(expected) }
226
+ }
227
+
228
+ if (!stat.isFile()) {
229
+ return { matches: false, reason: "worktree path is not a regular file" }
230
+ }
231
+
232
+ const executableMatches = entry.mode === "100755" ? isExecutable(stat) : !isExecutable(stat)
233
+
234
+ if (!executableMatches) {
235
+ return { matches: false, reason: "worktree executable bit differs from template" }
236
+ }
237
+
238
+ return { matches: readFileSync(worktreePath).equals(expected) }
239
+ }
240
+
241
+ function removeExistingPath(worktreePath: string) {
242
+ if (!pathExists(worktreePath)) {
243
+ return
244
+ }
245
+
246
+ rmSync(worktreePath, { force: true, recursive: false })
247
+ }
248
+
249
+ function applyEntry(repoRoot: string, path: string, entry: GitEntry | null) {
250
+ const worktreePath = safeWorktreePath(repoRoot, path)
251
+
252
+ if (entry === null) {
253
+ removeExistingPath(worktreePath)
254
+ return
255
+ }
256
+
257
+ mkdirSync(dirname(worktreePath), { recursive: true })
258
+
259
+ if (entry.mode === "120000") {
260
+ removeExistingPath(worktreePath)
261
+ symlinkSync(readGitBlob(entry).toString("utf8"), worktreePath)
262
+ return
263
+ }
264
+
265
+ const stat = pathExists(worktreePath) ? lstatSync(worktreePath) : null
266
+
267
+ if (stat && !stat.isFile()) {
268
+ removeExistingPath(worktreePath)
269
+ }
270
+
271
+ writeFileSync(worktreePath, readGitBlob(entry))
272
+ chmodSync(worktreePath, entry.mode === "100755" ? 0o755 : 0o644)
273
+ }
274
+
275
+ function actionFor(baseEntry: GitEntry | null, targetEntry: GitEntry | null) {
276
+ if (baseEntry === null && targetEntry !== null) {
277
+ return "add"
278
+ }
279
+
280
+ if (baseEntry !== null && targetEntry === null) {
281
+ return "delete"
282
+ }
283
+
284
+ return "update"
285
+ }
286
+
287
+ function printEntries(title: string, entries: PendingEntry[]) {
288
+ if (entries.length === 0) {
289
+ return
290
+ }
291
+
292
+ console.log(`${title}:`)
293
+
294
+ for (const entry of entries) {
295
+ const detail = entry.reason ? ` (${entry.reason})` : ""
296
+ console.log(` ${entry.action}: ${entry.path}${detail}`)
297
+ }
298
+
299
+ console.log("")
300
+ }
301
+
302
+ export function fastForwardTemplateFiles(args: FastForwardTemplateFilesOptions) {
303
+ const repoRoot = getRepoRoot()
304
+ const base = args.base ?? readPackageCoreframeVersion(repoRoot)
305
+ const target = args.target
306
+ const changes = listChangedPaths(base, target)
307
+ const clean: PendingEntry[] = []
308
+ const ignored: PendingEntry[] = []
309
+ const skipped: PendingEntry[] = []
310
+ const unchanged: PendingEntry[] = []
311
+
312
+ for (const change of changes) {
313
+ if (isProjectOwnedPath(change.path)) {
314
+ ignored.push({
315
+ action: "ignored",
316
+ path: change.path,
317
+ reason: "project-owned migration history",
318
+ })
319
+ continue
320
+ }
321
+
322
+ const baseEntry = getGitEntry(base, change.path)
323
+ const targetEntry = getGitEntry(target, change.path)
324
+ const action = actionFor(baseEntry, targetEntry)
325
+
326
+ if (!entryIsSupported(baseEntry) || !entryIsSupported(targetEntry)) {
327
+ skipped.push({
328
+ action,
329
+ path: change.path,
330
+ reason: "unsupported template entry type",
331
+ })
332
+ continue
333
+ }
334
+
335
+ const targetMatch = currentMatchesEntry(repoRoot, change.path, targetEntry)
336
+
337
+ if (targetMatch.matches) {
338
+ unchanged.push({ action: "unchanged", path: change.path })
339
+ continue
340
+ }
341
+
342
+ const baseMatch = currentMatchesEntry(repoRoot, change.path, baseEntry)
343
+
344
+ if (!baseMatch.matches) {
345
+ skipped.push({
346
+ action,
347
+ path: change.path,
348
+ reason: baseMatch.reason ?? "downstream differs from base template",
349
+ })
350
+ continue
351
+ }
352
+
353
+ clean.push({ action, path: change.path, targetEntry })
354
+ }
355
+
356
+ if (args.apply) {
357
+ for (const entry of clean) {
358
+ applyEntry(repoRoot, entry.path, entry.targetEntry ?? null)
359
+ }
360
+ }
361
+
362
+ console.log(`Coreframe template fast-forward: ${base} -> ${target}`)
363
+ console.log("")
364
+
365
+ printEntries(
366
+ args.apply ? "Applied clean template updates" : "Would apply clean template updates",
367
+ clean,
368
+ )
369
+ printEntries("Ignored project-owned paths", ignored)
370
+ printEntries("Skipped for agent review", skipped)
371
+
372
+ if (unchanged.length > 0) {
373
+ console.log(`${unchanged.length} changed template path(s) already matched the target state.`)
374
+ }
375
+
376
+ if (changes.length === 0) {
377
+ console.log("No template file changes found in the requested range.")
378
+ }
379
+
380
+ if (!args.apply && clean.length > 0) {
381
+ console.log("")
382
+ console.log(`Run with --apply to write the ${clean.length} clean update(s).`)
383
+ }
384
+ }
385
+
386
+ if (import.meta.main) {
387
+ const targetIndex = process.argv.indexOf("--target")
388
+ const target = targetIndex === -1 ? null : process.argv[targetIndex + 1]
389
+
390
+ if (!target) {
391
+ console.error(
392
+ "Usage: coreframe upgrade fast-forward-template-files --target <git-ref> [--base <git-ref>] [--apply]",
393
+ )
394
+ process.exit(64)
395
+ }
396
+
397
+ try {
398
+ const baseIndex = process.argv.indexOf("--base")
399
+ fastForwardTemplateFiles({
400
+ apply: process.argv.includes("--apply"),
401
+ base: baseIndex === -1 ? undefined : process.argv[baseIndex + 1],
402
+ target,
403
+ })
404
+ } catch (error) {
405
+ console.error(error instanceof Error ? error.message : String(error))
406
+ process.exit(65)
407
+ }
408
+ }
package/readme.md DELETED
@@ -1 +0,0 @@
1
- Coming soon...