@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,203 @@
1
+ import { access, readFile, readdir } from "node:fs/promises";
2
+ import { basename, join, relative, resolve } from "node:path";
3
+ import { HELD_MIGRATIONS_FOLDER, MIGRATIONS_FOLDER } from "../db/config.js";
4
+ const staticBlockedPatterns = [
5
+ {
6
+ code: "drop-schema-object",
7
+ pattern: /\bdrop\s+(?:table|view|materialized\s+view|index|sequence|type|schema|function|trigger|policy)\b/giu,
8
+ reason: "drops schema that the currently deployed app may still depend on",
9
+ },
10
+ {
11
+ code: "drop-column",
12
+ pattern: /\balter\s+table\b[^;]*?\bdrop\s+column\b/giu,
13
+ reason: "removes a column that the currently deployed app may still read or write",
14
+ },
15
+ {
16
+ code: "drop-constraint",
17
+ pattern: /\balter\s+table\b[^;]*?\bdrop\s+constraint\b/giu,
18
+ reason: "removes a constraint outside the staged contract phase",
19
+ },
20
+ {
21
+ code: "truncate",
22
+ pattern: /\btruncate\b/giu,
23
+ reason: "removes data outside the deploy-safe migration path",
24
+ },
25
+ {
26
+ code: "rename-table-or-column",
27
+ pattern: /\balter\s+table\b[^;]*?\brename(?:\s+column)?\b/giu,
28
+ reason: "renames schema in a way old and new app versions cannot both address",
29
+ },
30
+ {
31
+ code: "rewrite-column-type",
32
+ pattern: /\balter\s+table\b[^;]*?\balter\s+column\b[^;]*?\btype\b/giu,
33
+ reason: "rewrites a column type outside a staged expand/contract rollout",
34
+ },
35
+ {
36
+ code: "set-not-null",
37
+ pattern: /\balter\s+table\b[^;]*?\balter\s+column\b[^;]*?\bset\s+not\s+null\b/giu,
38
+ reason: "enforces NOT NULL before a separate backfill and verification step",
39
+ },
40
+ {
41
+ code: "set-default",
42
+ pattern: /\balter\s+table\b[^;]*?\balter\s+column\b[^;]*?\bset\s+default\b/giu,
43
+ reason: "changes default write behavior while old app code may still be running",
44
+ },
45
+ {
46
+ code: "broad-update",
47
+ pattern: /\bupdate\b/giu,
48
+ reason: "changes data outside a separately reviewed backfill or held migration",
49
+ },
50
+ {
51
+ code: "broad-delete",
52
+ pattern: /\bdelete\s+from\b/giu,
53
+ reason: "removes data outside a held migration",
54
+ },
55
+ ];
56
+ export async function collectDeployMigrationFiles(cwd = process.cwd()) {
57
+ return collectDrizzleMigrationFiles(resolve(cwd, MIGRATIONS_FOLDER), cwd);
58
+ }
59
+ export async function collectHeldMigrationFiles(cwd = process.cwd()) {
60
+ return collectFlatSqlFiles(resolve(cwd, HELD_MIGRATIONS_FOLDER), cwd);
61
+ }
62
+ export async function scanDeployMigrationFiles(migrations) {
63
+ const results = await Promise.all(migrations.map(async (migration) => {
64
+ const sql = await readFile(migration.path, "utf8");
65
+ return {
66
+ migration,
67
+ risks: scanMigrationSql(sql),
68
+ };
69
+ }));
70
+ return results.filter((result) => result.risks.length > 0);
71
+ }
72
+ export function scanMigrationSql(sql) {
73
+ const maskedSql = maskSql(sql);
74
+ const createdTables = collectCreatedTables(maskedSql);
75
+ const risks = [];
76
+ for (const blocked of staticBlockedPatterns) {
77
+ for (const match of maskedSql.matchAll(blocked.pattern)) {
78
+ risks.push(riskForMatch(sql, match, blocked.code, blocked.reason));
79
+ }
80
+ }
81
+ for (const match of maskedSql.matchAll(/\balter\s+table\s+(?:if\s+exists\s+|only\s+)?(?<table>(?:"[^"]+"|\w+)(?:\s*\.\s*(?:"[^"]+"|\w+))?)\s+add\s+column\b[^;]*?\bnot\s+null\b/giu)) {
82
+ if (!createdTables.has(normalizeIdentifier(match.groups?.table))) {
83
+ risks.push(riskForMatch(sql, match, "add-not-null-column", "adds a required column to an existing table before a separate compatibility rollout"));
84
+ }
85
+ }
86
+ for (const match of maskedSql.matchAll(/\balter\s+table\s+(?:if\s+exists\s+|only\s+)?(?<table>(?:"[^"]+"|\w+)(?:\s*\.\s*(?:"[^"]+"|\w+))?)\s+add\s+constraint\b[^;]*?\b(?:foreign\s+key|unique|primary\s+key|check)\b/giu)) {
87
+ if (!createdTables.has(normalizeIdentifier(match.groups?.table))) {
88
+ risks.push(riskForMatch(sql, match, "add-constraint", "enforces a constraint on an existing table before a separate validation step"));
89
+ }
90
+ }
91
+ for (const match of maskedSql.matchAll(/\bcreate\s+unique\s+index(?:\s+concurrently)?(?:\s+if\s+not\s+exists)?\s+(?:"[^"]+"|\w+)\s+on\s+(?<table>(?:"[^"]+"|\w+)(?:\s*\.\s*(?:"[^"]+"|\w+))?)/giu)) {
92
+ if (!createdTables.has(normalizeIdentifier(match.groups?.table))) {
93
+ risks.push(riskForMatch(sql, match, "create-unique-index", "adds uniqueness to existing data before a separate dedupe and validation step"));
94
+ }
95
+ }
96
+ return risks.sort((left, right) => left.line - right.line || left.code.localeCompare(right.code));
97
+ }
98
+ export function formatMigrationScanFailures(results) {
99
+ return results
100
+ .flatMap((result) => result.risks.map((risk) => `${result.migration.relativePath}:${risk.line} ${risk.code}: ${risk.reason} (${risk.match.trim()})`))
101
+ .join("\n");
102
+ }
103
+ export function formatMigrationList(migrations) {
104
+ if (migrations.length === 0) {
105
+ return "No migration files found.";
106
+ }
107
+ return migrations.map((migration) => `- ${migration.relativePath}`).join("\n");
108
+ }
109
+ function collectCreatedTables(sql) {
110
+ const tables = new Set();
111
+ for (const match of sql.matchAll(/\bcreate\s+(?:temporary\s+|temp\s+)?table\s+(?:if\s+not\s+exists\s+)?(?<table>(?:"[^"]+"|\w+)(?:\s*\.\s*(?:"[^"]+"|\w+))?)/giu)) {
112
+ tables.add(normalizeIdentifier(match.groups?.table));
113
+ }
114
+ return tables;
115
+ }
116
+ async function collectDrizzleMigrationFiles(migrationsPath, cwd) {
117
+ const entries = await readDirectoryOrEmpty(migrationsPath);
118
+ const migrations = [];
119
+ for (const entry of entries) {
120
+ const entryPath = join(migrationsPath, entry.name);
121
+ if (entry.isDirectory()) {
122
+ const migrationPath = join(entryPath, "migration.sql");
123
+ if (await fileExists(migrationPath)) {
124
+ migrations.push(migrationFileFromPath(migrationPath, cwd, entry.name));
125
+ }
126
+ continue;
127
+ }
128
+ if (entry.isFile() && entry.name.endsWith(".sql")) {
129
+ migrations.push(migrationFileFromPath(entryPath, cwd, entry.name.replace(/\.sql$/u, "")));
130
+ }
131
+ }
132
+ return migrations.sort((left, right) => left.id.localeCompare(right.id));
133
+ }
134
+ async function collectFlatSqlFiles(directory, cwd) {
135
+ const entries = await readDirectoryOrEmpty(directory);
136
+ const migrations = entries
137
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".sql"))
138
+ .map((entry) => migrationFileFromPath(join(directory, entry.name), cwd, entry.name.replace(/\.sql$/u, "")));
139
+ return migrations.sort((left, right) => left.id.localeCompare(right.id));
140
+ }
141
+ async function readDirectoryOrEmpty(directory) {
142
+ try {
143
+ return await readdir(directory, { withFileTypes: true });
144
+ }
145
+ catch (error) {
146
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
147
+ return [];
148
+ }
149
+ throw error;
150
+ }
151
+ }
152
+ async function fileExists(path) {
153
+ try {
154
+ await access(path);
155
+ return true;
156
+ }
157
+ catch (error) {
158
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
159
+ return false;
160
+ }
161
+ throw error;
162
+ }
163
+ }
164
+ function migrationFileFromPath(path, cwd, id = basename(path, ".sql")) {
165
+ return {
166
+ id,
167
+ path,
168
+ relativePath: relative(cwd, path),
169
+ };
170
+ }
171
+ function maskSql(sql) {
172
+ return sql
173
+ .replace(/--[^\n\r]*/gu, blank)
174
+ .replace(/\/\*[\s\S]*?\*\//gu, blank)
175
+ .replace(/\$([A-Za-z_][A-Za-z0-9_]*)?\$[\s\S]*?\$\1\$/gu, blank)
176
+ .replace(/'(?:''|[^'])*'/gu, blank);
177
+ }
178
+ function blank(match) {
179
+ return match.replace(/[^\n\r]/gu, " ");
180
+ }
181
+ function normalizeIdentifier(identifier) {
182
+ return ((identifier ?? "")
183
+ .split(".")
184
+ .map((part) => part.trim().replace(/^"|"$/gu, "").toLowerCase())
185
+ .at(-1) ?? "");
186
+ }
187
+ function riskForMatch(sql, match, code, reason) {
188
+ return {
189
+ code,
190
+ line: lineForIndex(sql, match.index ?? 0),
191
+ match: sql.slice(match.index ?? 0, (match.index ?? 0) + match[0].length),
192
+ reason,
193
+ };
194
+ }
195
+ function lineForIndex(value, index) {
196
+ let line = 1;
197
+ for (let offset = 0; offset < index; offset += 1) {
198
+ if (value[offset] === "\n") {
199
+ line += 1;
200
+ }
201
+ }
202
+ return line;
203
+ }
@@ -0,0 +1,5 @@
1
+ import type { CoreframeDatabaseClient, CoreframeDatabaseDialect } from "../db/client.ts";
2
+ import type { MigrationFile } from "./migrations.ts";
3
+ export declare function filterPendingMigrations(migrations: MigrationFile[], appliedNames: Iterable<string>): MigrationFile[];
4
+ export declare function readAppliedMigrationNames(database: CoreframeDatabaseClient): Promise<Set<string>>;
5
+ export declare function appliedMigrationsQuery(dialect: CoreframeDatabaseDialect): "select name from drizzle.__drizzle_migrations where name is not null" | "select name from __drizzle_migrations where name is not null";
@@ -0,0 +1,30 @@
1
+ export function filterPendingMigrations(migrations, appliedNames) {
2
+ const applied = new Set(appliedNames);
3
+ return migrations.filter((migration) => !applied.has(migration.id));
4
+ }
5
+ export async function readAppliedMigrationNames(database) {
6
+ try {
7
+ const result = await database.execute(appliedMigrationsQuery(database.dialect));
8
+ return new Set(result.rows.flatMap((row) => (row.name ? [row.name] : [])));
9
+ }
10
+ catch (error) {
11
+ if (isMissingMigrationTableError(error)) {
12
+ return new Set();
13
+ }
14
+ throw error;
15
+ }
16
+ }
17
+ export function appliedMigrationsQuery(dialect) {
18
+ switch (dialect) {
19
+ case "postgres":
20
+ return "select name from drizzle.__drizzle_migrations where name is not null";
21
+ case "sqlite":
22
+ return "select name from __drizzle_migrations where name is not null";
23
+ }
24
+ }
25
+ function isMissingMigrationTableError(error) {
26
+ return (error instanceof Error &&
27
+ (("code" in error && (error.code === "42P01" || error.code === "3F000")) ||
28
+ error.message.includes("no such table") ||
29
+ error.message.includes("no such schema")));
30
+ }
@@ -0,0 +1,85 @@
1
+ import type { TauriReleaseConfig, TauriReleasePlatform } from "./config.ts";
2
+ export type TauriReleaseArgs = {
3
+ tauriLocalPlatforms: TauriReleasePlatform[];
4
+ tauriWorkflowPlatforms: TauriReleasePlatform[];
5
+ tauriWorkflowName?: string;
6
+ tauriWorkflowRef?: string;
7
+ };
8
+ export type TauriReleasePlan = {
9
+ artifactDownloadDir: string;
10
+ b2ApplicationKey?: string;
11
+ b2ApplicationKeyId?: string;
12
+ b2Bucket?: string;
13
+ enabled: boolean;
14
+ latestManifestPath: string;
15
+ localPlatforms: TauriReleasePlatform[];
16
+ platform: Record<TauriReleasePlatform, ResolvedTauriReleasePlatformConfig>;
17
+ publicBaseUrl?: string;
18
+ publicPathPrefix: string;
19
+ releaseNotesPath?: string;
20
+ releasePath: string;
21
+ releaseUploadDir: string;
22
+ signingPrivateKey?: string;
23
+ workflowName: string;
24
+ workflowPlatforms: TauriReleasePlatform[];
25
+ workflowPollIntervalMs: number;
26
+ workflowRef?: string;
27
+ workflowTimeoutMs: number;
28
+ };
29
+ export type TauriReleaseMetadataSnapshot = {
30
+ files: {
31
+ path: string;
32
+ source: string;
33
+ }[];
34
+ };
35
+ export type PreparedTauriRelease = {
36
+ manifestPath: string;
37
+ uploadDir: string;
38
+ };
39
+ type ResolvedTauriReleasePlatformConfig = {
40
+ artifactGlobs: string[];
41
+ buildArgs: string[];
42
+ targetTriple: string;
43
+ updatePlatform: string;
44
+ };
45
+ type TauriUpdateManifest = {
46
+ notes?: string;
47
+ platforms: Record<string, {
48
+ signature: string;
49
+ url: string;
50
+ }>;
51
+ pub_date: string;
52
+ version: string;
53
+ };
54
+ export declare function parseTauriReleasePlatformList(value: string | undefined): ("macos" | "windows" | "linux" | "ios" | "android")[];
55
+ export declare function parseTauriReleasePlatform(value: string): TauriReleasePlatform;
56
+ export declare function resolveTauriReleasePlan({ args, config, env, }: {
57
+ args: TauriReleaseArgs;
58
+ config?: TauriReleaseConfig;
59
+ env: NodeJS.ProcessEnv;
60
+ }): TauriReleasePlan;
61
+ export declare function validateTauriReleasePlan(plan: TauriReleasePlan, cwd?: string): Promise<void>;
62
+ export declare function writeTauriReleaseMetadata(plan: TauriReleasePlan, version: string, cwd?: string): Promise<TauriReleaseMetadataSnapshot>;
63
+ export declare function restoreTauriReleaseMetadata(snapshot: TauriReleaseMetadataSnapshot): Promise<void>;
64
+ export declare function prepareTauriRelease({ env, plan, releaseName, version, }: {
65
+ env: NodeJS.ProcessEnv;
66
+ plan: TauriReleasePlan;
67
+ releaseName: string;
68
+ version: string;
69
+ }): Promise<PreparedTauriRelease | undefined>;
70
+ export declare function uploadTauriReleaseToB2(plan: TauriReleasePlan, release: PreparedTauriRelease): Promise<void>;
71
+ export declare function createTauriUpdateManifest({ artifacts, notes, pubDate, version, }: {
72
+ artifacts: {
73
+ signature: string;
74
+ updatePlatform: string;
75
+ url: string;
76
+ }[];
77
+ notes?: string;
78
+ pubDate?: Date;
79
+ version: string;
80
+ }): TauriUpdateManifest;
81
+ export declare function createTauriReleaseUpload({ plan, version, }: {
82
+ plan: TauriReleasePlan;
83
+ version: string;
84
+ }): Promise<PreparedTauriRelease>;
85
+ export {};