@happyvertical/smrt-cli 0.40.9 → 0.40.11
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 +1 -1
- package/README.md +18 -6
- package/dist/{commands-CFm89PR4.js → commands-mHvAZYmi.js} +46 -3
- package/dist/index.js +44 -10
- package/package.json +8 -8
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
|
|
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
|
|
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.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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:
|
|
7313
|
-
forceMigrations:
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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 =
|
|
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 =
|
|
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.
|
|
3
|
+
"version": "0.40.11",
|
|
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-
|
|
36
|
-
"@happyvertical/smrt-
|
|
37
|
-
"@happyvertical/smrt-
|
|
38
|
-
"@happyvertical/smrt-
|
|
39
|
-
"@happyvertical/smrt-
|
|
40
|
-
"@happyvertical/smrt-
|
|
35
|
+
"@happyvertical/smrt-config": "0.40.11",
|
|
36
|
+
"@happyvertical/smrt-dev-mcp": "0.40.11",
|
|
37
|
+
"@happyvertical/smrt-types": "0.40.11",
|
|
38
|
+
"@happyvertical/smrt-playground": "0.40.11",
|
|
39
|
+
"@happyvertical/smrt-agents": "0.40.11",
|
|
40
|
+
"@happyvertical/smrt-core": "0.40.11"
|
|
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"
|