@happyvertical/smrt-cli 0.40.10 → 0.40.12

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/AGENTS.md CHANGED
@@ -8,7 +8,7 @@ Developer CLI with lazy-loaded commands, manifest discovery, and class introspec
8
8
  smrt introspect # Discover SMRT objects in project
9
9
  smrt db:status # Pending schema changes + failed migration classification
10
10
  smrt db:migrate # Apply migrations
11
- smrt db:migrate --force-migration <exact-id> # Force one generated migration only
11
+ smrt db:migrate --force-migration <exact-id> [--force-migration <exact-id>...] # Force exact generated migrations in one atomic batch
12
12
  smrt db:migrate-uuid # Convert schema-declared UUID text columns after data remap
13
13
  smrt db:diff # Show schema differences without generating migration files
14
14
  smrt db:rollback # Rollback migrations
package/README.md CHANGED
@@ -26,7 +26,7 @@ pnpm add -D @happyvertical/smrt-cli
26
26
  |---------|------------|
27
27
  | `smrt db:status` | Show pending schema changes and classify failed migration history |
28
28
  | `smrt db:migrate` | Apply pending migrations |
29
- | `smrt db:migrate --force-migration <exact-id>` | Force one generated migration while preserving guards for every other migration in the batch |
29
+ | `smrt db:migrate --force-migration <exact-id> [--force-migration <exact-id>...]` | Force one or more exact generated migrations in one atomic batch while preserving every other guard |
30
30
  | `smrt db:migrate-uuid` | Convert schema-declared UUID text columns to native PostgreSQL uuid after data has been remapped |
31
31
  | `smrt db:diff` | Show schema differences without generating migration files |
32
32
  | `smrt db:rollback` | Rollback last migration |
@@ -37,11 +37,23 @@ migrations are manifest-driven; model schema with SMRT objects and apply changes
37
37
  with `smrt db:migrate`.
38
38
 
39
39
  Use `--force-migration <exact-id>` for a known checksum, failed, or interrupted
40
- migration that is safe to retry. The selector is an exact generated migration
41
- ID, and unrelated checksum, failed, and running records remain fail-closed even
42
- when the atomic batch reconciles live schema drift. Global `--force` remains
43
- available for backward compatibility but intentionally overrides guards for the
44
- whole pending batch.
40
+ migration that is safe to retry. Repeat the flag to recover multiple verified
41
+ IDs in the same atomic invocation:
42
+
43
+ ```bash
44
+ smrt db:migrate \
45
+ --force-migration create_table_commissions \
46
+ --force-migration create_table_referral_links
47
+ ```
48
+
49
+ Each selector must be one exact generated migration ID. Comma-separated lists,
50
+ wildcards, empty values, and combining exact selectors with global `--force`
51
+ are rejected, as are IDs absent from the current generated migration batch.
52
+ Duplicate exact IDs are normalized to one selection. Unrelated checksum,
53
+ failed, and running records remain fail-closed even when the atomic batch
54
+ reconciles live schema drift. Global `--force` remains available by itself for
55
+ backward compatibility but intentionally overrides guards for the whole pending
56
+ batch.
45
57
 
46
58
  ### Code Generation
47
59
 
@@ -6477,6 +6477,43 @@ function formatStiConflictIdentity(conflict) {
6477
6477
  const entries = Object.entries(conflict.conflictIdentity);
6478
6478
  return `${entries.length > 0 ? entries.map(([column, value]) => `${column}=${JSON.stringify(value)}`).join(", ") : "no non-_meta_type conflict columns"}${conflict.legacyId || conflict.qualifiedId ? ` (legacy id: ${conflict.legacyId ?? "unknown"}, qualified id: ${conflict.qualifiedId ?? "unknown"})` : ""}`;
6479
6479
  }
