@actuate-media/cli 0.4.1 → 0.5.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 (92) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +69 -12
  3. package/CHANGELOG.md +58 -0
  4. package/dist/__tests__/db-init.test.d.ts +2 -0
  5. package/dist/__tests__/db-init.test.d.ts.map +1 -0
  6. package/dist/__tests__/db-init.test.js +127 -0
  7. package/dist/__tests__/db-init.test.js.map +1 -0
  8. package/dist/__tests__/db-sync.test.d.ts +2 -0
  9. package/dist/__tests__/db-sync.test.d.ts.map +1 -0
  10. package/dist/__tests__/db-sync.test.js +136 -0
  11. package/dist/__tests__/db-sync.test.js.map +1 -0
  12. package/dist/__tests__/deployment-diagnostics.test.js.map +1 -1
  13. package/dist/__tests__/init.test.js.map +1 -1
  14. package/dist/__tests__/schema-fragment.test.js +1 -1
  15. package/dist/__tests__/schema-fragment.test.js.map +1 -1
  16. package/dist/__tests__/seed.test.js.map +1 -1
  17. package/dist/commands/db-init.d.ts +19 -2
  18. package/dist/commands/db-init.d.ts.map +1 -1
  19. package/dist/commands/db-init.js +128 -306
  20. package/dist/commands/db-init.js.map +1 -1
  21. package/dist/commands/db-status.d.ts +1 -1
  22. package/dist/commands/db-status.d.ts.map +1 -1
  23. package/dist/commands/db-status.js +33 -33
  24. package/dist/commands/db-status.js.map +1 -1
  25. package/dist/commands/db-sync.d.ts +31 -0
  26. package/dist/commands/db-sync.d.ts.map +1 -0
  27. package/dist/commands/db-sync.js +195 -0
  28. package/dist/commands/db-sync.js.map +1 -0
  29. package/dist/commands/doctor.d.ts +1 -1
  30. package/dist/commands/doctor.d.ts.map +1 -1
  31. package/dist/commands/doctor.js +48 -41
  32. package/dist/commands/doctor.js.map +1 -1
  33. package/dist/commands/export.d.ts +1 -1
  34. package/dist/commands/export.d.ts.map +1 -1
  35. package/dist/commands/export.js +32 -32
  36. package/dist/commands/export.js.map +1 -1
  37. package/dist/commands/generate.d.ts +1 -1
  38. package/dist/commands/generate.d.ts.map +1 -1
  39. package/dist/commands/generate.js +8 -8
  40. package/dist/commands/generate.js.map +1 -1
  41. package/dist/commands/import.d.ts +1 -1
  42. package/dist/commands/import.d.ts.map +1 -1
  43. package/dist/commands/import.js +55 -58
  44. package/dist/commands/import.js.map +1 -1
  45. package/dist/commands/init.d.ts.map +1 -1
  46. package/dist/commands/init.js.map +1 -1
  47. package/dist/commands/migrate.d.ts +1 -1
  48. package/dist/commands/migrate.d.ts.map +1 -1
  49. package/dist/commands/migrate.js +18 -24
  50. package/dist/commands/migrate.js.map +1 -1
  51. package/dist/commands/seed.d.ts +1 -1
  52. package/dist/commands/seed.d.ts.map +1 -1
  53. package/dist/commands/seed.js +156 -157
  54. package/dist/commands/seed.js.map +1 -1
  55. package/dist/commands/update-check.d.ts +1 -1
  56. package/dist/commands/update-check.d.ts.map +1 -1
  57. package/dist/commands/update-check.js +34 -27
  58. package/dist/commands/update-check.js.map +1 -1
  59. package/dist/commands/upgrade.d.ts +1 -1
  60. package/dist/commands/upgrade.d.ts.map +1 -1
  61. package/dist/commands/upgrade.js +46 -34
  62. package/dist/commands/upgrade.js.map +1 -1
  63. package/dist/deployment/diagnostics.d.ts.map +1 -1
  64. package/dist/deployment/diagnostics.js +7 -2
  65. package/dist/deployment/diagnostics.js.map +1 -1
  66. package/dist/index.js +17 -15
  67. package/dist/index.js.map +1 -1
  68. package/dist/utils/logger.d.ts.map +1 -1
  69. package/dist/utils/logger.js +5 -5
  70. package/dist/utils/logger.js.map +1 -1
  71. package/package.json +3 -3
  72. package/src/__tests__/db-init.test.ts +155 -0
  73. package/src/__tests__/db-sync.test.ts +167 -0
  74. package/src/__tests__/deployment-diagnostics.test.ts +68 -60
  75. package/src/__tests__/init.test.ts +17 -17
  76. package/src/__tests__/schema-fragment.test.ts +29 -25
  77. package/src/__tests__/seed.test.ts +25 -25
  78. package/src/commands/db-init.ts +146 -319
  79. package/src/commands/db-status.ts +70 -68
  80. package/src/commands/db-sync.ts +227 -0
  81. package/src/commands/doctor.ts +102 -88
  82. package/src/commands/export.ts +65 -75
  83. package/src/commands/generate.ts +14 -16
  84. package/src/commands/import.ts +125 -140
  85. package/src/commands/init.ts +14 -14
  86. package/src/commands/migrate.ts +29 -35
  87. package/src/commands/seed.ts +294 -300
  88. package/src/commands/update-check.ts +77 -72
  89. package/src/commands/upgrade.ts +100 -85
  90. package/src/deployment/diagnostics.ts +86 -72
  91. package/src/index.ts +32 -30
  92. package/src/utils/logger.ts +10 -10
