@happyvertical/smrt-cli 0.40.21 → 0.40.22

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/README.md CHANGED
@@ -55,6 +55,27 @@ reconciles live schema drift. Global `--force` remains available by itself for
55
55
  backward compatibility but intentionally overrides guards for the whole pending
56
56
  batch.
57
57
 
58
+ ### Audited PostgreSQL timestamp conversion
59
+
60
+ `timestamp without time zone` values do not carry enough information for SMRT
61
+ to infer their original instant. After auditing every historical writer,
62
+ database default, trigger, and raw SQL path, an operator may confirm that the
63
+ legacy values are UTC wall times and include the exact opt-in:
64
+
65
+ ```bash
66
+ smrt db:migrate --postgres-timestamp-legacy-timezone UTC
67
+ ```
68
+
69
+ The option has no default and rejects every value other than `UTC`. On
70
+ PostgreSQL, it allows the manifest-owned timestamp columns to be converted to
71
+ `timestamptz` with `USING column AT TIME ZONE 'UTC'`, preserving the proven UTC
72
+ instants. It is not safe for a database with any local-time writer; use an
73
+ application-owned, provenance-aware migration in that case. Rehearse against a
74
+ restored clone and keep a verified backup because type upgrades have no
75
+ automatic down migration. `smrt db:diff --postgres-timestamp-legacy-timezone
76
+ UTC` and `smrt db:migrate --dry-run --postgres-timestamp-legacy-timezone UTC`
77
+ both preview the same conversion without writing it.
78
+
58
79
  ### Code Generation
59
80
 
60
81
  | Command | Description |
@@ -408,6 +408,20 @@ async function autoDiscoverAndLoad(projectRoot = process.cwd()) {
408
408
  };
409
409
  }
410
410
  //#endregion
411
+ //#region src/commands/postgres-timestamp-migration.ts
412
+ /**
413
+ * Fail-closed confirmation shared by PostgreSQL schema preview and migration.
414
+ *
415
+ * A `timestamp without time zone` value cannot establish its own original
416
+ * instant. Callers must therefore make the same explicit UTC provenance
417
+ * confirmation whether they are previewing or applying the conversion.
418
+ */
419
+ function resolvePostgresTimestampMigration(legacyTimezone) {
420
+ if (legacyTimezone === void 0) return;
421
+ if (legacyTimezone !== "UTC") throw new Error("--postgres-timestamp-legacy-timezone must be exactly UTC; refusing to infer the offset of legacy PostgreSQL timestamps");
422
+ return { legacyTimezone: "UTC" };
423
+ }
424
+ //#endregion
411
425
  //#region src/commands/db-diff.ts
