@objectstack/cli 10.0.0 → 10.3.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 (66) hide show
  1. package/dist/commands/compile.d.ts.map +1 -1
  2. package/dist/commands/compile.js +34 -2
  3. package/dist/commands/compile.js.map +1 -1
  4. package/dist/commands/doctor.js +1 -1
  5. package/dist/commands/doctor.js.map +1 -1
  6. package/dist/commands/lint.js +1 -1
  7. package/dist/commands/lint.js.map +1 -1
  8. package/dist/commands/migrate/apply.d.ts +19 -0
  9. package/dist/commands/migrate/apply.d.ts.map +1 -0
  10. package/dist/commands/migrate/apply.js +155 -0
  11. package/dist/commands/migrate/apply.js.map +1 -0
  12. package/dist/commands/migrate/index.d.ts +9 -0
  13. package/dist/commands/migrate/index.d.ts.map +1 -0
  14. package/dist/commands/migrate/index.js +10 -0
  15. package/dist/commands/migrate/index.js.map +1 -0
  16. package/dist/commands/migrate/plan.d.ts +16 -0
  17. package/dist/commands/migrate/plan.d.ts.map +1 -0
  18. package/dist/commands/migrate/plan.js +93 -0
  19. package/dist/commands/migrate/plan.js.map +1 -0
  20. package/dist/commands/package/publish.js +4 -4
  21. package/dist/commands/serve.d.ts.map +1 -1
  22. package/dist/commands/serve.js +74 -98
  23. package/dist/commands/serve.js.map +1 -1
  24. package/dist/commands/start.d.ts.map +1 -1
  25. package/dist/commands/start.js +10 -0
  26. package/dist/commands/start.js.map +1 -1
  27. package/dist/commands/validate.d.ts.map +1 -1
  28. package/dist/commands/validate.js +70 -3
  29. package/dist/commands/validate.js.map +1 -1
  30. package/dist/index.d.ts +3 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +4 -0
  33. package/dist/index.js.map +1 -1
  34. package/dist/utils/schema-migrate.d.ts +26 -0
  35. package/dist/utils/schema-migrate.d.ts.map +1 -0
  36. package/dist/utils/schema-migrate.integration.test.d.ts +2 -0
  37. package/dist/utils/schema-migrate.integration.test.d.ts.map +1 -0
  38. package/dist/utils/schema-migrate.integration.test.js +83 -0
  39. package/dist/utils/schema-migrate.integration.test.js.map +1 -0
  40. package/dist/utils/schema-migrate.js +123 -0
  41. package/dist/utils/schema-migrate.js.map +1 -0
  42. package/package.json +48 -47
  43. package/dist/commands/publish.d.ts +0 -17
  44. package/dist/commands/publish.d.ts.map +0 -1
  45. package/dist/commands/publish.js +0 -135
  46. package/dist/commands/publish.js.map +0 -1
  47. package/dist/commands/rollback.d.ts +0 -13
  48. package/dist/commands/rollback.d.ts.map +0 -1
  49. package/dist/commands/rollback.js +0 -77
  50. package/dist/commands/rollback.js.map +0 -1
  51. package/dist/utils/validate-expressions.d.ts +0 -19
  52. package/dist/utils/validate-expressions.d.ts.map +0 -1
  53. package/dist/utils/validate-expressions.js +0 -201
  54. package/dist/utils/validate-expressions.js.map +0 -1
  55. package/dist/utils/validate-expressions.test.d.ts +0 -2
  56. package/dist/utils/validate-expressions.test.d.ts.map +0 -1
  57. package/dist/utils/validate-expressions.test.js +0 -320
  58. package/dist/utils/validate-expressions.test.js.map +0 -1
  59. package/dist/utils/validate-widget-bindings.d.ts +0 -74
  60. package/dist/utils/validate-widget-bindings.d.ts.map +0 -1
  61. package/dist/utils/validate-widget-bindings.js +0 -304
  62. package/dist/utils/validate-widget-bindings.js.map +0 -1
  63. package/dist/utils/validate-widget-bindings.test.d.ts +0 -2
  64. package/dist/utils/validate-widget-bindings.test.d.ts.map +0 -1
  65. package/dist/utils/validate-widget-bindings.test.js +0 -285
  66. package/dist/utils/validate-widget-bindings.test.js.map +0 -1