@@ -1,120 +1,122 @@
1
- import { Command } from "commander";
2
- import { execSync } from "node:child_process";
3
- import { readFile, access } from "node:fs/promises";
4
- import { resolve } from "node:path";
5
- import ora from "ora";
6
- import chalk from "chalk";
7
- import { logger } from "../utils/logger.js";
8
- import { REQUIRED_CMS_MODELS } from "../deployment/diagnostics.js";
1
+ import { Command } from 'commander'
2
+ import { execSync } from 'node:child_process'
3
+ import { readFile, access } from 'node:fs/promises'
4
+ import { resolve } from 'node:path'
5
+ import ora from 'ora'
6
+ import chalk from 'chalk'
7
+ import { logger } from '../utils/logger.js'
8
+ import { REQUIRED_CMS_MODELS } from '../deployment/diagnostics.js'
9
9
 
10
- const CMS_SCHEMA_MARKER = "// ── Actuate CMS models";
10
+ const CMS_SCHEMA_MARKER = '// ── Actuate CMS models'
11
11
 
12
12
  async function fileExists(filePath: string): Promise<boolean> {
13
13
  try {
14
- await access(filePath);
15
- return true;
14
+ await access(filePath)
15
+ return true
16
16
  } catch {
17
- return false;
17
+ return false
18
18
  }
19
19
  }
20
20
 