6480
+ /**
6481
+ * Normalize exact migration selectors from the public single/repeated CLI
6482
+ * forms. List and wildcard syntax are deliberately rejected: every selected
6483
+ * migration must be named by its own exact flag.
6484
+ */
6485
+ function resolveForceMigrationSelection(force, value) {
6486
+ const globalForce = Boolean(force);
6487
+ if (value === void 0) return { force: globalForce };
6488
+ const rawValues = Array.isArray(value) ? value : [value];
6489
+ const forceMigrations = [];
6490
+ const seen = /* @__PURE__ */ new Set();
6491
+ for (const rawValue of rawValues) {
6492
+ if (typeof rawValue !== "string") throw new Error("--force-migration requires an exact generated migration ID");
6493
+ const migrationId = rawValue.trim();
6494
+ if (migrationId.length === 0) throw new Error("--force-migration cannot be empty");
6495
+ if (migrationId.includes(",")) throw new Error("Comma-separated --force-migration lists are not supported; repeat the flag for each exact ID");
6496
+ if (/\s/u.test(migrationId)) throw new Error(`Invalid --force-migration ID "${migrationId}": exact IDs cannot contain whitespace`);
6497
+ if (/[*?[\]{}]/u.test(migrationId)) throw new Error(`Invalid --force-migration ID "${migrationId}": wildcard selectors are not supported`);
6498
+ if (migrationId.startsWith("-")) throw new Error(`Invalid --force-migration ID "${migrationId}": expected an exact generated migration ID`);
6499
+ if (!seen.has(migrationId)) {
6500
+ seen.add(migrationId);
6501
+ forceMigrations.push(migrationId);
6502
+ }
6503
+ }
6504
+ if (forceMigrations.length === 0) throw new Error("--force-migration requires at least one exact ID");
6505
+ if (globalForce) throw new Error("Do not combine --force with --force-migration; choose global forcing or exact migration IDs");
6506
+ return {
6507
+ force: false,
6508
+ forceMigrations
6509
+ };
6510
+ }
6511
+ function assertForceMigrationTargetsExist(forceMigrations, generatedMigrationIds) {
6512
+ if (!forceMigrations?.length) return;
6513
+ const generated = new Set(generatedMigrationIds);
6514
+ const unknown = forceMigrations.filter((migrationId) => !generated.has(migrationId));
6515
+ if (unknown.length > 0) throw new Error(`--force-migration target${unknown.length === 1 ? "" : "s"} not found in the current generated migration batch: ${unknown.join(", ")}`);
6516
+ }
6480
6517
  function getErrorContext(error) {
6481
6518
  if (error && typeof error === "object" && "context" in error) {
6482
6519
  const context = error.context;
@@ -7044,7 +7081,8 @@ export default testManifest;
7044
7081
  },
7045
7082
  "force-migration": {
7046
7083
  type: "string",
7047
- description: "Force only the generated migration with this exact ID; all other migration guards remain enabled"
7084
+ description: "Force one generated migration by exact ID; repeat for multiple IDs while all other guards remain enabled",
7085
+ multiple: true
7048
7086
  },
7049
7087
  "repair-data": {
7050
7088
  type: "boolean",
@@ -7072,6 +7110,7 @@ export default testManifest;
7072
7110
  console.log("\n🔄 Migrating database schema...\n");
7073
7111
  let db;
7074
7112
  try {
7113
+ const forceSelection = resolveForceMigrationSelection(options.force, options["force-migration"]);
7075
7114
  const { getPackageConfig } = await import("@happyvertical/smrt-config");
7076
7115
  const { DEFAULT_CLI_CONFIG } = await import("./config-BwrFRL8L.js");
7077
7116
  const config = getPackageConfig("cli", DEFAULT_CLI_CONFIG);
@@ -7189,6 +7228,10 @@ export default testManifest;
7189
7228
  const partitionedChanges = partitionSchemaChanges(diff.changes, getClassForTable);
7190
7229
  migrations.push(...partitionedChanges.migrations);
7191
7230
  manualInterventions.push(...partitionedChanges.manualInterventions);
7231
+ assertForceMigrationTargetsExist(forceSelection.forceMigrations, [...diff.added_tables.map((schema) => `create_table_${schema.tableName}`), ...migrations.flatMap((migration) => {
7232
+ const migrationName = getSyntheticMigrationNameForAction(migration);
7233
+ return migrationName ? [migrationName] : [];
7234
+ })]);
7192
7235
  console.log();
7193
7236
  if (manualInterventions.length > 0) {
7194
7237
  console.log("⚠️ Schema drift detected that requires manual intervention:\n");
@@ -7309,8 +7352,8 @@ export default testManifest;
7309
7352
  const results = await tracker.applyAll(migrationDefs, {
7310
7353
  atomic: true,
7311
7354
  postgresSafe: false,
7312
- force: options.force ?? false,
7313
- forceMigrations: options["force-migration"] ? [options["force-migration"]] : void 0,
7355
+ force: forceSelection.force,
7356
+ forceMigrations: forceSelection.forceMigrations,
7314
7357
  reconcile: true,
7315
7358
  onProgress: (result) => {
7316
7359
  if (!result.success) return;
package/dist/index.js CHANGED
@@ -48,60 +48,94 @@ var _docsCommands = null;
48
48
  var _playgroundCommands = null;
49
49
  async function getGnodeCommands() {
50
50
  if (!_gnodeCommands) {
51
- const { gnodeCommands } = await import("./commands-CFm89PR4.js");
51
+ const { gnodeCommands } = await import("./commands-mHvAZYmi.js");
52
52
  _gnodeCommands = gnodeCommands;
53
53
  }
54
54
  return _gnodeCommands;
55
55
  }
56
56
  async function getGitCommands() {
57
57
  if (!_gitCommands) {
58
- const { gitCommands } = await import("./commands-CFm89PR4.js");
58
+ const { gitCommands } = await import("./commands-mHvAZYmi.js");
59
59
  _gitCommands = gitCommands;
60
60
  }
61
61
  return _gitCommands;
62
62
  }
63
63
  async function getGenerateCommands() {
64
64
  if (!_generateCommands) {
65
- const { generateCommands } = await import("./commands-CFm89PR4.js");
65
+ const { generateCommands } = await import("./commands-mHvAZYmi.js");
66
66
  _generateCommands = generateCommands;
67
67
  }
68
68
  return _generateCommands;
69
69
  }
70
70
  async function getInitCommands() {
71
71
  if (!_initCommands) {
72
- const { initCommands } = await import("./commands-CFm89PR4.js");
72
+ const { initCommands } = await import("./commands-mHvAZYmi.js");
73
73
  _initCommands = initCommands;
74
74
  }
75
75
  return _initCommands;
76
76
  }
77
77
  async function getUtilityCommands() {
78
78
  if (!_utilityCommands) {
79
- const { utilityCommands } = await import("./commands-CFm89PR4.js");
79
+ const { utilityCommands } = await import("./commands-mHvAZYmi.js");
80
80
  _utilityCommands = utilityCommands;
81
81
  }
82
82
  return _utilityCommands;
83
83
  }
84
84
  async function getDispatchCommands() {
85
85
  if (!_dispatchCommands) {
86
- const { dispatchCommands } = await import("./commands-CFm89PR4.js");
86
+ const { dispatchCommands } = await import("./commands-mHvAZYmi.js");
87
87
  _dispatchCommands = dispatchCommands;
88
88
  }
89
89
  return _dispatchCommands;
90
90
  }
91
91
  async function getDocsCommands() {
92
92
  if (!_docsCommands) {
93
- const { docsCommands } = await import("./commands-CFm89PR4.js");
93
+ const { docsCommands } = await import("./commands-mHvAZYmi.js");
94
94
  _docsCommands = docsCommands;
95
95
  }
96
96
  return _docsCommands;
97
97
  }
98
98
  async function getPlaygroundCommands() {
99
99
  if (!_playgroundCommands) {
100
- const { playgroundCommands } = await import("./commands-CFm89PR4.js");
100
+ const { playgroundCommands } = await import("./commands-mHvAZYmi.js");
101
101
  _playgroundCommands = playgroundCommands;
102
102
  }
103
103
  return _playgroundCommands;
104
104
  }
105
+ function collectRepeatableOptionValues(argv, optionName) {
106
+ const flag = `--${optionName}`;
107
+ const inlinePrefix = `${flag}=`;
108
+ const values = [];
109
+ for (let index = 0; index < argv.length; index += 1) {
110
+ const token = argv[index];
111
+ if (token === "--") break;
112
+ if (token.startsWith(inlinePrefix)) {
113
+ values.push(token.slice(inlinePrefix.length));
114
+ continue;
115
+ }
116
+ if (token !== flag) continue;
117
+ const value = argv[index + 1];
118
+ if (value === void 0 || value.startsWith("-")) throw new Error(`Option ${flag} requires a value`);
119
+ values.push(value);
120
+ index += 1;
121
+ }
122
+ return values;
123
+ }
124
+ /**
125
+ * Parse one command while preserving every occurrence of options marked
126
+ * `multiple`. Non-repeatable options retain the shared SDK parser behavior.
127
+ */
128
+ function parseCliCommandArgs(argv, commands, builtInCommands = {}) {
129
+ const parsed = parseCliArgs(argv, commands, builtInCommands);
130
+ const command = parsed.command ? builtInCommands[parsed.command] ?? commands.find((candidate) => candidate.name === parsed.command || candidate.aliases?.includes(parsed.command)) : void 0;
131
+ if (!command?.options) return parsed;
132
+ for (const [name, option] of Object.entries(command.options)) {
133
+ if (!option.multiple) continue;
134
+ const values = collectRepeatableOptionValues(argv, name);
135
+ if (values.length > 0) parsed.options[name] = values;
136
+ }
137
+ return parsed;
138
+ }
105
139
  /**
106
140
  * Generate CLI commands for smrt objects
107
141
  */
@@ -263,7 +297,7 @@ var CLIGenerator = class {
263
297
  return async (argv) => {
264
298
  const commands = await this.generateCommands();
265
299
  const processedArgv = this.preprocessObjectCommands(argv, commands);
266
- const parsed = parseCliArgs(processedArgv, commands, {});
300
+ const parsed = parseCliCommandArgs(processedArgv, commands, {});
267
301
  await this.executeCommand(parsed, commands, processedArgv);
268
302
  };
269
303
  }
@@ -672,7 +706,7 @@ var CLIGenerator = class {
672
706
  this.exitWithError(`Command '${parsed.command}' has no handler defined`);
673
707
  return;
674
708
  }
675
- const reParsed = parseCliArgs(processedArgv, [builtInCommand], {});
709
+ const reParsed = parseCliCommandArgs(processedArgv, [builtInCommand], {});
676
710
  try {
677
711
  await builtInCommand.handler(reParsed.args, reParsed.options);
678
712
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-cli",
3
- "version": "0.40.10",
3
+ "version": "0.40.12",
4
4
  "description": "Developer CLI for SMRT framework - introspection, testing, and project management",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -32,12 +32,12 @@
32
32
  "acorn": "^8.17.0",
33
33
  "fast-glob": "3.3.3",
34
34
  "tar": "^7.5.19",
35
- "@happyvertical/smrt-agents": "0.40.10",
36
- "@happyvertical/smrt-config": "0.40.10",
37
- "@happyvertical/smrt-dev-mcp": "0.40.10",
38
- "@happyvertical/smrt-core": "0.40.10",
39
- "@happyvertical/smrt-playground": "0.40.10",
40
- "@happyvertical/smrt-types": "0.40.10"
35
+ "@happyvertical/smrt-agents": "0.40.12",
36
+ "@happyvertical/smrt-core": "0.40.12",
37
+ "@happyvertical/smrt-config": "0.40.12",
38
+ "@happyvertical/smrt-dev-mcp": "0.40.12",
39
+ "@happyvertical/smrt-types": "0.40.12",
40
+ "@happyvertical/smrt-playground": "0.40.12"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "24.13.2",
@@ -72,7 +72,7 @@
72
72
  "clean": "rm -rf dist",
73
73
  "dev": "npm run build:watch",
74
74
  "test": "vitest run",
75
- "test:postgres": "node ../../scripts/run-with-ci-postgres.mjs -- pnpm exec vitest run src/commands/__tests__/db-migrate-uuid.test.ts",
75
+ "test:postgres": "node ../../scripts/run-with-ci-postgres.mjs -- pnpm exec vitest run src/commands/__tests__/db-migrate-uuid.test.ts src/commands/__tests__/db-migrate-force-postgres.test.ts",
76
76
  "test:watch": "vitest",
77
77
  "typecheck": "tsc -p tsconfig.typecheck.json",
78
78
  "cli": "tsx src/index.ts"