@@ -0,0 +1,93 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+ import { Command, Flags } from '@oclif/core';
3
+ import chalk from 'chalk';
4
+ import { printHeader, printSuccess, printWarning, printError, printInfo, printStep, createTimer, } from '../../utils/format.js';
5
+ import { bootSchemaStack, renderPlan, summarize } from '../../utils/schema-migrate.js';
6
+ /**
7
+ * `os migrate plan` — dry-run diff of metadata vs the physical database,
8
+ * categorised safe / needs-confirm / destructive (issue #2186). Never mutates
9
+ * the schema.
10
+ */
11
+ export default class MigratePlan extends Command {
12
+ static description = 'Show how the physical database has drifted from metadata (dry run; no changes applied)';
13
+ static examples = [
14
+ '$ os migrate plan',
15
+ '$ os migrate plan --json',
16
+ '$ os migrate plan --database-url postgres://localhost/app',
17
+ ];
18
+ static flags = {
19
+ 'database-url': Flags.string({
20
+ description: 'Database URL to inspect (defaults to $OS_DATABASE_URL / the project DB)',
21
+ env: 'OS_DATABASE_URL',
22
+ }),
23
+ json: Flags.boolean({ description: 'Output as JSON' }),
24
+ };
25
+ async run() {
26
+ const { flags } = await this.parse(MigratePlan);
27
+ const timer = createTimer();
28
+ if (!flags.json) {
29
+ printHeader('Migrate · plan');
30
+ printStep('Booting schema stack…');
31
+ }
32
+ let stack;
33
+ try {
34
+ stack = await bootSchemaStack({ databaseUrl: flags['database-url'] });
35
+ }
36
+ catch (error) {
37
+ if (flags.json) {
38
+ console.log(JSON.stringify({ error: error.message }));
39
+ this.exit(1);
40
+ }
41
+ printError(error.message || String(error));
42
+ this.exit(1);
43
+ return;
44
+ }
45
+ try {
46
+ if (!stack.driver) {
47
+ if (flags.json) {
48
+ console.log(JSON.stringify({ error: 'no_sql_driver', changes: [] }));
49
+ return;
50
+ }
51
+ printWarning('Schema migration is only supported on SQL drivers (SQLite / Postgres). No SQL driver is active.');
52
+ return;
53
+ }
54
+ const drift = await stack.driver.detectManagedDrift();
55
+ if (flags.json) {
56
+ console.log(JSON.stringify({
57
+ database: stack.dbLabel,
58
+ managedTables: stack.managedTableCount,
59
+ total: drift.length,
60
+ changes: drift,
61
+ duration: timer.elapsed(),
62
+ }, null, 2));
63
+ return;
64
+ }
65
+ printInfo(`Database: ${chalk.white(stack.dbLabel)}`);
66
+ printInfo(`Examined ${chalk.white(String(stack.managedTableCount))} managed table(s).`);
67
+ console.log('');
68
+ if (drift.length === 0) {
69
+ printSuccess('Physical schema is in sync with metadata — nothing to migrate.');
70
+ console.log('');
71
+ return;
72
+ }
73
+ renderPlan(drift);
74
+ printInfo(summarize(drift));
75
+ console.log(chalk.dim(' Apply with: ') + chalk.white('os migrate apply') +
76
+ chalk.dim(' (add --allow-destructive for drops / tightenings)'));
77
+ console.log(chalk.dim(` ${timer.display()}`));
78
+ console.log('');
79
+ }
80
+ catch (error) {
81
+ if (flags.json) {
82
+ console.log(JSON.stringify({ error: error.message }));
83
+ this.exit(1);
84
+ }
85
+ printError(error.message || String(error));
86
+ this.exit(1);
87
+ }
88
+ finally {
89
+ await stack.shutdown();
90
+ }
91
+ }
92
+ }
93
+ //# sourceMappingURL=plan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan.js","sourceRoot":"","sources":["../../../src/commands/migrate/plan.ts"],"names":[],"mappings":"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"}
@@ -11,10 +11,10 @@
11
11
  * into sys_package_version.manifest_json (status=published).