412
426
  /**
413
427
  * db:diff Command
@@ -463,11 +477,16 @@ var dbDiffCommand = {
463
477
  type: "boolean",
464
478
  description: "Include orphan-index drops in the diff (indexes in DB but not in the manifest, excluding *_pkey/*_key implicit-from-constraint indexes). Off by default for safety.",
465
479
  default: false
480
+ },
481
+ "postgres-timestamp-legacy-timezone": {
482
+ type: "string",
483
+ description: "Confirm that legacy PostgreSQL timestamp-without-time-zone values are UTC wall times before previewing their conversion to timestamptz. Exact value required: UTC; omitted by default."
466
484
  }
467
485
  },
468
486
  handler: async (_args, options) => {
469
487
  let db;
470
488
  try {
489
+ const postgresTimestampMigration = resolvePostgresTimestampMigration(options["postgres-timestamp-legacy-timezone"]);
471
490
  const unsupportedFileOptions = [
472
491
  "generate",
473
492
  "name",
@@ -516,7 +535,8 @@ var dbDiffCommand = {
516
535
  const diff = await new SchemaComparer(db, {
517
536
  includeDroppedTables: false,
518
537
  includeDroppedColumns: false,
519
- includeDroppedIndexes: Boolean(options["drop-indexes"])
538
+ includeDroppedIndexes: Boolean(options["drop-indexes"]),
539
+ postgresTimestampMigration
520
540
  }).compare(schemaDefinitions);
521
541
  if (options.json) {
522
542
  console.log(JSON.stringify({
@@ -7074,6 +7094,10 @@ export default testManifest;
7074
7094
  description: "Use PostgreSQL-safe operations (CONCURRENTLY for indexes, lock_timeout)",
7075
7095
  default: false
7076
7096
  },
7097
+ "postgres-timestamp-legacy-timezone": {
7098
+ type: "string",
7099
+ description: "Confirm that legacy PostgreSQL timestamp-without-time-zone values are UTC wall times before converting them to timestamptz. Exact value required: UTC; omitted by default."
7100
+ },
7077
7101
  force: {
7078
7102
  type: "boolean",
7079
7103
  description: "Force re-apply even if already applied (skip checksum validation)",
@@ -7111,6 +7135,7 @@ export default testManifest;
7111
7135
  let db;
7112
7136
  try {
7113
7137
  const forceSelection = resolveForceMigrationSelection(options.force, options["force-migration"]);
7138
+ const postgresTimestampMigration = resolvePostgresTimestampMigration(options["postgres-timestamp-legacy-timezone"]);
7114
7139
  const { getPackageConfig } = await import("@happyvertical/smrt-config");
7115
7140
  const { DEFAULT_CLI_CONFIG } = await import("./config-BwrFRL8L.js");
7116
7141
  const config = getPackageConfig("cli", DEFAULT_CLI_CONFIG);
@@ -7211,7 +7236,10 @@ export default testManifest;
7211
7236
  console.warn(" Use --repair-data instead.\n");
7212
7237
  }
7213
7238
  console.log("🔍 Comparing schemas...\n");
7214
- const diff = await new SchemaComparer(db, { includeDroppedIndexes: Boolean(options["drop-indexes"]) }).compare(manifestSchemas);
7239
+ const diff = await new SchemaComparer(db, {
7240
+ includeDroppedIndexes: Boolean(options["drop-indexes"]),
7241
+ postgresTimestampMigration
7242
+ }).compare(manifestSchemas);
7215
7243
  const getClassForTable = (tableName) => {
7216
7244
  for (const className of initOrder) if (ObjectRegistry.getTableName(className) === tableName) return className;
7217
7245
  return tableName;
package/dist/index.js CHANGED
@@ -48,56 +48,56 @@ var _docsCommands = null;
48
48
  var _playgroundCommands = null;
49
49
  async function getGnodeCommands() {
50
50
  if (!_gnodeCommands) {
51
- const { gnodeCommands } = await import("./commands-mHvAZYmi.js");
51
+ const { gnodeCommands } = await import("./commands-DrK9PHS4.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-mHvAZYmi.js");
58
+ const { gitCommands } = await import("./commands-DrK9PHS4.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-mHvAZYmi.js");
65
+ const { generateCommands } = await import("./commands-DrK9PHS4.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-mHvAZYmi.js");
72
+ const { initCommands } = await import("./commands-DrK9PHS4.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-mHvAZYmi.js");
79
+ const { utilityCommands } = await import("./commands-DrK9PHS4.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-mHvAZYmi.js");
86
+ const { dispatchCommands } = await import("./commands-DrK9PHS4.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-mHvAZYmi.js");
93
+ const { docsCommands } = await import("./commands-DrK9PHS4.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-mHvAZYmi.js");
100
+ const { playgroundCommands } = await import("./commands-DrK9PHS4.js");
101
101
  _playgroundCommands = playgroundCommands;
102
102
  }
103
103
  return _playgroundCommands;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-cli",
3
- "version": "0.40.21",
3
+ "version": "0.40.22",
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-core": "0.40.21",
36
- "@happyvertical/smrt-agents": "0.40.21",
37
- "@happyvertical/smrt-config": "0.40.21",
38
- "@happyvertical/smrt-playground": "0.40.21",
39
- "@happyvertical/smrt-dev-mcp": "0.40.21",
40
- "@happyvertical/smrt-types": "0.40.21"
35
+ "@happyvertical/smrt-agents": "0.40.22",
36
+ "@happyvertical/smrt-core": "0.40.22",
37
+ "@happyvertical/smrt-config": "0.40.22",
38
+ "@happyvertical/smrt-dev-mcp": "0.40.22",
39
+ "@happyvertical/smrt-playground": "0.40.22",
40
+ "@happyvertical/smrt-types": "0.40.22"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "24.13.2",