21
21
  export function registerDbStatusCommand(program: Command): void {
22
22
  program
23
- .command("db:status")
24
- .description("Check which Actuate CMS models are present in your Prisma schema")
25
- .option("--schema <path>", "Path to schema.prisma", "prisma/schema.prisma")
23
+ .command('db:status')
24
+ .description('Check which Actuate CMS models are present in your Prisma schema')
25
+ .option('--schema <path>', 'Path to schema.prisma', 'prisma/schema.prisma')
26
26
  .action(async (opts: { schema: string }) => {
27
- const schemaPath = resolve(process.cwd(), opts.schema);
27
+ const schemaPath = resolve(process.cwd(), opts.schema)
28
28
 
29
29
  if (!(await fileExists(schemaPath))) {
30
- logger.error(`Schema file not found at ${schemaPath}`);
31
- logger.info("Run `npx prisma init` first, or specify --schema <path>.");
32
- process.exitCode = 1;
33
- return;
30
+ logger.error(`Schema file not found at ${schemaPath}`)
31
+ logger.info('Run `npx prisma init` first, or specify --schema <path>.')
32
+ process.exitCode = 1
33
+ return
34
34
  }
35
35
 
36
- const spinner = ora("Checking schema...").start();
36
+ const spinner = ora('Checking schema...').start()
37
37
 
38
- let content: string;
38
+ let content: string
39
39
  try {
40
- content = await readFile(schemaPath, "utf-8");
40
+ content = await readFile(schemaPath, 'utf-8')
41
41
  } catch (err) {
42
- spinner.fail("Failed to read schema file.");
43
- logger.error(err instanceof Error ? err.message : String(err));
44
- process.exitCode = 1;
45
- return;
42
+ spinner.fail('Failed to read schema file.')
43
+ logger.error(err instanceof Error ? err.message : String(err))
44
+ process.exitCode = 1
45
+ return
46
46
  }
47
47
 
48
- spinner.stop();
48
+ spinner.stop()
49
49
 
50
- const hasMarker = content.includes(CMS_SCHEMA_MARKER);
51
- const modelRegex = /^model\s+(\w+)\s*\{/gm;
52
- const schemaModels = new Set<string>();
53
- let match;
50
+ const hasMarker = content.includes(CMS_SCHEMA_MARKER)
51
+ const modelRegex = /^model\s+(\w+)\s*\{/gm
52
+ const schemaModels = new Set<string>()
53
+ let match
54
54
  while ((match = modelRegex.exec(content)) !== null) {
55
- schemaModels.add(match[1]!);
55
+ schemaModels.add(match[1]!)
56
56
  }
57
57
 
58
- console.log();
59
- console.log(chalk.bold(" Actuate CMS Model Status"));
60
- console.log(chalk.dim(" ─────────────────────────────────"));
61
- console.log();
58
+ console.log()
59
+ console.log(chalk.bold(' Actuate CMS Model Status'))
60
+ console.log(chalk.dim(' ─────────────────────────────────'))
61
+ console.log()
62
62
 
63
- let present = 0;
64
- let missing = 0;
63
+ let present = 0
64
+ let missing = 0
65
65
 
66
66
  for (const model of REQUIRED_CMS_MODELS) {
67
67
  if (schemaModels.has(model)) {
68
- console.log(` ${chalk.green("")} ${model}`);
69
- present++;
68
+ console.log(` ${chalk.green('')} ${model}`)
69
+ present++
70
70
  } else {
71
- console.log(` ${chalk.red("")} ${model} ${chalk.dim("— missing")}`);
72
- missing++;
71
+ console.log(` ${chalk.red('')} ${model} ${chalk.dim('— missing')}`)
72
+ missing++
73
73
  }
74
74
  }
75
75
 
76
- console.log();
77
- console.log(chalk.dim(" ─────────────────────────────────"));
78
- console.log(` ${chalk.green(present)} present, ${missing > 0 ? chalk.red(missing) : chalk.green(missing)} missing`);
76
+ console.log()
77
+ console.log(chalk.dim(' ─────────────────────────────────'))
78
+ console.log(
79
+ ` ${chalk.green(present)} present, ${missing > 0 ? chalk.red(missing) : chalk.green(missing)} missing`,
80
+ )
79
81
 
80
82
  if (!hasMarker) {
81
- console.log();
82
- logger.warn("CMS schema marker not found. Models may have been added manually.");
83
- logger.info("Run `actuate db:init` to add CMS models with proper markers.");
83
+ console.log()
84
+ logger.warn('CMS schema marker not found. Models may have been added manually.')
85
+ logger.info('Run `actuate db:init` to add CMS models with proper markers.')
84
86
  }
85
87
 
86
88
  if (missing > 0) {
87
- console.log();
88
- logger.info("Run `actuate db:init` to add missing models.");
89
+ console.log()
90
+ logger.info('Run `actuate db:init` to add missing models.')
89
91
  }
90
92
 
91
- let dbConnected = false;
93
+ let dbConnected = false
92
94
  try {
93
- spinner.start("Checking database connection...");
94
- execSync("npx prisma db execute --stdin --schema " + JSON.stringify(schemaPath), {
95
- input: "SELECT 1;",
96
- stdio: ["pipe", "pipe", "pipe"],
95
+ spinner.start('Checking database connection...')
96
+ execSync('npx prisma db execute --stdin --schema ' + JSON.stringify(schemaPath), {
97
+ input: 'SELECT 1;',
98
+ stdio: ['pipe', 'pipe', 'pipe'],
97
99
  cwd: process.cwd(),
98
- });
99
- spinner.succeed("Database connection OK.");
100
- dbConnected = true;
100
+ })
101
+ spinner.succeed('Database connection OK.')
102
+ dbConnected = true
101
103
  } catch {
102
- spinner.warn("Could not connect to database. Check DATABASE_URL.");
104
+ spinner.warn('Could not connect to database. Check DATABASE_URL.')
103
105
  }
104
106
 
105
107
  if (dbConnected && missing === 0) {
106
108
  try {
107
- spinner.start("Checking migration status...");
108
- spinner.stop();
109
- execSync("npx prisma migrate status", {
110
- stdio: "inherit",
109
+ spinner.start('Checking migration status...')
110
+ spinner.stop()
111
+ execSync('npx prisma migrate status', {
112
+ stdio: 'inherit',
111
113
  cwd: process.cwd(),
112
- });
114
+ })
113
115
  } catch {
114
- logger.warn("Could not check migration status.");
116
+ logger.warn('Could not check migration status.')
115
117
  }
116
118
  }
117
119
 
118
- console.log();
119
- });
120
+ console.log()
121
+ })
120
122
  }