12
12
  * 3. (optional) auto-install into a target environment via --env.
13
13
  *
14
- * This is the "upload my local code to my org" path. It does NOT write
15
- * sys_environment_revision (that's the legacy `objectstack publish` path,
16
- * which still exists for backward compatibility while ADR-0006 v4 Phase B
17
- * transitions complete).
14
+ * This is the "upload my local code to my org" path the single supported
15
+ * way to publish. (The legacy direct-to-environment `os publish` / `os
16
+ * rollback` commands, which wrote sys_environment_revision, have been
17
+ * removed.)
18
18
  */
19
19
  import { readFile } from 'node:fs/promises';
20
20
  import { resolve as resolvePath, basename, dirname, isAbsolute } from 'node:path';
@@ -1 +1 @@
1
- {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/commands/serve.ts"],"names":[],"mappings":"AAEA,OAAO,EAAQ,OAAO,EAAS,MAAM,aAAa,CAAC;AAyInD,MAAM,CAAC,OAAO,OAAO,KAAM,SAAQ,OAAO;IACxC,OAAgB,WAAW,SAAkM;IAE7N,OAAgB,IAAI;;MAElB;IAEF,OAAgB,KAAK;;;;;;;;;;MAoBnB;IAEF;;;;;;;;;;;;;;;;;OAiBG;IACH,MAAM,CAAC,QAAQ,CAAC,sBAAsB,EAAE,SAAS,MAAM,EAAE,CAQtD;IAEH;;;;OAIG;IACH,MAAM,CAAC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAIpD;IAEI,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;CAi2D3B"}
1
+ {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/commands/serve.ts"],"names":[],"mappings":"AAEA,OAAO,EAAQ,OAAO,EAAS,MAAM,aAAa,CAAC;AAyInD,MAAM,CAAC,OAAO,OAAO,KAAM,SAAQ,OAAO;IACxC,OAAgB,WAAW,SAAkM;IAE7N,OAAgB,IAAI;;MAElB;IAEF,OAAgB,KAAK;;;;;;;;;;MAoBnB;IAEF;;;;;;;;;;;;;;;;;OAiBG;IACH,MAAM,CAAC,QAAQ,CAAC,sBAAsB,EAAE,SAAS,MAAM,EAAE,CAQtD;IAEH;;;;OAIG;IACH,MAAM,CAAC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAIpD;IAEI,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;CA80D3B"}
@@ -412,7 +412,7 @@ export default class Serve extends Command {
412
412
  // "missing artifact" error and assemble a bare kernel that
413
413
  // can later install marketplace apps at runtime.
414
414
  const { createDefaultHostConfig } = await import('@objectstack/runtime');
415
- const bootResult = await createDefaultHostConfig({ requireArtifact: !useEmptyBoot });
415
+ const bootResult = await createDefaultHostConfig({ requireArtifact: !useEmptyBoot, dev: isDev });
416
416
  config = { ...originalConfig, ...bootResult };
417
417
  }
418
418
  else if (resolvedMode === 'standalone') {
@@ -423,6 +423,9 @@ export default class Serve extends Command {
423
423
  const standaloneInput = {
424
424
  ...(config.standalone ?? {}),
425
425
  projectRoot: (config.standalone?.projectRoot ?? path.dirname(absolutePath)),
426
+ // #2229: dev enables the native-better-sqlite3 → wasm → in-memory
427
+ // step-down in the shared datasource factory; prod fails loudly.
428
+ dev: isDev,
426
429
  };
427
430
  const bootResult = await createStandaloneStack(standaloneInput);
428
431
  config = { ...originalConfig, ...bootResult };
@@ -516,10 +519,39 @@ export default class Serve extends Command {
516
519
  envLevel: readLogLevelEnv(),
517
520
  }),
518
521
  };
522
+ // Cluster wiring: env-driven driver selection (mirrors OS_DATABASE_URL).
523
+ // The remote driver self-registers on import; import it dynamically so it
524
+ // works in BOTH config-boot and compiled-artifact mode. Open-core ships
525
+ // only the in-memory driver — remote drivers (e.g. redis) come from the EE
526
+ // distribution; if absent we fall back to the in-memory cluster.
527
+ let clusterConfig;
528
+ const __clusterDriver = process.env.OS_CLUSTER_DRIVER?.trim();
529
+ if (__clusterDriver && __clusterDriver !== 'memory') {
530
+ // Multi-node authorization gate (open mechanism): a distribution (e.g.
531
+ // an EE license) may deny multi-node. On denial, downgrade to
532
+ // single-node rather than fail — multi-node is an add-on, never brick.
533
+ // Dynamic, non-literal specifier so the CLI does not statically depend
534
+ // on the cluster package (mirrors the remote-driver import below).
535
+ const __clusterPkg = '@objectstack/service-cluster';
536
+ const { checkMultiNodeAllowed } = (await import(__clusterPkg));
537
+ const __gate = checkMultiNodeAllowed();
538
+ if (!__gate.allowed) {
539
+ console.warn(`[cluster] multi-node not authorized (${__gate.reason ?? 'denied'}) — ` +
540
+ `downgrading to single-node (in-memory cluster). Remove OS_CLUSTER_DRIVER to silence.`);
541
+ }
542
+ else {
543
+ try {
544
+ await import(`@objectstack/service-cluster-${__clusterDriver}`);
545
+ }
546
+ catch { /* may already be registered by the loaded config */ }
547
+ clusterConfig = { driver: __clusterDriver, url: process.env.OS_REDIS_URL };
548
+ }
549
+ }
519
550
  const runtime = new Runtime({
520
551
  kernel: {
521
552
  logger: loggerConfig
522
- }
553
+ },
554
+ cluster: clusterConfig,
523
555
  });
524
556
  const kernel = runtime.getKernel();
525
557
  // Load plugins from configuration
@@ -587,16 +619,25 @@ export default class Serve extends Command {
587
619
  resolvedDatabaseUrl = databaseUrl ?? 'mongodb://localhost:27017/objectstack';
588
620
  }
589
621
  else if (driverType === 'sqlite' || driverType === 'sql') {
590
- const { SqlDriver } = await import('@objectstack/driver-sql');
591
622
  const filePath = (databaseUrl ?? ':memory:').replace(/^file:/, '').replace(/^sqlite:/, '').replace(/^sql:\/\//, '');
592
- await kernel.use(new DriverPlugin(new SqlDriver({
593
- client: 'better-sqlite3',
594
- connection: { filename: filePath },
595
- useNullAsDefault: true,
596
- })));
597
- trackPlugin('SqlDriver');
598
- resolvedDriverLabel = 'SqlDriver(sqlite)';
599
- resolvedDatabaseUrl = databaseUrl ?? ':memory:';
623
+ // Probe-by-connect with a dev-only native → wasm → in-memory
624
+ // step-down (#2229). better-sqlite3 loads its native addon lazily
625
+ // (first query), so an ABI mismatch is invisible here and would
626
+ // otherwise surface much later as a runtime crash. resolveSqliteDriver
627
+ // forces the load and degrades gracefully in dev / fails loudly in prod.
628
+ const { resolveSqliteDriver } = await import('@objectstack/service-datasource');
629
+ const resolved = await resolveSqliteDriver({
630
+ filename: filePath,
631
+ dev: isDev,
632
+ // #2186: in dev, self-heal a persisted DB when a metadata change
633
+ // relaxes a constraint (loosen-only; never destructive / never in prod).
634
+ autoMigrate: isDev ? 'safe' : undefined,
635
+ warn: (m) => console.warn(chalk.yellow(m)),
636
+ });
637
+ await kernel.use(new DriverPlugin(resolved.driver));
638
+ trackPlugin(resolved.engine === 'memory' ? 'MemoryDriver' : resolved.engine === 'sqlite-wasm' ? 'SqliteWasmDriver' : 'SqlDriver');
639
+ resolvedDriverLabel = resolved.label;
640
+ resolvedDatabaseUrl = resolved.engine === 'memory' ? '(in-memory)' : (databaseUrl ?? ':memory:');
600
641
  }
601
642
  else if (driverType === 'sqlite-wasm' || driverType === 'wasm-sqlite' || driverType === 'wasm') {
602
643
  const { SqliteWasmDriver } = await import('@objectstack/driver-sqlite-wasm');
@@ -615,6 +656,7 @@ export default class Serve extends Command {
615
656
  client: 'pg',
616
657
  connection: databaseUrl,
617
658
  pool: { min: 0, max: 5 },
659
+ autoMigrate: isDev ? 'safe' : undefined, // #2186 dev loosen-only self-heal
618
660
  })));
