@happyvertical/smrt-cli 0.45.3 → 0.46.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.
package/AGENTS.md CHANGED
@@ -15,6 +15,8 @@ smrt db:migrate --postgres-safe # PostgreSQL concurrent-index mode (see below)
15
15
  smrt db:migrate --force-migration <exact-id> [--force-migration <exact-id>...] # Force exact generated migrations in one atomic batch
16
16
  smrt db:migrate-uuid # Convert schema-declared UUID text columns after data remap
17
17
  smrt db:migrate-int8 # Explicitly widen pre-#2373 int4 columns after preflight
18
+ smrt db:drop-framework-base-tables # One-time drop of the five #2644-orphaned framework-base tables
19
+ smrt db:drop-framework-base-tables --dry-run # Print the drop plan without executing
18
20
  smrt db:diff # Show schema differences without generating migration files
19
21
  smrt db:rollback # Roll back migrations by executing their recorded DOWN
20
22
  smrt db:rollback --mark-only # Record-only flip; schema deliberately untouched
@@ -154,6 +156,12 @@ to run a build.
154
156
 
155
157
  ## Live-schema parity (#2368)
156
158
 
159
+ PostgreSQL role contracts also surface through read-only `doctor --db` and
160
+ `db:validate`. Explicit repair uses `db:permissions` with a reviewed fingerprint;
161
+ ordinary migrations never change ACLs. See the
162
+ [permission guide](../../docs/content/postgres-permissions.md) for configuration,
163
+ authority boundaries, and recovery.
164
+
157
165
  `doctor --db` and `db:status --parity` share `src/commands/db-parity.ts`, which
158
166
  runs core's `checkLiveSchemaParity()`. This answers a different question than
159
167
  `db:status`/`db:diff`: those compare the live database to the **manifest**, i.e.
@@ -257,6 +265,64 @@ sessions/magic-link tokens/CLI-auth requests from `smrt-users`).
257
265
  - Deployments running a jobs `TaskRunner` already get the same sweep every six
258
266
  hours; this command is for those that do not, and for one-off operator runs.
259
267
 