@@ -0,0 +1,227 @@
1
+ import { Command } from 'commander'
2
+ import { access, cp, mkdir, readFile, readdir, writeFile } from 'node:fs/promises'
3
+ import { existsSync } from 'node:fs'
4
+ import { dirname, join, resolve } from 'node:path'
5
+ import { createRequire } from 'node:module'
6
+ import ora from 'ora'
7
+ import { logger } from '../utils/logger.js'
8
+
9
+ const require = createRequire(import.meta.url)
10
+
11
+ // Marker that identifies a schema this CLI/scaffolder owns (auto-synced from
12
+ // cms-core). We refuse to overwrite a schema lacking it unless --force, so a
13
+ // hand-customized schema is never silently clobbered.
14
+ const AUTO_SYNCED_MARKER = 'AUTO-SYNCED from @actuate-media/cms-core'
15
+
16
+ // Must match create-actuate-cms/scripts/sync-prisma-assets.ts `SCAFFOLD_SCHEMA_HEADER`
17
+ // so re-syncing a scaffolded project is idempotent (no spurious diffs).
18
+ const SCHEMA_HEADER = `// ─────────────────────────────────────────────────────────────────────────────
19
+ // Actuate CMS — Prisma schema
20
+ //
21
+ // AUTO-SYNCED from @actuate-media/cms-core. Do NOT edit the model definitions
22
+ // by hand — they must match the bundled migrations and cms-core's API layer.
23
+ // (Generated by \`actuate db:sync\`)
24
+ // ─────────────────────────────────────────────────────────────────────────────
25
+
26
+ generator client {
27
+ provider = "prisma-client"
28
+ output = "../generated/prisma"
29
+ previewFeatures = ["fullTextSearchPostgres", "relationJoins"]
30
+ }
31
+
32
+ datasource db {
33
+ provider = "postgresql"
34
+ }`
35
+
36
+ /**
37
+ * Build the consumer's `schema.prisma` from cms-core's canonical schema: strip
38
+ * cms-core's own generator/datasource blocks and prepend the consumer header
39
+ * (client output `../generated/prisma`, no datasource `url` — supplied by
40
+ * `prisma.config.ts`). Pure for testing. Mirrors the scaffolder's builder.
41
+ */
42
+ export function buildConsumerSchema(coreSchemaSource: string): string {
43
+ const datasourceMatch = coreSchemaSource.match(/datasource\s+\w+\s*\{[\s\S]*?\}/)
44
+ if (!datasourceMatch || datasourceMatch.index === undefined) {
45
+ throw new Error('Could not locate the `datasource` block in cms-core schema.prisma')
46
+ }
47
+ const body = coreSchemaSource
48
+ .slice(datasourceMatch.index + datasourceMatch[0].length)
49
+ .replace(/^\s+/, '')
50
+
51
+ if (!/@@map\("actuate_users"\)/.test(body)) {
52
+ throw new Error('cms-core schema body is missing `@@map("actuate_users")` — aborting sync')
53
+ }
54
+
55
+ return `${SCHEMA_HEADER}\n\n${body.trimEnd()}\n`
56
+ }
57
+
58
+ /**
59
+ * Resolve the installed `@actuate-media/cms-core` package's `prisma/` directory
60
+ * (the source of truth for schema + migrations). Overridable for tests.
61
+ */
62
+ export let resolveCmsCorePrismaDir = (): string | null => {
63
+ try {
64
+ const exported = require.resolve('@actuate-media/cms-core/prisma/schema')
65
+ return dirname(exported)
66
+ } catch {
67
+ /* exports map missing the subpath — try the main entry */
68
+ }
69
+ try {
70
+ const mainEntry = require.resolve('@actuate-media/cms-core')
71
+ return join(dirname(mainEntry), '..', 'prisma')
72
+ } catch {
73
+ return null
74
+ }
75
+ }
76
+
77
+ const defaultPrismaDirResolver = resolveCmsCorePrismaDir
78
+
79
+ export function setCmsCorePrismaDirResolver(resolver: typeof resolveCmsCorePrismaDir): void {
80
+ resolveCmsCorePrismaDir = resolver
81
+ }
82
+
83
+ export function resetCmsCorePrismaDirResolver(): void {
84
+ resolveCmsCorePrismaDir = defaultPrismaDirResolver
85
+ }
86
+
87
+ async function listMigrationDirs(dir: string): Promise<string[]> {
88
+ try {
89
+ const entries = await readdir(dir, { withFileTypes: true })
90
+ return entries.filter((e) => e.isDirectory()).map((e) => e.name)
91
+ } catch {
92
+ return []
93
+ }
94
+ }
95
+
96
+ interface DbSyncOptions {
97
+ schema: string
98
+ force?: boolean
99
+ }
100
+
101
+ export interface DbSyncResult {
102
+ schemaWritten: boolean
103
+ migrationsAdded: string[]
104
+ skippedReason?: string
105
+ }
106
+
107
+ /**
108
+ * Core sync logic, separated from CLI plumbing for testing. Additive for
109
+ * migrations (never deletes existing ones — they are immutable history), and
110
+ * guarded for the schema (won't overwrite a non-auto-synced schema without
111
+ * `force`).
112
+ */
113
+ export async function syncPrismaAssets(
114
+ consumerSchemaPath: string,
115
+ corePrismaDir: string,
116
+ opts: { force?: boolean } = {},
117
+ ): Promise<DbSyncResult> {
118
+ const coreSchemaPath = join(corePrismaDir, 'schema.prisma')
119
+ const coreSchema = await readFile(coreSchemaPath, 'utf-8')
120
+ const nextSchema = buildConsumerSchema(coreSchema)
121
+
122
+ const consumerDir = dirname(consumerSchemaPath)
123
+ const consumerMigrationsDir = join(consumerDir, 'migrations')
124
+ const coreMigrationsDir = join(corePrismaDir, 'migrations')
125
+
126
+ // Guard the schema overwrite.
127
+ let schemaWritten = false
128
+ let skippedReason: string | undefined
129
+ const existing = existsSync(consumerSchemaPath)
130
+ ? await readFile(consumerSchemaPath, 'utf-8')
131
+ : null
132
+
133
+ if (existing && !existing.includes(AUTO_SYNCED_MARKER) && !opts.force) {
134
+ skippedReason =
135
+ 'schema.prisma is not an auto-synced Actuate schema (no AUTO-SYNCED marker). Re-run with --force to overwrite it.'
136
+ } else {
137
+ await mkdir(consumerDir, { recursive: true })
138
+ if (existing !== nextSchema) {
139
+ await writeFile(consumerSchemaPath, nextSchema)
140
+ schemaWritten = true
141
+ }
142
+ }
143
+
144
+ // Additively copy any cms-core migrations the consumer doesn't already have.
145
+ const coreMigrations = await listMigrationDirs(coreMigrationsDir)
146
+ const existingMigrations = new Set(await listMigrationDirs(consumerMigrationsDir))
147
+ const migrationsAdded: string[] = []
148
+
149
+ if (coreMigrations.length > 0) {
150
+ await mkdir(consumerMigrationsDir, { recursive: true })
151
+ const lockSrc = join(coreMigrationsDir, 'migration_lock.toml')
152
+ const lockDest = join(consumerMigrationsDir, 'migration_lock.toml')
153
+ if (existsSync(lockSrc) && !existsSync(lockDest)) {
154
+ await cp(lockSrc, lockDest)
155
+ }
156
+ for (const name of coreMigrations) {
157
+ if (existingMigrations.has(name)) continue
158
+ await cp(join(coreMigrationsDir, name), join(consumerMigrationsDir, name), {
159
+ recursive: true,
160
+ })
161
+ migrationsAdded.push(name)
162
+ }
163
+ }
164
+
165
+ return { schemaWritten, migrationsAdded, skippedReason }
166
+ }
167
+
168
+ async function runDbSync(options: DbSyncOptions): Promise<void> {
169
+ const consumerSchemaPath = resolve(process.cwd(), options.schema)
170
+ const spinner = ora('Locating @actuate-media/cms-core…').start()
171
+
172
+ const corePrismaDir = resolveCmsCorePrismaDir()
173
+ if (!corePrismaDir) {
174
+ spinner.fail('Could not locate @actuate-media/cms-core.')
175
+ logger.info('Install it first: `npm install @actuate-media/cms-core`.')
176
+ process.exitCode = 1
177
+ return
178
+ }
179
+
180
+ try {
181
+ await access(join(corePrismaDir, 'schema.prisma'))
182
+ } catch {
183
+ spinner.fail(`cms-core does not ship a Prisma schema at ${corePrismaDir}.`)
184
+ process.exitCode = 1
185
+ return
186
+ }
187
+
188
+ spinner.text = 'Syncing schema + migrations…'
189
+ let result: DbSyncResult
190
+ try {
191
+ result = await syncPrismaAssets(consumerSchemaPath, corePrismaDir, { force: options.force })
192
+ } catch (err) {
193
+ spinner.fail('Failed to sync Prisma assets.')
194
+ logger.error(err instanceof Error ? err.message : String(err))
195
+ process.exitCode = 1
196
+ return
197
+ }
198
+
199
+ spinner.succeed('Prisma assets synced from cms-core.')
200
+
201
+ if (result.skippedReason) {
202
+ logger.warn(`Schema not updated: ${result.skippedReason}`)
203
+ } else if (result.schemaWritten) {
204
+ logger.success('schema.prisma refreshed.')
205
+ } else {
206
+ logger.info('schema.prisma already up to date.')
207
+ }
208
+
209
+ if (result.migrationsAdded.length > 0) {
210
+ logger.success(`Added ${result.migrationsAdded.length} new migration(s).`)
211
+ } else {
212
+ logger.info('No new migrations to add.')
213
+ }
214
+
215
+ if (result.schemaWritten || result.migrationsAdded.length > 0) {
216
+ logger.info('Next: run `npx prisma migrate deploy` then `npx prisma generate`.')
217
+ }
218
+ }
219
+
220
+ export function registerDbSyncCommand(program: Command): void {
221
+ program
222
+ .command('db:sync')
223
+ .description('Sync the canonical Prisma schema + migrations from the installed cms-core')
224
+ .option('--schema <path>', 'Path to schema.prisma', 'prisma/schema.prisma')
225
+ .option('--force', 'Overwrite schema.prisma even if it lacks the AUTO-SYNCED marker')
226
+ .action(runDbSync)
227
+ }