619
661
  trackPlugin('PostgresDriver');
620
662
  resolvedDriverLabel = 'SqlDriver(pg)';
@@ -626,99 +668,33 @@ export default class Serve extends Command {
626
668
  client: 'mysql2',
627
669
  connection: databaseUrl,
628
670
  pool: { min: 0, max: 5 },
671
+ autoMigrate: isDev ? 'safe' : undefined, // #2186 dev loosen-only self-heal
629
672
  })));
630
673
  trackPlugin('MySQLDriver');
631
674
  resolvedDriverLabel = 'SqlDriver(mysql2)';
632
675
  resolvedDatabaseUrl = databaseUrl;
633
676
  }
634
677
  else if (isDev) {
635
- // Default in dev: prefer native SQLite for production-like SQL
636
- // semantics at native speed. When the native `better-sqlite3`
637
- // binary is unavailable not built, ABI mismatch after a Node
638
- // upgrade (e.g. Node 25 NODE_MODULE_VERSION mismatch), or a
639
- // blocked prebuild download fall back to the pure-JS wasm SQLite
640
- // driver, which keeps *real* SQL semantics (and on-disk
641
- // persistence) without any native build step. Only if wasm also
642
- // fails to load do we drop to the in-memory driver (mingo), which
643
- // is neither real SQL nor persistent.
644
- //
645
- // knex loads its client lazily (at first query, not at construction),
646
- // so the only reliable signal inside this registration window is to
647
- // actually open a connection: connect() runs `SELECT 1`, which forces
648
- // better-sqlite3 to load. If that throws we step down the chain here
649
- // instead of letting the failure surface much later — as a
650
- // missing-module crash on the first real query — or be swallowed by
651
- // the silent catch below, leaving the kernel with no driver at all.
652
- let sqliteDriver;
653
- let sqliteOk = false;
654
- try {
655
- const { SqlDriver } = await import('@objectstack/driver-sql');
656
- sqliteDriver = new SqlDriver({
657
- client: 'better-sqlite3',
658
- connection: { filename: ':memory:' },
659
- useNullAsDefault: true,
660
- });
661
- await sqliteDriver.connect();
662
- sqliteOk = true;
663
- }
664
- catch {
665
- sqliteOk = false;
666
- if (sqliteDriver?.disconnect) {
667
- try {
668
- await sqliteDriver.disconnect();
669
- }
670
- catch { /* ignore */ }
671
- }
672
- }
673
- if (sqliteOk) {
674
- await kernel.use(new DriverPlugin(sqliteDriver));
675
- trackPlugin('SqlDriver');
676
- resolvedDriverLabel = 'SqlDriver(sqlite)';
677
- resolvedDatabaseUrl = ':memory:';
678
- }
679
- else {
680
- // Native unavailable → try the pure-JS wasm SQLite driver before
681
- // giving up on SQL fidelity entirely. Same probe-by-connect
682
- // approach: actually open the connection so a load failure is
683
- // caught here rather than on the first real query.
684
- let wasmDriver;
685
- let wasmOk = false;
686
- try {
687
- const { SqliteWasmDriver } = await import('@objectstack/driver-sqlite-wasm');
688
- wasmDriver = new SqliteWasmDriver({
689
- filename: ':memory:',
690
- persist: 'on-disconnect',
691
- });
692
- await wasmDriver.connect();
693
- wasmOk = true;
694
- }
695
- catch {
696
- wasmOk = false;
697
- if (wasmDriver?.disconnect) {
698
- try {
699
- await wasmDriver.disconnect();
700
- }
701
- catch { /* ignore */ }
702
- }
703
- }
704
- if (wasmOk) {
705
- await kernel.use(new DriverPlugin(wasmDriver));
706
- trackPlugin('SqliteWasmDriver');
707
- resolvedDriverLabel = 'SqliteWasmDriver';
708
- resolvedDatabaseUrl = ':memory:';
709
- console.warn(chalk.yellow(' ⚠ native better-sqlite3 unavailable (ABI mismatch or not built) — dev using wasm SQLite (real SQL, slower).\n' +
710
- ' Rebuild better-sqlite3 for native speed, or set OS_DATABASE_DRIVER=sqlite-wasm to silence this.'));
711
- }
712
- else {
713
- const { InMemoryDriver } = await import('@objectstack/driver-memory');
714
- await kernel.use(new DriverPlugin(new InMemoryDriver()));
715
- trackPlugin('MemoryDriver');
716
- resolvedDriverLabel = 'InMemoryDriver';
717
- resolvedDatabaseUrl = '(in-memory)';
718
- console.warn(chalk.yellow(' ⚠ neither native nor wasm SQLite available — dev falling back to InMemoryDriver (mingo, not real SQL).\n' +
719
- ' Rebuild better-sqlite3, or set OS_DATABASE_URL / OS_DATABASE_DRIVER for SQL fidelity.'));
720
- }
721
- }
678
+ // Default in dev (no DB configured): prefer native SQLite for
679
+ // production-like SQL at native speed, with a graceful step-down to
680
+ // wasm SQLite (real SQL + on-disk persistence) then in-memory when the
681
+ // native better-sqlite3 binary is unavailable not built, ABI mismatch
682
+ // after a Node upgrade (e.g. NODE_MODULE_VERSION change), or a blocked
683
+ // prebuild download. Shared with the explicit-file branch and the
684
+ // datasource factory via resolveSqliteDriver (#2229), which probes by
685
+ // actually opening a connection + running SELECT 1 (better-sqlite3 loads
686
+ // its native addon lazily at first query, not at construction).
687
+ const { resolveSqliteDriver } = await import('@objectstack/service-datasource');
688
+ const resolved = await resolveSqliteDriver({
689
+ filename: ':memory:',
690
+ dev: true,
691
+ autoMigrate: 'safe', // #2186 dev loosen-only self-heal
692
+ warn: (m) => console.warn(chalk.yellow(m)),
693
+ });
694
+ await kernel.use(new DriverPlugin(resolved.driver));
695
+ trackPlugin(resolved.engine === 'memory' ? 'MemoryDriver' : resolved.engine === 'sqlite-wasm' ? 'SqliteWasmDriver' : 'SqlDriver');
696
+ resolvedDriverLabel = resolved.label;
697
+ resolvedDatabaseUrl = resolved.engine === 'memory' ? '(in-memory)' : ':memory:';
722
698
  }
723
699
  }
724
700
  catch (e) {