268
+ ## `db:drop-framework-base-tables` is a one-time remediation (#2647)
269
+
270
+ `db:migrate` no longer plans `smrt_objects`/`smrt_classes`/`smrt_collections`/
271
+ `smrt_hierarchicals`/`smrt_polymorphic_associations` (#2644), but any
272
+ deployment that ran `db:migrate` before that fix still has all five. This
273
+ command, backed by `planFrameworkBaseTableDrop()` /
274
+ `dropFrameworkBaseTables()` in `@happyvertical/smrt-core/migrations`, drops
275
+ exactly those five hardcoded names and nothing else — **never** derived from
276
+ `SchemaDiff.orphan_tables`, which would recreate the dangerous global orphan
277
+ drop `includeDroppedTables` (default `false`, both call sites explicit —
278
+ `differ.ts:450`, `db-diff.ts:210`) deliberately refuses to be.
279
+
280
+ - Execute-or-refuse, like `db:rollback`: it refuses (non-zero exit, nothing
281
+ dropped) if any target table has rows, has anything but that table's own
282
+ expected columns and types, or is referenced by a foreign key anywhere in
283
+ the live database. A consumer's own unrelated table sharing one of these
284
+ five names is exactly what the shape check protects.
285
+ - The expected shape is **per table, not one universal baseline**:
286
+ `smrt_objects`/`smrt_classes`/`smrt_collections` are exactly `id`/`slug`/
287
+ `context`/`created_at`/`updated_at`, but `smrt_hierarchicals` also
288
+ requires `parent_id` and `smrt_polymorphic_associations` also requires
289
+ `meta_type`/`meta_id`/`role`/`sort_order` — `SmrtHierarchical`'s and
290
+ `SmrtPolymorphicAssociation`'s own real fields, confirmed against a
291
+ genuine pre-#2644 `db:migrate` run. A table under either of those two
292
+ names with *only* the plain five columns is refused as unexpected shape,
293
+ not treated as safe.
294
+ - Companion indexes are enumerated from each table's live schema (never
295
+ guessed). `smrt_hierarchicals`/`smrt_polymorphic_associations` do not
296
+ carry the `created_at` list-ordering index the other three do (also
297
+ empirically confirmed, not assumed).
298
+ - Bounded PostgreSQL execution reuses the same `SET LOCAL lock_timeout` /
299
+ `statement_timeout` pattern as every other DDL path (#2362). Every target
300
+ is locked `IN ACCESS EXCLUSIVE MODE` before re-checking it — a plain
301
+ `SELECT COUNT(*)` alone would not block a concurrent writer — and shape,
302
+ type, and emptiness are all re-verified inside that transaction
303
+ immediately before dropping anything, via raw `information_schema.columns`
304
+ / `PRAGMA table_info` (`getTableSchema()` is not available on the
305
+ transaction-scoped connection). Only `DROP TABLE` is ever executed —
306
+ never a `DROP INDEX` by name — so `DROP TABLE`'s own cascade removes the
307
+ table's indexes identity-safely instead of trusting a possibly-stale
308
+ planned index name.
309
+ - `--dry-run` prints the exact plan (which tables exist, their companion
310
+ indexes, and the DROP statements) without executing.
311
+ - Skipping it is safe: a table left behind is inert and permanently orphaned,
312
+ never written to or read from again. Documented, narrow residual risk, all
313
+ schema-integrity (never data loss — the target is always verified empty
314
+ first): DuckDB's adapter does not report foreign keys through
315
+ `getTableSchema()` at all; this module's reverse-FK scan (like every other
316
+ PostgreSQL schema tool in this package) only looks at the `public` schema;
317
+ and foreign keys are checked at planning time only, not re-scanned inside
318
+ the execution transaction. On PostgreSQL a foreign key that appeared after
319
+ planning is still caught there — `DROP TABLE` refuses when a real
320
+ dependent exists, dependency-based, empty parent or not. **This does not
321
+ hold on SQLite**: its FK enforcement on `DROP TABLE` is row-based, so an
322
+ empty parent (this command's precondition) drops cleanly past a real
323
+ foreign key, verified directly. The plan-time scan is the only gate on
324
+ SQLite/DuckDB, narrower than PostgreSQL's.
325
+
260
326
  ## Architecture
261
327
 
262
328
  - **Lazy command loading**: commands loaded on-demand via dynamic import (~100ms overhead on first use)
package/README.md CHANGED
@@ -47,10 +47,20 @@ The four snapshot options are atomic: supplying any one requires all four.
47
47
  | `smrt db:migrate` | Apply pending migrations |
48
48
  | `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 |
49
49
  | `smrt db:migrate-uuid` | Convert schema-declared UUID text columns to native PostgreSQL uuid after data has been remapped |
50
+ | `smrt db:migrate-int8` | Widen legacy pre-#2373 int4 columns to BIGINT after reviewing the maintenance-window preflight |
51
+ | `smrt db:drop-framework-base-tables` | One-time removal of the five framework-base tables (`smrt_objects`, `smrt_classes`, `smrt_collections`, `smrt_hierarchicals`, `smrt_polymorphic_associations`) orphaned by #2644; refuses if any target table has rows, an unexpected shape, or an inbound foreign key |
52
+ | `smrt db:drop-framework-base-tables --dry-run` | Print the drop plan (tables and companion indexes) without executing |
50
53
  | `smrt db:diff` | Show schema differences without generating migration files |
51
54
  | `smrt db:rollback` | Roll back the last migration by executing its recorded DOWN script; refuses when no DOWN script exists |
52
55
  | `smrt db:rollback --mark-only` | Record-only: mark migrations rolled back without running any DOWN script (schema untouched) |
53
56
  | `smrt db:history` | Show migration history with active-vs-superseded failure classification |
57
+ | `smrt db:permissions --dry-run` | Plan the declared PostgreSQL role permission contract |
58
+ | `smrt db:permissions --apply --expected-fingerprint <hash>` | Explicitly apply a reviewed permission plan |
59
+ | `smrt db:validate` | Validate configured PostgreSQL permissions or JSON database integrity |
60
+
61
+ For separate migration-owner, runtime, and monitoring roles, see the
62
+ [PostgreSQL permissions guide](../../docs/content/postgres-permissions.md).
63
+ `doctor --db` includes read-only permission diagnostics when configured.
54
64
 
55
65
  File-backed SQL/TypeScript migration generation is not supported. s-m-r-t schema
56
66
  migrations are manifest-driven; model schema with s-m-r-t objects and apply changes
@@ -1,3 +1,4 @@
1
+ import { t as DEFAULT_CLI_CONFIG } from "./config-DfBo4hHU.js";
1
2
  import { t as resolveManifestEntryPackageName } from "./index.js";
2
3
  import { createRequire } from "node:module";
3
4
  import * as fs from "node:fs";
@@ -11,6 +12,7 @@ import { access, cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
11
12
  import { createLogger } from "@happyvertical/logger";
12
13
  import glob from "fast-glob";
13
14
  import { createHash } from "node:crypto";
15
+ import { getPackageConfig } from "@happyvertical/smrt-config";
14
16
  import { toSnakeCase } from "@happyvertical/smrt-core/utils";
15
17
  import { discoverScopedPackageDirectories, readAgentModuleDocs, readPackageAgentDoc, resolveAgentModuleDocPaths } from "@happyvertical/smrt-core/knowledge";
16
18
  import { MCPGenerator } from "@happyvertical/smrt-core/generators";
@@ -20,7 +22,7 @@ import https from "node:https";
20
22
  import { homedir, tmpdir } from "node:os";
21
23
  import { extract } from "tar";
22
24
  import { importWorkspaceModule } from "@happyvertical/smrt-core/utils/import-workspace-module";
23
- import { buildIntegerWidthTableStatements, collectIntegerWidthTargets, parsePostgresTimeoutMs, preflightIntegerWidthWidening, widenIntegerColumnsToBigInt } from "@happyvertical/smrt-core/migrations";
25
+ import { FRAMEWORK_BASE_TABLE_NAMES, buildIntegerWidthTableStatements, collectIntegerWidthTargets, dropFrameworkBaseTables, parsePostgresTimeoutMs, planFrameworkBaseTableDrop, preflightIntegerWidthWidening, widenIntegerColumnsToBigInt } from "@happyvertical/smrt-core/migrations";
24
26
  import { buildArchitectureContext, buildKnowledgeIndex, buildReviewContext, checkKnowledgeFreshness, compactContextResult, diffKnowledgeIndex, renderFreshnessResult, renderKnowledgeIndexMarkdown } from "@happyvertical/smrt-dev-mcp/knowledge";
25
27
  //#region src/commands/db-command-utils.ts
26
28
  var SENSITIVE_QUERY_PARAMS = /* @__PURE__ */ new Set([
@@ -123,7 +125,7 @@ var configExportCommand = {
123
125
  process.exit(1);
124
126
  }
125
127
  const { getPackageConfig } = await import("@happyvertical/smrt-config");
126
- const { DEFAULT_CLI_CONFIG } = await import("./config-BwrFRL8L.js");
128
+ const { DEFAULT_CLI_CONFIG } = await import("./config-DfBo4hHU.js").then((n) => n.n);
127
129
  const config = getPackageConfig("cli", DEFAULT_CLI_CONFIG);
128
130
  if (!config.database?.url || config.database.url === ":memory:") {
129
131
  if (options.json) console.log(JSON.stringify({ error: "Database not configured" }));
@@ -962,7 +964,7 @@ var dbDiffCommand = {
962
964
  return;
963
965
  }
964
966
  const { getPackageConfig } = await import("@happyvertical/smrt-config");
965
- const { DEFAULT_CLI_CONFIG } = await import("./config-BwrFRL8L.js");
967
+ const { DEFAULT_CLI_CONFIG } = await import("./config-DfBo4hHU.js").then((n) => n.n);
966
968
  const config = getPackageConfig("cli", DEFAULT_CLI_CONFIG);
967
969
  if (!config.database?.url || config.database.url === ":memory:") {
968
970
  if (options.json) console.log(JSON.stringify({ error: "Database not configured" }));
@@ -1285,7 +1287,7 @@ var dbHistoryCommand = {
1285
1287
  let db;
1286
1288
  try {
1287
1289
  const { getPackageConfig } = await import("@happyvertical/smrt-config");
1288
- const { DEFAULT_CLI_CONFIG } = await import("./config-BwrFRL8L.js");
1290
+ const { DEFAULT_CLI_CONFIG } = await import("./config-DfBo4hHU.js").then((n) => n.n);
1289
1291
  const config = getPackageConfig("cli", DEFAULT_CLI_CONFIG);
1290
1292
  if (!config.database?.url || config.database.url === ":memory:") {
1291
1293
  if (options.json) console.log(JSON.stringify({ error: "Database not configured" }));
@@ -1456,6 +1458,109 @@ function formatDateTime(date) {
1456
1458
  return date.toISOString().replace("T", " ").substring(0, 19);
1457
1459
  }
1458
1460
  //#endregion
1461
+ //#region src/commands/db-permissions.ts
1462
+ /** The only mutating caller is the explicit permissions command. */
1463
+ async function runPostgresPermissions(options = {}, optional = false) {
1464
+ const config = getPackageConfig("cli", DEFAULT_CLI_CONFIG);
1465
+ const configured = config.postgresPermissions;
1466
+ const failure = (error) => ({
1467
+ skipped: false,
1468
+ plan: null,
1469
+ error
1470
+ });
1471
+ if (!configured && optional) return {
1472
+ skipped: true,
1473
+ plan: null,
1474
+ error: null
1475
+ };
1476
+ if (!configured) return failure("Configure postgresPermissions before inspecting or applying database permissions.");
1477
+ if (config.database?.type !== "postgres" || !config.database.url) return failure("PostgreSQL permissions require a configured PostgreSQL database.");
1478
+ if (options.apply && (options["dry-run"] || !options["expected-fingerprint"])) return failure("--apply requires --expected-fingerprint from a reviewed plan and cannot be combined with --dry-run.");
1479
+ let db;
1480
+ try {
1481
+ await autoDiscoverAndLoad();
1482
+ const { getDatabase } = await import("@happyvertical/sql");
1483
+ const { ObjectRegistry, getSystemTableShapes, planPostgresPermissions, applyPostgresPermissions } = await import("@happyvertical/smrt-core");
1484
+ db = await getDatabase({
1485
+ type: "postgres",
1486
+ url: config.database.url
1487
+ });
1488
+ const { rows } = await db.query("SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname = $1", [configured.schema]);
1489
+ const present = new Set(rows.map((row) => row.tablename));
1490
+ const managedTables = [.../* @__PURE__ */ new Set([
1491
+ ...Object.keys(ObjectRegistry.getAllSchemasAsDefinitions()),
1492
+ ...[...getSystemTableShapes("postgres").keys()].filter((table) => present.has(table)),
1493
+ ...configured.managedTables ?? []
1494
+ ])].sort();
1495
+ const contract = {
1496
+ ...configured,
1497
+ managedTables
1498
+ };
1499
+ return {
1500
+ skipped: false,
1501
+ plan: options.apply ? await applyPostgresPermissions(db, contract, { expectedFingerprint: options["expected-fingerprint"] ?? "" }) : await planPostgresPermissions(db, contract),
1502
+ error: null
1503
+ };
1504
+ } catch {
1505
+ return failure("PostgreSQL permissions could not be verified or applied. Check connectivity, contract validity, administrator authority, and the reviewed fingerprint.");
1506
+ } finally {
1507
+ try {
1508
+ await closeDatabaseConnection(db);
1509
+ } catch {}
1510
+ }
1511
+ }
1512
+ function formatPermissionsOutcome(outcome) {
1513
+ if (outcome.skipped) return [];
1514
+ if (outcome.error) return [`PostgreSQL permissions: ${outcome.error}`];
1515
+ const plan = outcome.plan;
1516
+ if (!plan) return ["PostgreSQL permissions could not be verified."];
1517
+ return [
1518
+ `PostgreSQL permissions: ${plan.diagnostics.length} finding(s)`,
1519
+ ...plan.diagnostics.map((finding) => ` ${finding.severity}: ${finding.role ? `${finding.role} · ` : ""}${finding.resource}: ${finding.message}`),
1520
+ `Plan fingerprint: ${plan.fingerprint}`,
1521
+ ...plan.limitations.map((limitation) => `Scope: ${limitation}`),
1522
+ ...plan.canApply ? [] : ["Automatic application refused; resolve unsupported conditions first."]
1523
+ ];
1524
+ }
1525
+ function outputPermissionsOutcome(outcome, json = false) {
1526
+ if (json) console.log(JSON.stringify(outcome, null, 2));
1527
+ else for (const line of formatPermissionsOutcome(outcome)) console.log(line);
1528
+ if (outcome.error || outcome.plan?.diagnostics.length) process.exitCode = 1;
1529
+ }
1530
+ var dbPermissionsCommand = {
1531
+ name: "db:permissions",
1532
+ description: "Inspect PostgreSQL role permissions; explicitly apply a reviewed plan",
1533
+ args: [],
1534
+ options: {
1535
+ "dry-run": {
1536
+ type: "boolean",
1537
+ description: "Read-only plan (the default)"
1538
+ },
1539
+ apply: {
1540
+ type: "boolean",
1541
+ description: "Apply the reviewed plan explicitly",
1542
+ default: false
1543
+ },
1544
+ "expected-fingerprint": {
1545
+ type: "string",
1546
+ description: "Fingerprint from the reviewed read-only plan"
1547
+ },
1548
+ json: {
1549
+ type: "boolean",
1550
+ description: "Output plan and diagnostics as JSON",
1551
+ default: false
1552
+ }
1553
+ },
1554
+ handler: async (_args, options) => {
1555
+ const outcome = await runPostgresPermissions(options);
1556
+ outputPermissionsOutcome(outcome, options.json);
1557
+ if (!options.json && outcome.plan && !options.apply) {
1558
+ console.log("Read-only plan SQL:");
1559
+ for (const sql of outcome.plan.statements) console.log(sql);
1560
+ }
1561
+ }
1562
+ };
1563
+ //#endregion
1459
1564
  //#region src/commands/db-prune.ts
1460
1565
  /**
1461
1566
  * Packages that contribute retention tasks by registering them on import.
@@ -1595,7 +1700,7 @@ var dbPruneCommand = {
1595
1700
  let db;
1596
1701
  try {
1597
1702
  const { getPackageConfig } = await import("@happyvertical/smrt-config");
1598
- const { DEFAULT_CLI_CONFIG } = await import("./config-BwrFRL8L.js");
1703
+ const { DEFAULT_CLI_CONFIG } = await import("./config-DfBo4hHU.js").then((n) => n.n);
1599
1704
  const config = getPackageConfig("cli", DEFAULT_CLI_CONFIG);
1600
1705
  if (!config.database?.url) {
1601
1706
  const message = "Database not configured. Set database.url in smrt.config.ts.";
@@ -1755,7 +1860,7 @@ var dbRollbackCommand = {
1755
1860
  const markOnly = options["mark-only"] ?? options.markOnly;
1756
1861
  try {
1757
1862
  const { getPackageConfig } = await import("@happyvertical/smrt-config");
1758
- const { DEFAULT_CLI_CONFIG } = await import("./config-BwrFRL8L.js");
1863
+ const { DEFAULT_CLI_CONFIG } = await import("./config-DfBo4hHU.js").then((n) => n.n);
1759
1864
  const config = getPackageConfig("cli", DEFAULT_CLI_CONFIG);
1760
1865
  if (!config.database?.url || config.database.url === ":memory:") {
1761
1866
  if (options.json) console.log(JSON.stringify({ error: "Database not configured" }));
@@ -2013,7 +2118,7 @@ async function runLiveSchemaParity(options = {}) {
2013
2118
  let db;
2014
2119
  try {
2015
2120
  const { getPackageConfig } = await import("@happyvertical/smrt-config");
2016
- const { DEFAULT_CLI_CONFIG } = await import("./config-BwrFRL8L.js");
2121
+ const { DEFAULT_CLI_CONFIG } = await import("./config-DfBo4hHU.js").then((n) => n.n);
2017
2122
  const config = getPackageConfig("cli", DEFAULT_CLI_CONFIG);
2018
2123
  if (!config.database?.url || config.database.url === ":memory:") return {
2019
2124
  report: null,
@@ -2552,7 +2657,7 @@ var dbStatusCommand = {
2552
2657
  let db;
2553
2658
  try {
2554
2659
  const { getPackageConfig } = await import("@happyvertical/smrt-config");
2555
- const { DEFAULT_CLI_CONFIG } = await import("./config-BwrFRL8L.js");
2660
+ const { DEFAULT_CLI_CONFIG } = await import("./config-DfBo4hHU.js").then((n) => n.n);
2556
2661
  const config = getPackageConfig("cli", DEFAULT_CLI_CONFIG);
2557
2662
  if (!config.database?.url || config.database.url === ":memory:") {
2558
2663
  if (options.json) console.log(JSON.stringify({ error: "Database not configured" }));
@@ -2814,7 +2919,7 @@ async function getDispatchBus() {
2814
2919
  const { createDispatchBus } = await import("@happyvertical/smrt-core");
2815
2920
  const { getPackageConfig } = await import("@happyvertical/smrt-config");
2816
2921
  const { getDatabase } = await import("@happyvertical/sql");
2817
- const { DEFAULT_CLI_CONFIG } = await import("./config-BwrFRL8L.js");
2922
+ const { DEFAULT_CLI_CONFIG } = await import("./config-DfBo4hHU.js").then((n) => n.n);
2818
2923
  const dbConfig = getPackageConfig("cli", DEFAULT_CLI_CONFIG).database;
2819
2924
  if (!dbConfig?.url) throw new Error("Database not configured. Set database.url in smrt.config.ts or use --db option.");
2820
2925
  let db;
@@ -3732,7 +3837,7 @@ var exportCommand = {
3732
3837
  let db;
3733
3838
  try {
3734
3839
  const { getConfig, getPackageConfig } = await import("@happyvertical/smrt-config");
3735
- const { DEFAULT_CLI_CONFIG } = await import("./config-BwrFRL8L.js");
3840
+ const { DEFAULT_CLI_CONFIG } = await import("./config-DfBo4hHU.js").then((n) => n.n);
3736
3841
  const smrtConfig = getConfig();
3737
3842
  const cliConfig = getPackageConfig("cli", DEFAULT_CLI_CONFIG);
3738
3843
  if (!smrtConfig) {
@@ -6140,6 +6245,122 @@ var playgroundCommands = {
6140
6245
  }
6141
6246
  };
6142
6247
  //#endregion
6248
+ //#region src/commands/db-drop-framework-base-tables.ts
6249
+ /**
6250
+ * db:drop-framework-base-tables Command
6251
+ *
6252
+ * One-time remediation for the five orphaned framework-base tables
6253
+ * (`smrt_objects`, `smrt_classes`, `smrt_collections`, `smrt_hierarchicals`,
6254
+ * `smrt_polymorphic_associations`) that a pre-#2644 `db:migrate` planned for
6255
+ * SMRT's abstract framework-base classes. #2644 stopped planning them; this
6256
+ * command drops the ones a deployment already created.
6257
+ *
6258
+ * Deliberately separate from `db:migrate` and from `db:diff`'s orphan-table
6259
+ * reporting: `SchemaDiff.includeDroppedTables` stays `false` so an ordinary
6260
+ * migration never drops a table just because it is absent from the manifest.
6261
+ * This command drops **only** the five hardcoded names above, and only after
6262
+ * verifying each one is empty, has no foreign keys pointing at it, and has
6263
+ * exactly that table's own expected columns — the universal `id`/`slug`/
6264
+ * `context`/`created_at`/`updated_at` base for `smrt_objects`/`smrt_classes`/
6265
+ * `smrt_collections`, extended with `parent_id` for `smrt_hierarchicals` and
6266
+ * `meta_type`/`meta_id`/`role`/`sort_order` for
6267
+ * `smrt_polymorphic_associations` (their own real fields, not the plain
6268
+ * baseline — see `@happyvertical/smrt-core/migrations`'
6269
+ * `planFrameworkBaseTableDrop()` for the full per-table safety model).
6270
+ *
6271
+ * Skipping this command is safe: a table left behind here is inert and
6272
+ * permanently orphaned, never written to or read from again.
6273
+ */
6274
+ var DEFAULT_POSTGRES_LOCK_TIMEOUT_MS$1 = 3e4;
6275
+ var DEFAULT_POSTGRES_STATEMENT_TIMEOUT_MS$1 = 6e4;
6276
+ function describeRefusal(refusal) {
6277
+ switch (refusal.kind) {
6278
+ case "not-empty": return `has ${refusal.rowCount} row(s) — nothing should ever have written to this table`;
6279
+ case "unexpected-shape": {
6280
+ const parts = [];
6281
+ if (refusal.extraColumns.length > 0) parts.push(`unexpected column(s) [${refusal.extraColumns.join(", ")}]`);
6282
+ if (refusal.missingColumns.length > 0) parts.push(`missing baseline column(s) [${refusal.missingColumns.join(", ")}]`);
6283
+ return `has an unexpected shape: ${parts.join("; ")} (actual columns: ${refusal.actualColumns.join(", ")})`;
6284
+ }
6285
+ case "unexpected-column-type": return `has an unexpected column type: ${refusal.mismatches.map((mismatch) => `${mismatch.column} is "${mismatch.actualType}" (expected ${mismatch.expectedBuckets.join(" or ")})`).join(", ")}`;
6286
+ case "referenced-by-foreign-key": return `is referenced by foreign key(s) from: ${refusal.references.map((reference) => `${reference.table}.${reference.column}`).join(", ")}`;
6287
+ case "introspection-unavailable": return refusal.reason;
6288
+ default: return "is unsafe to drop for an unrecognized reason";
6289
+ }
6290
+ }
6291
+ var dbDropFrameworkBaseTablesCommand = {
6292
+ name: "db:drop-framework-base-tables",
6293
+ description: "One-time removal of the five framework-base tables orphaned by #2644 (smrt_objects, smrt_classes, smrt_collections, smrt_hierarchicals, smrt_polymorphic_associations). Refuses if any target table has rows, an unexpected shape, or an inbound foreign key.",
6294
+ aliases: ["drop-framework-base-tables", "db-drop-framework-base-tables"],
6295
+ args: [],
6296
+ options: {
6297
+ "dry-run": {
6298
+ type: "boolean",
6299
+ description: "Print the plan without dropping anything.",
6300
+ default: false
6301
+ },
6302
+ verbose: {
6303
+ type: "boolean",
6304
+ description: "Print a per-table report, including tables that are not present.",
6305
+ default: false,
6306
+ short: "v"
6307
+ }
6308
+ },
6309
+ handler: async (_args, options) => {
6310
+ let db;
6311
+ const dryRun = Boolean(options["dry-run"]);
6312
+ try {
6313
+ const { getPackageConfig } = await import("@happyvertical/smrt-config");
6314
+ const { DEFAULT_CLI_CONFIG } = await import("./config-DfBo4hHU.js").then((n) => n.n);
6315
+ const config = getPackageConfig("cli", DEFAULT_CLI_CONFIG);
6316
+ if (!config.database?.url || config.database.url === ":memory:") throw new Error("Database configuration required for db:drop-framework-base-tables. Configure database.url in smrt.config.ts (or DATABASE_URL).");
6317
+ const dbType = config.database.type || "sqlite";
6318
+ const dbUrl = config.database.url;
6319
+ const { getDatabase } = await import("@happyvertical/sql");
6320
+ db = await getDatabase({
6321
+ type: dbType,
6322
+ url: dbUrl
6323
+ });
6324
+ console.log("\n🗑️ Framework base-table remediation (#2647)\n");
6325
+ console.log(`✓ Connected to ${formatDatabaseDisplayUrl(dbType, dbUrl)}\n`);
6326
+ const plan = await planFrameworkBaseTableDrop(db, { engineHint: dbType });
6327
+ const present = plan.tables.filter((table) => table.exists);
6328
+ const absent = plan.tables.filter((table) => !table.exists);
6329
+ if (options.verbose || present.length === 0) for (const table of absent) console.log(` - ${table.table}: not present, nothing to do`);
6330
+ for (const table of present) {
6331
+ if (table.refusals.length === 0) {
6332
+ console.log(` - ${table.table}: safe to drop (${table.indexNames.length} companion index(es))`);
6333
+ continue;
6334
+ }
6335
+ console.log(` - ${table.table}: UNSAFE`);
6336
+ for (const refusal of table.refusals) console.log(` ${describeRefusal(refusal)}`);
6337
+ }
6338
+ if (!plan.safe) throw new Error(`Refusing to drop framework base tables — ${FRAMEWORK_BASE_TABLE_NAMES.length} target name(s) checked, at least one is unsafe (see above). Nothing was dropped.`);
6339
+ if (present.length === 0) {
6340
+ console.log("\nNone of the five framework-base tables are present. Nothing to do.\n");
6341
+ return;
6342
+ }
6343
+ console.log(`\n${dryRun ? "DRY RUN — would execute" : "Applying"} ${plan.statements.length} statement(s):`);
6344
+ for (const statement of plan.statements) console.log(` ${statement};`);
6345
+ if (dryRun) {
6346
+ console.log("\nDry run complete — no changes applied.\n");
6347
+ return;
6348
+ }
6349
+ const postgresMigrationConfig = config.migrations?.postgres;
6350
+ const result = await dropFrameworkBaseTables(db, plan, {
6351
+ lockTimeout: parsePostgresTimeoutMs(postgresMigrationConfig?.lockTimeout, DEFAULT_POSTGRES_LOCK_TIMEOUT_MS$1),
6352
+ statementTimeout: parsePostgresTimeoutMs(postgresMigrationConfig?.statementTimeout, DEFAULT_POSTGRES_STATEMENT_TIMEOUT_MS$1)
6353
+ });
6354
+ console.log(`\n✓ Dropped ${result.droppedTables.length} table(s) and ${result.droppedIndexes.length} companion index(es): ${result.droppedTables.map((table) => quoteIdentifier(table)).join(", ")}\n`);
6355
+ } catch (error) {
6356
+ console.error(`\n❌ Framework base-table remediation failed: ${error instanceof Error ? error.message : String(error)}\n`);
6357
+ process.exitCode = 1;
6358
+ } finally {
6359
+ await closeDatabaseConnection(db);
6360
+ }
6361
+ }
6362
+ };
6363
+ //#endregion
6143
6364
  //#region src/commands/db-migrate-int8.ts
6144
6365
  /**
6145
6366
  * db:migrate-int8 Command
@@ -6175,7 +6396,7 @@ var dbMigrateInt8Command = {
6175
6396
  const dryRun = Boolean(options["dry-run"]);
6176
6397
  try {
6177
6398
  const { getPackageConfig } = await import("@happyvertical/smrt-config");
6178
- const { DEFAULT_CLI_CONFIG } = await import("./config-BwrFRL8L.js");
6399
+ const { DEFAULT_CLI_CONFIG } = await import("./config-DfBo4hHU.js").then((n) => n.n);
6179
6400
  const config = getPackageConfig("cli", DEFAULT_CLI_CONFIG);
6180
6401
  if (!config.database?.url || config.database.url === ":memory:") throw new Error("Database configuration required for db:migrate-int8. Configure database.url in smrt.config.ts (or DATABASE_URL).");
6181
6402
  await autoDiscoverAndLoad();
@@ -6429,7 +6650,7 @@ var dbMigrateUuidCommand = {
6429
6650
  let db;
6430
6651
  try {
6431
6652
  const { getPackageConfig } = await import("@happyvertical/smrt-config");
6432
- const { DEFAULT_CLI_CONFIG } = await import("./config-BwrFRL8L.js");
6653
+ const { DEFAULT_CLI_CONFIG } = await import("./config-DfBo4hHU.js").then((n) => n.n);
6433
6654
  const config = getPackageConfig("cli", DEFAULT_CLI_CONFIG);
6434
6655
  if (!config.database?.url || config.database.url === ":memory:") {
6435
6656
  console.error("\n❌ Database configuration required for db:migrate-uuid");
@@ -7916,7 +8137,7 @@ export default testManifest;
7916
8137
  let db;
7917
8138
  try {
7918
8139
  const { getPackageConfig } = await import("@happyvertical/smrt-config");
7919
- const { DEFAULT_CLI_CONFIG } = await import("./config-BwrFRL8L.js");
8140
+ const { DEFAULT_CLI_CONFIG } = await import("./config-DfBo4hHU.js").then((n) => n.n);
7920
8141
  const config = getPackageConfig("cli", DEFAULT_CLI_CONFIG);
7921
8142
  if (!config.database?.url || config.database.url === ":memory:") {
7922
8143
  console.error("❌ Database configuration required for db:setup");
@@ -8124,7 +8345,7 @@ export default testManifest;
8124
8345
  },
8125
8346
  "db:validate": {
8126
8347
  name: "db:validate",
8127
- description: "Validate JSON database integrity against manifest schema",
8348
+ description: "Validate JSON database integrity or configured PostgreSQL permissions",
8128
8349
  aliases: ["json:validate", "validate-db"],
8129
8350
  args: [],
8130
8351
  options: {
@@ -8159,8 +8380,18 @@ export default testManifest;
8159
8380
  },
8160
8381
  handler: async (_args, options) => {
8161
8382
  const startTime = Date.now();
8383
+ const { getPackageConfig } = await import("@happyvertical/smrt-config");
8384
+ const { DEFAULT_CLI_CONFIG } = await import("./config-DfBo4hHU.js").then((n) => n.n);
8385
+ const config = getPackageConfig("cli", DEFAULT_CLI_CONFIG);
8386
+ if (!options.data && config.database?.type === "postgres") {
8387
+ const permissions = await runPostgresPermissions({}, true);
8388
+ if (!permissions.skipped) {
8389
+ outputPermissionsOutcome(permissions, options.json);
8390
+ return;
8391
+ }
8392
+ }
8162
8393
  try {
8163
- const { resolveDataPath, discoverJsonFiles, JsonDatabaseValidator, displayValidationResults } = await import("./json-validator-D5hN4DGj.js");
8394
+ const { resolveDataPath, discoverJsonFiles, JsonDatabaseValidator, displayValidationResults } = await import("./json-validator-CjX8fHx9.js");
8164
8395
  const dataPath = await resolveDataPath(options.data);
8165
8396
  if (!dataPath) {
8166
8397
  console.error("\n❌ Could not find data directory");
@@ -8332,7 +8563,7 @@ export default testManifest;
8332
8563
  const postgresTimestampMigration = resolvePostgresTimestampMigration(options["postgres-timestamp-legacy-timezone"]);
8333
8564
  const isDryRun = options["dry-run"] ?? false;
8334
8565
  const { getPackageConfig } = await import("@happyvertical/smrt-config");
8335
- const { DEFAULT_CLI_CONFIG } = await import("./config-BwrFRL8L.js");
8566
+ const { DEFAULT_CLI_CONFIG } = await import("./config-DfBo4hHU.js").then((n) => n.n);
8336
8567
  const config = getPackageConfig("cli", DEFAULT_CLI_CONFIG);
8337
8568
  if (!config.database?.url || config.database.url === ":memory:") {
8338
8569
  console.error("❌ Database configuration required for db:migrate");
@@ -9049,7 +9280,7 @@ export default testManifest;
9049
9280
  check("src/lib/server/smrt.ts exists", existsSync(resolve(cwd, "src/lib/server/smrt.ts")), "Missing server config - run: smrt init");
9050
9281
  try {
9051
9282
  const { getPackageConfig } = await import("@happyvertical/smrt-config");
9052
- const { DEFAULT_CLI_CONFIG } = await import("./config-BwrFRL8L.js");
9283
+ const { DEFAULT_CLI_CONFIG } = await import("./config-DfBo4hHU.js").then((n) => n.n);
9053
9284
  const config = getPackageConfig("cli", DEFAULT_CLI_CONFIG);
9054
9285
  if (config.database?.url && config.database.url !== ":memory:") check(`Database configured (${config.database.type || "sqlite"})`, true);
9055
9286
  else check("Database configured", false, void 0, "Using in-memory database - set DATABASE_URL for persistence");
@@ -9075,6 +9306,9 @@ export default testManifest;
9075
9306
  for (const finding of parityWarnings) warnings.push(`live schema: ${finding.table}${finding.target ? `.${finding.target}` : ""} (${finding.kind})`);
9076
9307
  if (parityErrors.length === 0 && parityWarnings.length === 0) passed.push("Live schema parity");
9077
9308
  }
9309
+ const permissions = await runPostgresPermissions({}, true);
9310
+ for (const line of formatPermissionsOutcome(permissions)) console.log(line);
9311
+ if (!permissions.skipped) check("PostgreSQL role permissions", !permissions.error && permissions.plan?.diagnostics.length === 0, permissions.error ?? "PostgreSQL role permissions differ from the configured contract");
9078
9312
  console.log();
9079
9313
  }
9080
9314
  console.log("━".repeat(50));
@@ -9109,7 +9343,9 @@ export default testManifest;
9109
9343
  "db:generate": dbGenerateCommand,
9110
9344
  "db:migrate-uuid": dbMigrateUuidCommand,
9111
9345
  "db:migrate-int8": dbMigrateInt8Command,
9346
+ "db:drop-framework-base-tables": dbDropFrameworkBaseTablesCommand,
9112
9347
  "db:prune": dbPruneCommand,
9348
+ "db:permissions": dbPermissionsCommand,
9113
9349
  "config:export": configExportCommand,
9114
9350
  export: exportCommand
9115
9351
  };
@@ -1,4 +1,18 @@
1
+ import "node:module";
2
+ //#region \0rolldown/runtime.js
3
+ var __defProp = Object.defineProperty;
4
+ var __exportAll = (all, no_symbols) => {
5
+ let target = {};
6
+ for (var name in all) __defProp(target, name, {
7
+ get: all[name],
8
+ enumerable: true
9
+ });
10
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
11
+ return target;
12
+ };
13
+ //#endregion
1
14
  //#region src/config.ts
15
+ var config_exports = /* @__PURE__ */ __exportAll({ DEFAULT_CLI_CONFIG: () => DEFAULT_CLI_CONFIG });
2
16
  /**
3
17
  * Default CLI configuration values
4
18
  */
@@ -29,4 +43,4 @@ var DEFAULT_CLI_CONFIG = {
29
43
  interactive: !process.env.CI
30
44
  };
31
45
  //#endregion
32
- export { DEFAULT_CLI_CONFIG };
46
+ export { config_exports as n, DEFAULT_CLI_CONFIG as t };
package/dist/index.js CHANGED
@@ -49,63 +49,63 @@ var _playgroundCommands = null;
49
49
  var _workbenchCommands = null;
50
50
  async function getGnodeCommands() {
51
51
  if (!_gnodeCommands) {
52
- const { gnodeCommands } = await import("./commands-COd77prj.js");
52
+ const { gnodeCommands } = await import("./commands-C4xB-FxS.js");
53
53
  _gnodeCommands = gnodeCommands;
54
54
  }
55
55
  return _gnodeCommands;
56
56
  }
57
57
  async function getGitCommands() {
58
58
  if (!_gitCommands) {
59
- const { gitCommands } = await import("./commands-COd77prj.js");
59
+ const { gitCommands } = await import("./commands-C4xB-FxS.js");
60
60
  _gitCommands = gitCommands;
61
61
  }
62
62
  return _gitCommands;
63
63
  }
64
64
  async function getGenerateCommands() {
65
65
  if (!_generateCommands) {
66
- const { generateCommands } = await import("./commands-COd77prj.js");
66
+ const { generateCommands } = await import("./commands-C4xB-FxS.js");
67
67
  _generateCommands = generateCommands;
68
68
  }
69
69
  return _generateCommands;
70
70
  }
71
71
  async function getInitCommands() {
72
72
  if (!_initCommands) {
73
- const { initCommands } = await import("./commands-COd77prj.js");
73
+ const { initCommands } = await import("./commands-C4xB-FxS.js");
74
74
  _initCommands = initCommands;
75
75
  }
76
76
  return _initCommands;
77
77
  }
78
78
  async function getUtilityCommands() {
79
79
  if (!_utilityCommands) {
80
- const { utilityCommands } = await import("./commands-COd77prj.js");
80
+ const { utilityCommands } = await import("./commands-C4xB-FxS.js");
81
81
  _utilityCommands = utilityCommands;
82
82
  }
83
83
  return _utilityCommands;
84
84
  }
85
85
  async function getDispatchCommands() {
86
86
  if (!_dispatchCommands) {
87
- const { dispatchCommands } = await import("./commands-COd77prj.js");
87
+ const { dispatchCommands } = await import("./commands-C4xB-FxS.js");
88
88
  _dispatchCommands = dispatchCommands;
89
89
  }
90
90
  return _dispatchCommands;
91
91
  }
92
92
  async function getDocsCommands() {
93
93
  if (!_docsCommands) {
94
- const { docsCommands } = await import("./commands-COd77prj.js");
94
+ const { docsCommands } = await import("./commands-C4xB-FxS.js");
95
95
  _docsCommands = docsCommands;
96
96
  }
97
97
  return _docsCommands;
98
98
  }
99
99
  async function getPlaygroundCommands() {
100
100
  if (!_playgroundCommands) {
101
- const { playgroundCommands } = await import("./commands-COd77prj.js");
101
+ const { playgroundCommands } = await import("./commands-C4xB-FxS.js");
102
102
  _playgroundCommands = playgroundCommands;
103
103
  }
104
104
  return _playgroundCommands;
105
105
  }
106
106
  async function getWorkbenchCommands() {
107
107
  if (!_workbenchCommands) {
108
- const { workbenchCommands } = await import("./commands-COd77prj.js");
108
+ const { workbenchCommands } = await import("./commands-C4xB-FxS.js");
109
109
  _workbenchCommands = workbenchCommands;
110
110
  }
111
111
  return _workbenchCommands;
@@ -322,7 +322,7 @@ var CLIGenerator = class {
322
322
  if (verbose) console.log("[CLI] Loading external classes...");
323
323
  await this.loadExternalClasses();
324
324
  const { getPackageConfig } = await import("@happyvertical/smrt-config");
325
- const { DEFAULT_CLI_CONFIG } = await import("./config-BwrFRL8L.js");
325
+ const { DEFAULT_CLI_CONFIG } = await import("./config-DfBo4hHU.js").then((n) => n.n);
326
326
  const config = getPackageConfig("cli", DEFAULT_CLI_CONFIG);
327
327
  const registeredCount = ObjectRegistry.getAllClasses().size;
328
328
  if (verbose || config.verbose) console.log(`[CLI] Successfully loaded ${registeredCount} SMRT objects`);
@@ -337,7 +337,7 @@ var CLIGenerator = class {
337
337
  */
338
338
  async loadLocalClasses() {
339
339
  const { getPackageConfig } = await import("@happyvertical/smrt-config");
340
- const { DEFAULT_CLI_CONFIG } = await import("./config-BwrFRL8L.js");
340
+ const { DEFAULT_CLI_CONFIG } = await import("./config-DfBo4hHU.js").then((n) => n.n);
341
341
  const fs = await import("node:fs");
342
342
  const path = await import("node:path");
343
343
  const config = getPackageConfig("cli", DEFAULT_CLI_CONFIG);
@@ -382,7 +382,7 @@ var CLIGenerator = class {
382
382
  */
383
383
  async loadExternalClasses() {
384
384
  const { getPackageConfig } = await import("@happyvertical/smrt-config");
385
- const { DEFAULT_CLI_CONFIG } = await import("./config-BwrFRL8L.js");
385
+ const { DEFAULT_CLI_CONFIG } = await import("./config-DfBo4hHU.js").then((n) => n.n);
386
386
  const { loadLocalTestManifestSync } = await import("@happyvertical/smrt-core/manifest");
387
387
  const fs = await import("node:fs");
388
388
  const path = await import("node:path");
@@ -1359,7 +1359,7 @@ var CLIGenerator = class {
1359
1359
  const { getConfig, getPackageConfig, getModuleConfig } = await import("@happyvertical/smrt-config");
1360
1360
  const smrtConfig = getConfig() || {};
1361
1361
  const moduleConfig = getModuleConfig(objectName.toLowerCase(), {});
1362
- const { DEFAULT_CLI_CONFIG } = await import("./config-BwrFRL8L.js");
1362
+ const { DEFAULT_CLI_CONFIG } = await import("./config-DfBo4hHU.js").then((n) => n.n);
1363
1363
  const cliConfig = getPackageConfig("cli", DEFAULT_CLI_CONFIG);
1364
1364
  let db = this.context.db;
1365
1365
  if (!db && cliConfig?.database?.url) {
@@ -1462,7 +1462,7 @@ var CLIGenerator = class {
1462
1462
  let db = this.context.db;
1463
1463
  if (!db) {
1464
1464
  const { getPackageConfig } = await import("@happyvertical/smrt-config");
1465
- const { DEFAULT_CLI_CONFIG } = await import("./config-BwrFRL8L.js");
1465
+ const { DEFAULT_CLI_CONFIG } = await import("./config-DfBo4hHU.js").then((n) => n.n);
1466
1466
  const config = getPackageConfig("cli", DEFAULT_CLI_CONFIG);
1467
1467
  const { getDatabase } = await import("@happyvertical/sql");
1468
1468
  db = await getDatabase({
@@ -85,7 +85,7 @@ async function resolveDataPath(explicitPath) {
85
85
  return resolved;
86
86
  }
87
87
  try {
88
- const { DEFAULT_CLI_CONFIG: defaultConfig } = await import("./config-BwrFRL8L.js");
88
+ const { DEFAULT_CLI_CONFIG: defaultConfig } = await import("./config-DfBo4hHU.js").then((n) => n.n);
89
89
  const config = getPackageConfig("cli", defaultConfig);
90
90
  if (config.database?.url) {
91
91
  const dbUrl = config.database.url;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-cli",
3
- "version": "0.45.3",
3
+ "version": "0.46.0",
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.45.3",
36
- "@happyvertical/smrt-config": "0.45.3",
37
- "@happyvertical/smrt-core": "0.45.3",
38
- "@happyvertical/smrt-dev-mcp": "0.45.3",
39
- "@happyvertical/smrt-types": "0.45.3",
40
- "@happyvertical/smrt-playground": "0.45.3"
35
+ "@happyvertical/smrt-core": "0.46.0",
36
+ "@happyvertical/smrt-playground": "0.46.0",
37
+ "@happyvertical/smrt-agents": "0.46.0",
38
+ "@happyvertical/smrt-types": "0.46.0",
39
+ "@happyvertical/smrt-config": "0.46.0",
40
+ "@happyvertical/smrt-dev-mcp": "0.46.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "24.13.2",