@ketvietlab/ketjs 0.1.2 → 0.1.3

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 (299) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +8 -2
  3. package/dist/agent/capabilities.d.ts +1 -3
  4. package/dist/agent/capabilities.d.ts.map +1 -1
  5. package/dist/agent/capabilities.js +10 -1
  6. package/dist/agent/capabilities.js.map +1 -1
  7. package/dist/agent/permissions.d.ts +43 -1
  8. package/dist/agent/permissions.d.ts.map +1 -1
  9. package/dist/agent/permissions.js +50 -2
  10. package/dist/agent/permissions.js.map +1 -1
  11. package/dist/cli.js +246 -97
  12. package/dist/cli.js.map +1 -1
  13. package/dist/codegen/dts.d.ts.map +1 -1
  14. package/dist/codegen/dts.js +1 -0
  15. package/dist/codegen/dts.js.map +1 -1
  16. package/dist/data/changeset.d.ts +12 -0
  17. package/dist/data/changeset.d.ts.map +1 -1
  18. package/dist/data/changeset.js +73 -17
  19. package/dist/data/changeset.js.map +1 -1
  20. package/dist/data/expr.d.ts +16 -0
  21. package/dist/data/expr.d.ts.map +1 -1
  22. package/dist/data/expr.js +33 -5
  23. package/dist/data/expr.js.map +1 -1
  24. package/dist/data/fleet.d.ts +41 -1
  25. package/dist/data/fleet.d.ts.map +1 -1
  26. package/dist/data/fleet.js +100 -11
  27. package/dist/data/fleet.js.map +1 -1
  28. package/dist/data/migrate.d.ts +20 -0
  29. package/dist/data/migrate.d.ts.map +1 -1
  30. package/dist/data/migrate.js +86 -3
  31. package/dist/data/migrate.js.map +1 -1
  32. package/dist/data/physical.d.ts +5 -0
  33. package/dist/data/physical.d.ts.map +1 -0
  34. package/dist/data/physical.js +196 -0
  35. package/dist/data/physical.js.map +1 -0
  36. package/dist/data/pool.d.ts +2 -1
  37. package/dist/data/pool.d.ts.map +1 -1
  38. package/dist/data/pool.js +159 -31
  39. package/dist/data/pool.js.map +1 -1
  40. package/dist/data/query.d.ts +10 -1
  41. package/dist/data/query.d.ts.map +1 -1
  42. package/dist/data/query.js +136 -27
  43. package/dist/data/query.js.map +1 -1
  44. package/dist/data/sqlite.d.ts.map +1 -1
  45. package/dist/data/sqlite.js +350 -52
  46. package/dist/data/sqlite.js.map +1 -1
  47. package/dist/data/time.d.ts +13 -0
  48. package/dist/data/time.d.ts.map +1 -1
  49. package/dist/data/time.js +61 -17
  50. package/dist/data/time.js.map +1 -1
  51. package/dist/index.d.ts +43 -22
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +32 -16
  54. package/dist/index.js.map +1 -1
  55. package/dist/kernel/audit.d.ts +27 -0
  56. package/dist/kernel/audit.d.ts.map +1 -0
  57. package/dist/kernel/audit.js +67 -0
  58. package/dist/kernel/audit.js.map +1 -0
  59. package/dist/kernel/classification.d.ts +26 -0
  60. package/dist/kernel/classification.d.ts.map +1 -0
  61. package/dist/kernel/classification.js +73 -0
  62. package/dist/kernel/classification.js.map +1 -0
  63. package/dist/kernel/compose.d.ts +6 -2
  64. package/dist/kernel/compose.d.ts.map +1 -1
  65. package/dist/kernel/compose.js +269 -15
  66. package/dist/kernel/compose.js.map +1 -1
  67. package/dist/kernel/define.d.ts +2 -3
  68. package/dist/kernel/define.d.ts.map +1 -1
  69. package/dist/kernel/define.js +22 -12
  70. package/dist/kernel/define.js.map +1 -1
  71. package/dist/kernel/diff.d.ts.map +1 -1
  72. package/dist/kernel/diff.js +28 -4
  73. package/dist/kernel/diff.js.map +1 -1
  74. package/dist/kernel/graph.js +1 -1
  75. package/dist/kernel/graph.js.map +1 -1
  76. package/dist/kernel/i18n.d.ts +10 -1
  77. package/dist/kernel/i18n.d.ts.map +1 -1
  78. package/dist/kernel/i18n.js +34 -1
  79. package/dist/kernel/i18n.js.map +1 -1
  80. package/dist/kernel/layout.d.ts +97 -0
  81. package/dist/kernel/layout.d.ts.map +1 -1
  82. package/dist/kernel/layout.js +294 -12
  83. package/dist/kernel/layout.js.map +1 -1
  84. package/dist/kernel/menu.d.ts +33 -2
  85. package/dist/kernel/menu.d.ts.map +1 -1
  86. package/dist/kernel/menu.js +99 -7
  87. package/dist/kernel/menu.js.map +1 -1
  88. package/dist/kernel/modules.d.ts +3 -3
  89. package/dist/kernel/modules.d.ts.map +1 -1
  90. package/dist/kernel/modules.js +21 -21
  91. package/dist/kernel/modules.js.map +1 -1
  92. package/dist/kernel/permissions.d.ts +10 -0
  93. package/dist/kernel/permissions.d.ts.map +1 -0
  94. package/dist/kernel/permissions.js +365 -0
  95. package/dist/kernel/permissions.js.map +1 -0
  96. package/dist/kernel/schedule.d.ts +23 -0
  97. package/dist/kernel/schedule.d.ts.map +1 -0
  98. package/dist/kernel/schedule.js +83 -0
  99. package/dist/kernel/schedule.js.map +1 -0
  100. package/dist/kernel/types.d.ts.map +1 -1
  101. package/dist/kernel/types.js +5 -4
  102. package/dist/kernel/types.js.map +1 -1
  103. package/dist/kernel/workspace.d.ts +92 -14
  104. package/dist/kernel/workspace.d.ts.map +1 -1
  105. package/dist/kernel/workspace.js +128 -49
  106. package/dist/kernel/workspace.js.map +1 -1
  107. package/dist/pdf/assets/Inter-Bold.ttf +0 -0
  108. package/dist/pdf/assets/Inter-Regular.ttf +0 -0
  109. package/dist/pdf/assets/Inter-SemiBold.ttf +0 -0
  110. package/dist/pdf/assets/LICENSE.txt +92 -0
  111. package/dist/pdf/font.d.ts +10 -0
  112. package/dist/pdf/font.d.ts.map +1 -0
  113. package/dist/pdf/font.js +83 -0
  114. package/dist/pdf/font.js.map +1 -0
  115. package/dist/pdf/image.d.ts +8 -0
  116. package/dist/pdf/image.d.ts.map +1 -0
  117. package/dist/pdf/image.js +94 -0
  118. package/dist/pdf/image.js.map +1 -0
  119. package/dist/pdf/index.d.ts +15 -0
  120. package/dist/pdf/index.d.ts.map +1 -0
  121. package/dist/pdf/index.js +17 -0
  122. package/dist/pdf/index.js.map +1 -0
  123. package/dist/pdf/markup.d.ts +19 -0
  124. package/dist/pdf/markup.d.ts.map +1 -0
  125. package/dist/pdf/markup.js +146 -0
  126. package/dist/pdf/markup.js.map +1 -0
  127. package/dist/pdf/render.d.ts +14 -0
  128. package/dist/pdf/render.d.ts.map +1 -0
  129. package/dist/pdf/render.js +387 -0
  130. package/dist/pdf/render.js.map +1 -0
  131. package/dist/scaffold/index.js +6 -6
  132. package/dist/scaffold/index.js.map +1 -1
  133. package/dist/scaffold/templates/{app.test.ts.tmpl → deployment.test.ts.tmpl} +3 -3
  134. package/dist/scaffold/templates/ket.workspace.ts.tmpl +4 -6
  135. package/dist/scaffold/templates/module.ts.tmpl +1 -4
  136. package/dist/server/assets.d.ts +23 -0
  137. package/dist/server/assets.d.ts.map +1 -0
  138. package/dist/server/assets.js +97 -0
  139. package/dist/server/assets.js.map +1 -0
  140. package/dist/server/boot.d.ts +157 -38
  141. package/dist/server/boot.d.ts.map +1 -1
  142. package/dist/server/boot.js +360 -138
  143. package/dist/server/boot.js.map +1 -1
  144. package/dist/server/config.d.ts +30 -10
  145. package/dist/server/config.d.ts.map +1 -1
  146. package/dist/server/config.js +73 -17
  147. package/dist/server/config.js.map +1 -1
  148. package/dist/server/ctx.d.ts +10 -0
  149. package/dist/server/ctx.d.ts.map +1 -1
  150. package/dist/server/ctx.js +266 -48
  151. package/dist/server/ctx.js.map +1 -1
  152. package/dist/server/development.d.ts +10 -0
  153. package/dist/server/development.d.ts.map +1 -0
  154. package/dist/server/development.js +15 -0
  155. package/dist/server/development.js.map +1 -0
  156. package/dist/server/fn.d.ts +22 -3
  157. package/dist/server/fn.d.ts.map +1 -1
  158. package/dist/server/fn.js +116 -32
  159. package/dist/server/fn.js.map +1 -1
  160. package/dist/server/form.d.ts +33 -0
  161. package/dist/server/form.d.ts.map +1 -0
  162. package/dist/server/form.js +47 -0
  163. package/dist/server/form.js.map +1 -0
  164. package/dist/server/http.d.ts +55 -3
  165. package/dist/server/http.d.ts.map +1 -1
  166. package/dist/server/http.js +306 -22
  167. package/dist/server/http.js.map +1 -1
  168. package/dist/server/idem.d.ts +3 -2
  169. package/dist/server/idem.d.ts.map +1 -1
  170. package/dist/server/idem.js +12 -4
  171. package/dist/server/idem.js.map +1 -1
  172. package/dist/server/jobs.js +1 -1
  173. package/dist/server/jobs.js.map +1 -1
  174. package/dist/server/log/combinators.d.ts +42 -0
  175. package/dist/server/log/combinators.d.ts.map +1 -0
  176. package/dist/server/log/combinators.js +270 -0
  177. package/dist/server/log/combinators.js.map +1 -0
  178. package/dist/server/log/console.d.ts +12 -0
  179. package/dist/server/log/console.d.ts.map +1 -0
  180. package/dist/server/log/console.js +96 -0
  181. package/dist/server/log/console.js.map +1 -0
  182. package/dist/server/log/file.d.ts +11 -0
  183. package/dist/server/log/file.d.ts.map +1 -0
  184. package/dist/server/log/file.js +61 -0
  185. package/dist/server/log/file.js.map +1 -0
  186. package/dist/server/log/index.d.ts +23 -0
  187. package/dist/server/log/index.d.ts.map +1 -0
  188. package/dist/server/log/index.js +45 -0
  189. package/dist/server/log/index.js.map +1 -0
  190. package/dist/server/log/logger.d.ts +53 -0
  191. package/dist/server/log/logger.d.ts.map +1 -0
  192. package/dist/server/log/logger.js +94 -0
  193. package/dist/server/log/logger.js.map +1 -0
  194. package/dist/server/log/memory.d.ts +11 -0
  195. package/dist/server/log/memory.d.ts.map +1 -0
  196. package/dist/server/log/memory.js +25 -0
  197. package/dist/server/log/memory.js.map +1 -0
  198. package/dist/server/log/types.d.ts +112 -0
  199. package/dist/server/log/types.d.ts.map +1 -0
  200. package/dist/server/log/types.js +58 -0
  201. package/dist/server/log/types.js.map +1 -0
  202. package/dist/server/policy.d.ts +29 -0
  203. package/dist/server/policy.d.ts.map +1 -0
  204. package/dist/server/policy.js +29 -0
  205. package/dist/server/policy.js.map +1 -0
  206. package/dist/server/queue.d.ts +1 -1
  207. package/dist/server/queue.d.ts.map +1 -1
  208. package/dist/server/queue.js.map +1 -1
  209. package/dist/server/ratelimit.d.ts +48 -0
  210. package/dist/server/ratelimit.d.ts.map +1 -0
  211. package/dist/server/ratelimit.js +102 -0
  212. package/dist/server/ratelimit.js.map +1 -0
  213. package/dist/server/respond.d.ts.map +1 -1
  214. package/dist/server/respond.js +16 -1
  215. package/dist/server/respond.js.map +1 -1
  216. package/dist/server/runtime.d.ts +18 -3
  217. package/dist/server/runtime.d.ts.map +1 -1
  218. package/dist/server/runtime.js +32 -2
  219. package/dist/server/runtime.js.map +1 -1
  220. package/dist/server/schedule.d.ts +18 -0
  221. package/dist/server/schedule.d.ts.map +1 -0
  222. package/dist/server/schedule.js +70 -0
  223. package/dist/server/schedule.js.map +1 -0
  224. package/dist/server/sequence.d.ts +42 -0
  225. package/dist/server/sequence.d.ts.map +1 -0
  226. package/dist/server/sequence.js +97 -0
  227. package/dist/server/sequence.js.map +1 -0
  228. package/dist/server/session.d.ts +21 -0
  229. package/dist/server/session.d.ts.map +1 -1
  230. package/dist/server/session.js +84 -10
  231. package/dist/server/session.js.map +1 -1
  232. package/dist/server/sessionstore.d.ts +3 -1
  233. package/dist/server/sessionstore.d.ts.map +1 -1
  234. package/dist/server/sessionstore.js +7 -3
  235. package/dist/server/sessionstore.js.map +1 -1
  236. package/dist/server/storage/index.d.ts +1 -1
  237. package/dist/server/storage/index.d.ts.map +1 -1
  238. package/dist/server/storage/index.js +61 -1
  239. package/dist/server/storage/index.js.map +1 -1
  240. package/dist/server/storage/types.d.ts +8 -0
  241. package/dist/server/storage/types.d.ts.map +1 -1
  242. package/dist/server/storage/types.js +58 -0
  243. package/dist/server/storage/types.js.map +1 -1
  244. package/dist/server/tenants.d.ts +20 -18
  245. package/dist/server/tenants.d.ts.map +1 -1
  246. package/dist/server/tenants.js +133 -84
  247. package/dist/server/tenants.js.map +1 -1
  248. package/dist/server/transport/types.js +2 -2
  249. package/dist/server/transport/types.js.map +1 -1
  250. package/dist/server/worker.d.ts +19 -3
  251. package/dist/server/worker.d.ts.map +1 -1
  252. package/dist/server/worker.js +168 -21
  253. package/dist/server/worker.js.map +1 -1
  254. package/dist/testing.d.ts +26 -10
  255. package/dist/testing.d.ts.map +1 -1
  256. package/dist/testing.js +41 -14
  257. package/dist/testing.js.map +1 -1
  258. package/dist/theme/contracts.d.ts +14 -1
  259. package/dist/theme/contracts.d.ts.map +1 -1
  260. package/dist/theme/contracts.js +35 -6
  261. package/dist/theme/contracts.js.map +1 -1
  262. package/dist/theme/index.d.ts +1 -1
  263. package/dist/theme/index.d.ts.map +1 -1
  264. package/dist/theme/index.js +1 -1
  265. package/dist/theme/index.js.map +1 -1
  266. package/dist/theme/joint-runtime.d.ts +33 -0
  267. package/dist/theme/joint-runtime.d.ts.map +1 -0
  268. package/dist/theme/joint-runtime.js +121 -0
  269. package/dist/theme/joint-runtime.js.map +1 -0
  270. package/dist/theme/joints.d.ts.map +1 -1
  271. package/dist/theme/joints.js +15 -97
  272. package/dist/theme/joints.js.map +1 -1
  273. package/dist/theme/ktl/compile.d.ts +4 -0
  274. package/dist/theme/ktl/compile.d.ts.map +1 -1
  275. package/dist/theme/ktl/compile.js +54 -1
  276. package/dist/theme/ktl/compile.js.map +1 -1
  277. package/dist/theme/ktl/parser.d.ts +4 -0
  278. package/dist/theme/ktl/parser.d.ts.map +1 -1
  279. package/dist/theme/ktl/parser.js +8 -0
  280. package/dist/theme/ktl/parser.js.map +1 -1
  281. package/dist/theme/render.d.ts +8 -0
  282. package/dist/theme/render.d.ts.map +1 -1
  283. package/dist/theme/render.js +88 -118
  284. package/dist/theme/render.js.map +1 -1
  285. package/dist/theme/tokens.d.ts +6 -0
  286. package/dist/theme/tokens.d.ts.map +1 -1
  287. package/dist/theme/tokens.js +6 -0
  288. package/dist/theme/tokens.js.map +1 -1
  289. package/dist/theme/viewmodel.d.ts +24 -0
  290. package/dist/theme/viewmodel.d.ts.map +1 -1
  291. package/dist/theme/viewmodel.js +75 -6
  292. package/dist/theme/viewmodel.js.map +1 -1
  293. package/dist/types.d.ts +304 -56
  294. package/dist/types.d.ts.map +1 -1
  295. package/package.json +6 -2
  296. package/dist/kernel/apps.d.ts +0 -43
  297. package/dist/kernel/apps.d.ts.map +0 -1
  298. package/dist/kernel/apps.js +0 -234
  299. package/dist/kernel/apps.js.map +0 -1
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // The CLI is deliberately thin: everything it prints is read off the manifest,
3
- // so there is no second source of truth about what an app contains.
3
+ // so there is no second source of truth about what a deployment contains.
4
4
  var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
5
5
  if (typeof path === "string" && /^\.\.?\//.test(path)) {
6
6
  return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
@@ -18,21 +18,22 @@ import { resolveWorkspace } from './kernel/modules.js';
18
18
  import { diffManifests, formatDiff } from './kernel/diff.js';
19
19
  import { generateDts } from './codegen/dts.js';
20
20
  import { agentDescriptor } from './agent/capabilities.js';
21
- import { reachOf, functionsOf, formatReach, formatInventory, grantsOfRole } from './agent/permissions.js';
21
+ import { reachOf, functionsOf, formatReach, formatInventory, grantsOfRole, permissionInventory, } from './agent/permissions.js';
22
+ import { classificationInventory, formatClassification } from './kernel/classification.js';
22
23
  import { readConfig, sqliteStore } from './server/config.js';
23
24
  import { schemaFromManifest, planMigration, renderSql } from './data/migrate.js';
24
- import { migrateOne, migrateFleet, formatFleet } from './data/fleet.js';
25
+ import { migrateOne, migrateFleet, formatFleet, verifyPhysicalSchema } from './data/fleet.js';
25
26
  import { createAdapterPool } from './data/pool.js';
26
27
  import { sqliteAdapter } from './data/sqlite.js';
27
- import { bootApp, serveApp } from './server/boot.js';
28
+ import { bootDeployment, serveDeployment } from './server/boot.js';
28
29
  import { bootRuntime } from './server/runtime.js';
29
30
  import { callFn } from './server/fn.js';
30
- import { createAppRegistry, restrictManifest } from './kernel/apps.js';
31
31
  import { bootWorker, serveWorker } from './server/worker.js';
32
+ import { createDevelopmentCloser } from './server/development.js';
32
33
  import { createQueue } from './server/queue.js';
33
34
  import { scaffold } from './scaffold/index.js';
34
35
  import { KetError } from './kernel/errors.js';
35
- import { CookieJar, createTestApp, TestClient, TestHttpError } from './testing.js';
36
+ import { CookieJar, createTestDeployment, TestClient, TestHttpError } from './testing.js';
36
37
  const [, , cmd = 'help', ...rest] = process.argv;
37
38
  const flag = (name) => rest.includes(`--${name}`);
38
39
  const opt = (name) => {
@@ -76,7 +77,7 @@ const CANDIDATES = [
76
77
  'workspace.js',
77
78
  'examples/workspace.js',
78
79
  ];
79
- /** Where the apps are declared. Named explicitly, or the first conventional path. */
80
+ /** Where deployments are declared. Named explicitly, or the first conventional path. */
80
81
  const workspacePath = () => {
81
82
  const given = opt('workspace');
82
83
  if (given) {
@@ -94,11 +95,11 @@ const loadWorkspace = async () => {
94
95
  const configuredPath = workspacePath();
95
96
  const absolutePath = resolve(configuredPath);
96
97
  const mod = (await import(__rewriteRelativeImportExtension(pathToFileURL(absolutePath).href)));
97
- const declaration = mod.default && typeof mod.default === 'object' && Array.isArray(mod.default.apps)
98
+ const declaration = mod.default && typeof mod.default === 'object' && Array.isArray(mod.default.deployments)
98
99
  ? mod.default
99
- : { apps: mod.apps, modulePaths: mod.modulePaths };
100
- if (!Array.isArray(declaration.apps))
101
- throw new Error(`${configuredPath} must default-export defineWorkspace(...) or export \`apps\`, an array of defineApp(...)`);
100
+ : { deployments: mod.deployments, modulePaths: mod.modulePaths };
101
+ if (!Array.isArray(declaration.deployments))
102
+ throw new Error(`${configuredPath} must default-export defineWorkspace(...) or export \`deployments\`, an array of defineDeployment(...)`);
102
103
  const fromEnv = (process.env.KET_MODULE_PATH ?? '')
103
104
  .split(delimiter)
104
105
  .map((path) => path.trim())
@@ -108,7 +109,11 @@ const loadWorkspace = async () => {
108
109
  extraModulePaths: [...fromEnv, ...opts('module-path')],
109
110
  allowSource: process.env.KET_DEV_SOURCE === '1',
110
111
  });
111
- return { ws: composeWorkspace(resolved.apps), apps: resolved.apps, resolved };
112
+ return {
113
+ ws: composeWorkspace(resolved.deployments),
114
+ deployments: resolved.deployments,
115
+ resolved,
116
+ };
112
117
  };
113
118
  const formatModules = (paths, modules) => {
114
119
  const lines = ['module paths:'];
@@ -118,53 +123,68 @@ const formatModules = (paths, modules) => {
118
123
  lines.push(' (none)');
119
124
  lines.push('modules:');
120
125
  for (const module of modules) {
121
- lines.push(` ${module.name.padEnd(24)} ${module.version.padEnd(10)} ${module.kind.padEnd(6)} apps=${module.apps.join(',')} ${module.source}`);
126
+ lines.push(` ${module.name.padEnd(24)} ${module.version.padEnd(10)} ${module.kind.padEnd(6)} deployments=${module.deployments.join(',')} ${module.source}`);
122
127
  }
123
128
  if (!modules.length)
124
129
  lines.push(' (none)');
125
130
  return lines.join('\n');
126
131
  };
127
- /** The AppSpec, not the composed manifest — serving needs what the app declared. */
132
+ /** The DeploymentSpec, not the composed manifest — serving needs the authored runtime settings. */
128
133
  const pickSpec = (specs) => {
129
- const name = opt('app');
134
+ const name = opt('deployment');
130
135
  if (!name) {
131
136
  const servable = specs.filter((s) => s.serve);
132
137
  const one = servable[0] ?? specs[0];
133
138
  if (!one)
134
- throw new Error('the workspace declares no apps');
139
+ throw new Error('the workspace declares no deployments');
135
140
  return one;
136
141
  }
137
142
  const found = specs.find((s) => s.name === name);
138
143
  if (!found)
139
- throw new Error(`unknown app "${name}" (have: ${specs.map((s) => s.name).join(', ')})`);
144
+ throw new Error(`unknown deployment "${name}" (have: ${specs.map((s) => s.name).join(', ')})`);
140
145
  return found;
141
146
  };
142
- const pickApp = (ws) => {
143
- const name = opt('app') ?? Object.keys(ws.apps)[0];
144
- const m = ws.apps[name];
147
+ const pickFleetSpec = (specs) => {
148
+ if (opt('deployment'))
149
+ return pickSpec(specs);
150
+ const fleets = specs.filter((spec) => spec.serve?.tenants);
151
+ if (!fleets.length)
152
+ throw new Error('the workspace declares no tenant-fleet deployment');
153
+ if (fleets.length > 1)
154
+ throw new Error(`the workspace declares multiple tenant-fleet deployments (${fleets.map((spec) => spec.name).join(', ')}); pass --deployment NAME`);
155
+ return fleets[0];
156
+ };
157
+ const pickDeployment = (ws) => {
158
+ const name = opt('deployment') ?? Object.keys(ws.deployments)[0];
159
+ const m = ws.deployments[name];
145
160
  if (!m)
146
- throw new Error(`unknown app "${name}" (have: ${Object.keys(ws.apps).join(', ')})`);
161
+ throw new Error(`unknown deployment "${name}" (have: ${Object.keys(ws.deployments).join(', ')})`);
147
162
  return [name, m];
148
163
  };
149
164
  const HELP = `ket — zero-dependency fullstack framework
150
165
 
151
- ket check compose every app and report contract violations
152
- ket manifest [--app X] print the composed manifest
153
- ket workspace show apps, datastores and shared modules
166
+ ket check compose every deployment and report contract violations
167
+ ket manifest [--deployment X] print the composed manifest
168
+ ket workspace show deployments, datastores and shared modules
154
169
  ket modules show resolved modules and their source paths
155
- ket types [--app X] generate .ket/types.d.ts from the manifest
156
- ket agent [--app X] print the agent capability descriptor
170
+ ket types [--deployment X] generate .ket/types.d.ts from the manifest
171
+ ket agent [--deployment X] print the agent capability descriptor
157
172
  ket permissions every function that exists to be granted
173
+ ket classification which fields hold personal data, and which never leave
158
174
  --grant a,b,c …or what a role granted exactly these can reach
159
175
  --module NAME …or what granting one module's whole surface reaches
160
176
  --role NAME …or what a role in the database actually grants
161
- ket migrate [--app X] plan migrations (add --allow-destructive to permit data loss)
177
+ --json machine-readable module/function inventory
178
+ --all with --json, inventory every deployment
179
+ ket migrate [--deployment X] plan migrations (add --allow-destructive to permit data loss)
162
180
  --all …or apply them to every tenant database (add --dry-run)
181
+ ket schema verify [--deployment X] compare the physical database, migration marker, and manifest
182
+ --tenant KEY select one tenant database; --all verifies the complete tenant fleet
163
183
  ket diff --against FILE compare the current manifest with a stored one
164
- ket snapshot [--app X] write .ket/manifest.<app>.json for a later diff
184
+ ket snapshot [--deployment X] write .ket/manifest.<deployment>.json for a later diff
165
185
 
166
- ket serve [--app X] boot the app and serve it
167
- ket worker [--app X] run declared queues for the same app and manifest
186
+ ket serve [--deployment X] boot and serve the deployment
187
+ ket worker [--deployment X] run declared queues for the same deployment and manifest
168
188
  ket call FUNCTION exercise a function through its real HTTP endpoint
169
189
  --against URL call an already-running development server
170
190
  --input JSON|@FILE|- function input (default: {})
@@ -175,27 +195,49 @@ const HELP = `ket — zero-dependency fullstack framework
175
195
  --login-path PATH override the application's login route
176
196
  --dry-run report declared writes without applying them
177
197
  --idempotency-key KEY exercise idempotent retry handling
178
- --isolated boot a temporary app/database for this call
198
+ --isolated boot a temporary deployment/database for this call
179
199
  ket provision FUNCTION run a declared one-shot bootstrap function
180
200
  --input - read JSON, including secrets, from stdin only
181
- --tenant KEY required when the app uses tenant databases
201
+ --tenant KEY required when the deployment uses tenant databases
182
202
  ket test [FILES...] run emitted headless tests with Node's test runner
183
203
  --watch rerun when the selected JavaScript artifacts change
184
204
  --test-name-pattern P select tests by name; --coverage enables coverage
185
- ket jobs list inspect durable jobs (--tenant is required for tenant apps)
205
+ ket jobs list inspect durable jobs (--tenant is required for tenant deployments)
186
206
  ket jobs retry ID make a retryable/discarded job available now
187
207
  ket jobs cancel ID cancel a pending or executing job
188
208
  ket jobs prune apply the 7/30-day retention policy
189
- ket dev [--app X] serve compiled output, restarting when an artifact changes
209
+ ket dev [--deployment X] serve compiled output, restarting when an artifact changes
190
210
  --all run HTTP and worker together under this one watcher
191
- (--no-auto-install holds back modules that install themselves)
192
- ket new NAME [--dir D] scaffold an app that runs
211
+ ket new NAME [--dir D] scaffold a deployment that runs
193
212
 
194
213
  Options: --workspace FILE (default: dist/ket.workspace.js, ket.workspace.js, workspace.js)
195
214
  --module-path DIR (repeatable; KET_MODULE_PATH uses the platform path separator)
196
- --app NAME, --port N, --header "Name: value" (repeatable)
215
+ --deployment NAME, --port N, --header "Name: value" (repeatable)
197
216
  `;
198
217
  const TEST_VALUE_OPTIONS = ['test-name-pattern', 'test-concurrency', 'reporter', 'out-dir'];
218
+ const formatSchemaVerification = (datastore, report) => {
219
+ if (report.ok)
220
+ return `ok ${datastore.padEnd(24)} physical schema, marker and manifest agree`;
221
+ const lines = [`FAIL ${datastore.padEnd(24)} schema verification failed`];
222
+ if (!report.applied)
223
+ lines.push(' marker: missing');
224
+ else if (!report.markerMatchesManifest)
225
+ lines.push(' marker: differs from the current manifest');
226
+ if (report.applied && report.markerIssues.length) {
227
+ lines.push(' physical vs marker:');
228
+ for (const issue of report.markerIssues)
229
+ lines.push(` - ${issue}`);
230
+ }
231
+ const sameIssues = report.markerMatchesManifest &&
232
+ report.markerIssues.length === report.manifestIssues.length &&
233
+ report.markerIssues.every((issue, index) => issue === report.manifestIssues[index]);
234
+ if (!sameIssues && report.manifestIssues.length) {
235
+ lines.push(' physical vs manifest:');
236
+ for (const issue of report.manifestIssues)
237
+ lines.push(` - ${issue}`);
238
+ }
239
+ return lines.join('\n');
240
+ };
199
241
  const collectTests = (path) => {
200
242
  if (!existsSync(path))
201
243
  throw new Error(`test path does not exist: ${path}`);
@@ -300,7 +342,6 @@ const callWithClient = async (baseUrl) => {
300
342
  'login-path',
301
343
  'idempotency-key',
302
344
  'header',
303
- 'app',
304
345
  'workspace',
305
346
  'module-path',
306
347
  ])[0];
@@ -359,7 +400,7 @@ try {
359
400
  }
360
401
  if (cmd === 'test')
361
402
  await runTests();
362
- // Remote smoke calls need no workspace and do not boot a second app.
403
+ // Remote smoke calls need no workspace and do not boot a second deployment.
363
404
  if (cmd === 'call' && opt('against')) {
364
405
  const against = new URL(opt('against'));
365
406
  if (against.protocol !== 'http:' && against.protocol !== 'https:')
@@ -382,15 +423,15 @@ try {
382
423
  process.on(sig, () => child.kill(sig));
383
424
  await new Promise(() => { });
384
425
  }
385
- const { ws, apps: specs, resolved } = await loadWorkspace();
386
- mkdirSync('.ket', { recursive: true });
426
+ const { ws, deployments: specs, resolved } = await loadWorkspace();
427
+ if (!(cmd === 'migrate' && flag('dry-run')) && cmd !== 'schema')
428
+ mkdirSync('.ket', { recursive: true });
387
429
  if (cmd === 'serve') {
388
430
  const spec = pickSpec(specs);
389
431
  if (!spec.serve)
390
- throw new Error(`app "${spec.name}" declares no serve block, so there is nothing to run`);
432
+ throw new Error(`deployment "${spec.name}" declares no serve block, so there is nothing to run`);
391
433
  const port = opt('port');
392
- const env = flag('no-auto-install') ? { ...process.env, KET_AUTO_INSTALL: '0' } : process.env;
393
- await serveApp(spec, { env, ...(port ? { port: Number(port) } : {}) });
434
+ await serveDeployment(spec, { env: process.env, ...(port ? { port: Number(port) } : {}) });
394
435
  }
395
436
  else if (cmd === 'worker') {
396
437
  const spec = pickSpec(specs);
@@ -400,45 +441,42 @@ try {
400
441
  else if (cmd === 'call') {
401
442
  const spec = pickSpec(specs);
402
443
  if (!spec.serve)
403
- throw new Error(`app "${spec.name}" declares no serve block`);
444
+ throw new Error(`deployment "${spec.name}" declares no serve block`);
404
445
  if (flag('isolated')) {
405
- const testApp = await createTestApp(spec, { worker: false });
446
+ const testDeployment = await createTestDeployment(spec, { worker: false });
406
447
  try {
407
- process.exitCode = await callWithClient(testApp.baseUrl);
448
+ process.exitCode = await callWithClient(testDeployment.baseUrl);
408
449
  }
409
450
  finally {
410
- await testApp.close();
451
+ await testDeployment.close();
411
452
  }
412
453
  }
413
454
  else {
414
- const env = {
415
- ...process.env,
416
- ...(flag('no-migrate') ? { KET_MIGRATE: '0' } : {}),
417
- ...(flag('no-auto-install') ? { KET_AUTO_INSTALL: '0' } : {}),
418
- };
419
- const app = await bootApp(spec, {
455
+ const env = { ...process.env, ...(flag('no-migrate') ? { KET_MIGRATE: '0' } : {}) };
456
+ const deployment = await bootDeployment(spec, {
420
457
  env,
421
458
  port: 0,
422
459
  log: flag('verbose') ? (line) => console.error(line) : () => { },
423
460
  });
424
461
  try {
425
- process.exitCode = await callWithClient(`http://127.0.0.1:${app.port}`);
462
+ process.exitCode = await callWithClient(`http://127.0.0.1:${deployment.port}`);
426
463
  }
427
464
  finally {
428
- await app.close();
465
+ await deployment.close();
429
466
  }
430
467
  }
431
468
  }
432
469
  else if (cmd === 'provision') {
433
- const fnKey = positionals(['input', 'tenant', 'app', 'workspace', 'module-path'])[0];
470
+ const fnKey = positionals(['input', 'tenant', 'deployment', 'workspace', 'module-path'])[0];
434
471
  if (!fnKey)
435
472
  throw new Error('usage: ket provision FUNCTION --input - [--tenant KEY]');
436
473
  if (opt('input') !== '-')
437
474
  throw new Error('ket provision reads input only from stdin; pass --input - so secrets never enter argv');
438
475
  const spec = pickSpec(specs);
439
476
  if (!spec.serve)
440
- throw new Error(`app "${spec.name}" declares no serve block`);
441
- const runtime = await bootRuntime(spec, { env: process.env });
477
+ throw new Error(`deployment "${spec.name}" declares no serve block`);
478
+ // No socket is opened here, so the records are a command's, not a server's.
479
+ const runtime = await bootRuntime(spec, { env: process.env, role: 'cli' });
442
480
  const meta = runtime.manifest.functions[fnKey];
443
481
  if (!meta)
444
482
  throw new Error(`unknown function "${fnKey}"`);
@@ -446,7 +484,7 @@ try {
446
484
  throw new Error(`function "${fnKey}" is not declared internal + provision`);
447
485
  const tenant = opt('tenant');
448
486
  if (spec.serve.tenants && !tenant)
449
- throw new Error(`app "${spec.name}" has tenant databases; pass --tenant NAME`);
487
+ throw new Error(`deployment "${spec.name}" has tenant databases; pass --tenant NAME`);
450
488
  const adapter = spec.serve.tenants
451
489
  ? await spec.serve.tenants.open(tenant, runtime.config)
452
490
  : await (spec.serve.openStore ?? sqliteStore)(runtime.config);
@@ -454,17 +492,9 @@ try {
454
492
  await adapter.open();
455
493
  try {
456
494
  await migrateOne(adapter, runtime.manifest);
457
- const apps = await createAppRegistry(runtime.manifest, adapter, {
458
- autoInstall: runtime.config.autoInstall,
459
- });
460
- const bootstrap = runtime.config.bootstrapApps ?? spec.serve.bootstrap ?? [];
461
- if ((await apps.enabled()).size === 0)
462
- for (const name of bootstrap)
463
- await apps.install(name);
464
- const live = restrictManifest(runtime.manifest, await apps.enabled());
465
495
  const result = await callFn(fnKey, jsonObject('-', '--input'), {
466
496
  adapter,
467
- manifest: live,
497
+ manifest: runtime.manifest,
468
498
  actor: 'system:provision',
469
499
  scope: { company: null, branch: null },
470
500
  });
@@ -482,24 +512,21 @@ try {
482
512
  else if (cmd === 'all') {
483
513
  const spec = pickSpec(specs);
484
514
  if (!spec.serve)
485
- throw new Error(`app "${spec.name}" declares no serve block`);
515
+ throw new Error(`deployment "${spec.name}" declares no serve block`);
486
516
  if (!spec.worker)
487
- throw new Error(`app "${spec.name}" declares no worker block`);
488
- const app = await bootApp(spec, {
517
+ throw new Error(`deployment "${spec.name}" declares no worker block`);
518
+ const deployment = await bootDeployment(spec, {
489
519
  env: process.env,
490
520
  ...(opt('port') ? { port: Number(opt('port')) } : {}),
491
521
  });
492
522
  const worker = await bootWorker(spec, { env: process.env });
493
523
  worker.start();
494
- console.log(await app.banner());
524
+ console.log(await deployment.banner());
495
525
  console.log(` worker ${worker.workerId} is running in this development process\n`);
496
- const close = async () => {
497
- await worker.close();
498
- await app.close();
499
- process.exit(0);
500
- };
501
- for (const signal of ['SIGINT', 'SIGTERM'])
502
- process.on(signal, () => void close());
526
+ const close = createDevelopmentCloser(deployment, worker);
527
+ for (const signal of ['SIGINT', 'SIGTERM']) {
528
+ process.on(signal, () => void close().then(() => process.exit(0)));
529
+ }
503
530
  await new Promise(() => { });
504
531
  }
505
532
  else if (cmd === 'jobs') {
@@ -518,7 +545,7 @@ try {
518
545
  });
519
546
  const tenant = opt('tenant');
520
547
  if (spec.serve?.tenants && !tenant)
521
- throw new Error(`app "${spec.name}" has tenant databases; pass --tenant NAME`);
548
+ throw new Error(`deployment "${spec.name}" has tenant databases; pass --tenant NAME`);
522
549
  const adapter = spec.serve?.tenants
523
550
  ? await spec.serve.tenants.open(tenant, config)
524
551
  : await (spec.serve?.openStore ?? sqliteStore)(config);
@@ -585,21 +612,40 @@ try {
585
612
  console.log(formatModules(resolved.modulePaths, resolved.modules));
586
613
  }
587
614
  else if (cmd === 'manifest') {
588
- const [, m] = pickApp(ws);
615
+ const [, m] = pickDeployment(ws);
589
616
  console.log(JSON.stringify(m, null, 2));
590
617
  }
591
618
  else if (cmd === 'types') {
592
- const [name, m] = pickApp(ws);
619
+ const [name, m] = pickDeployment(ws);
593
620
  const out = `.ket/types.${name}.d.ts`;
594
621
  writeFileSync(out, generateDts(m));
595
622
  console.log(`wrote ${out}`);
596
623
  }
597
624
  else if (cmd === 'permissions') {
598
- const [, m] = pickApp(ws);
599
625
  const module = opt('module');
600
626
  const grant = opt('grant');
601
627
  const role = opt('role');
602
- if (role) {
628
+ if (flag('json')) {
629
+ if (module || grant || role)
630
+ throw new Error('permissions --json cannot be combined with --module, --grant or --role');
631
+ if (flag('all')) {
632
+ if (opt('deployment'))
633
+ throw new Error('permissions --json accepts either --deployment NAME or --all, not both');
634
+ const deployments = Object.entries(ws.deployments)
635
+ .sort(([left], [right]) => left.localeCompare(right))
636
+ .map(([name, manifest]) => ({ name, inventory: permissionInventory(manifest) }));
637
+ console.log(JSON.stringify({ version: 1, deployments }, null, 2));
638
+ }
639
+ else {
640
+ const [name, manifest] = pickDeployment(ws);
641
+ console.log(JSON.stringify({ name, inventory: permissionInventory(manifest) }, null, 2));
642
+ }
643
+ }
644
+ else if (flag('all')) {
645
+ throw new Error('permissions --all requires --json');
646
+ }
647
+ else if (role) {
648
+ const [, m] = pickDeployment(ws);
603
649
  // The one form that reads the database: a role is data, and what it grants is
604
650
  // a fact about a deployment rather than about the code.
605
651
  const spec = pickSpec(specs);
@@ -614,22 +660,34 @@ try {
614
660
  await adapter.close();
615
661
  }
616
662
  }
617
- else if (module)
663
+ else if (module) {
664
+ const [, m] = pickDeployment(ws);
618
665
  console.log(formatReach(reachOf(m, functionsOf(m, module))));
619
- else if (grant)
666
+ }
667
+ else if (grant) {
668
+ const [, m] = pickDeployment(ws);
620
669
  console.log(formatReach(reachOf(m, grant
621
670
  .split(',')
622
671
  .map((s) => s.trim())
623
672
  .filter(Boolean))));
624
- else
673
+ }
674
+ else {
675
+ const [, m] = pickDeployment(ws);
625
676
  console.log(formatInventory(m));
677
+ }
678
+ }
679
+ else if (cmd === 'classification') {
680
+ const [, m] = pickDeployment(ws);
681
+ const inventory = classificationInventory(m);
682
+ // JSON for an audit that has to be filed; columns for a person reading it now.
683
+ console.log(flag('json') ? JSON.stringify(inventory, null, 2) : formatClassification(inventory));
626
684
  }
627
685
  else if (cmd === 'agent') {
628
- const [, m] = pickApp(ws);
686
+ const [, m] = pickDeployment(ws);
629
687
  console.log(JSON.stringify(agentDescriptor(m), null, 2));
630
688
  }
631
689
  else if (cmd === 'snapshot') {
632
- const [name, m] = pickApp(ws);
690
+ const [name, m] = pickDeployment(ws);
633
691
  const out = `.ket/manifest.${name}.json`;
634
692
  writeFileSync(out, JSON.stringify(m, null, 2));
635
693
  console.log(`wrote ${out}`);
@@ -638,26 +696,113 @@ try {
638
696
  const against = opt('against');
639
697
  if (!against || !existsSync(against))
640
698
  throw new Error('pass --against <manifest.json> (make one with `ket snapshot`)');
641
- const [, m] = pickApp(ws);
699
+ const [, m] = pickDeployment(ws);
642
700
  const before = JSON.parse(readFileSync(against, 'utf8'));
643
701
  const items = diffManifests(before, m);
644
702
  console.log(formatDiff(items));
645
703
  process.exit(items.some((i) => i.severity === 'breaking') ? 1 : 0);
646
704
  }
705
+ else if (cmd === 'schema') {
706
+ const action = positionals(['workspace', 'module-path', 'deployment', 'tenant'])[0];
707
+ if (action !== 'verify')
708
+ throw new Error('usage: ket schema verify [--deployment NAME] [--tenant NAME | --all]');
709
+ if (flag('all')) {
710
+ if (opt('tenant'))
711
+ throw new Error('pass either --tenant NAME or --all, not both');
712
+ const spec = pickFleetSpec(specs);
713
+ const tenants = spec.serve?.tenants;
714
+ if (!tenants)
715
+ throw new Error(`deployment "${spec.name}" serves a single datastore; drop --all`);
716
+ const tenantExists = tenants.exists;
717
+ if (!tenantExists)
718
+ throw new Error(`deployment "${spec.name}" must define serve.tenants.exists(key, config) before schema verification can inspect tenant datastores without creating them`);
719
+ const config = readConfig(process.env, {
720
+ sqliteFile: `.ket/${spec.name}.db`,
721
+ ...spec.serve?.defaults,
722
+ });
723
+ const pool = createAdapterPool({ create: (key) => tenants.open(key, config) });
724
+ let failed = false;
725
+ try {
726
+ const manifest = ws.deployments[spec.name];
727
+ const lines = [];
728
+ for (const key of await tenants.list()) {
729
+ try {
730
+ if (!(await tenantExists(key, config))) {
731
+ failed = true;
732
+ lines.push(`FAIL ${key.padEnd(24)} datastore does not exist; schema verification will not create it`);
733
+ continue;
734
+ }
735
+ const report = await pool.with(key, (adapter) => verifyPhysicalSchema(adapter, manifest));
736
+ lines.push(formatSchemaVerification(key, report));
737
+ failed ||= !report.ok;
738
+ }
739
+ catch (error) {
740
+ failed = true;
741
+ lines.push(`FAIL ${key.padEnd(24)} ${error.message}`);
742
+ }
743
+ }
744
+ console.log(lines.join('\n'));
745
+ }
746
+ finally {
747
+ await pool.close();
748
+ }
749
+ process.exitCode = failed ? 1 : 0;
750
+ }
751
+ else {
752
+ const spec = pickSpec(specs);
753
+ if (!spec.serve)
754
+ throw new Error(`deployment "${spec.name}" declares no serve block, so there is no datastore to verify`);
755
+ const tenant = opt('tenant');
756
+ if (spec.serve.tenants && !tenant)
757
+ throw new Error(`deployment "${spec.name}" has tenant databases; pass --tenant NAME or --all`);
758
+ if (!spec.serve.tenants && tenant)
759
+ throw new Error(`deployment "${spec.name}" serves a single datastore; drop --tenant`);
760
+ const config = readConfig(process.env, {
761
+ sqliteFile: `.ket/${spec.name}.db`,
762
+ ...spec.serve.defaults,
763
+ });
764
+ if (!spec.serve.tenants &&
765
+ !spec.serve.openStore &&
766
+ !config.databaseUrl &&
767
+ config.sqliteFile !== ':memory:' &&
768
+ !existsSync(config.sqliteFile))
769
+ throw new Error(`SQLite datastore "${config.sqliteFile}" does not exist; schema verification will not create it`);
770
+ if (spec.serve.tenants) {
771
+ const tenantExists = spec.serve.tenants.exists;
772
+ if (!tenantExists)
773
+ throw new Error(`deployment "${spec.name}" must define serve.tenants.exists(key, config) before schema verification can inspect tenant datastores without creating them`);
774
+ if (!(await tenantExists(tenant, config)))
775
+ throw new Error(`tenant datastore "${tenant}" does not exist; schema verification will not create it`);
776
+ }
777
+ const adapter = spec.serve.tenants
778
+ ? await spec.serve.tenants.open(tenant, config)
779
+ : await (spec.serve.openStore ?? sqliteStore)(config);
780
+ if (spec.serve.tenants)
781
+ await adapter.open();
782
+ try {
783
+ const report = await verifyPhysicalSchema(adapter, ws.deployments[spec.name]);
784
+ console.log(formatSchemaVerification(tenant ?? spec.name, report));
785
+ process.exitCode = report.ok ? 0 : 1;
786
+ }
787
+ finally {
788
+ await adapter.close();
789
+ }
790
+ }
791
+ }
647
792
  else if (cmd === 'migrate') {
648
- const spec = pickSpec(specs);
649
793
  if (flag('all')) {
650
794
  // The fleet. A deployment that ships a new module has to reach every tenant
651
795
  // database, and one that cannot be opened must not stop the others — a
652
796
  // half-migrated fleet you cannot see is worse than one you can.
797
+ const spec = pickFleetSpec(specs);
653
798
  const tenants = spec.serve?.tenants;
654
799
  if (!tenants)
655
- throw new Error(`app "${spec.name}" serves a single datastore; drop --all`);
800
+ throw new Error(`deployment "${spec.name}" serves a single datastore; drop --all`);
656
801
  const config = readConfig(process.env, spec.serve?.defaults ?? {});
657
802
  const pool = createAdapterPool({ create: (key) => tenants.open(key, config) });
658
803
  try {
659
804
  const keys = await tenants.list();
660
- const m = ws.apps[spec.name];
805
+ const m = ws.deployments[spec.name];
661
806
  const results = await migrateFleet(pool, keys, m, {
662
807
  allowDestructive: flag('allow-destructive'),
663
808
  dryRun: flag('dry-run'),
@@ -669,7 +814,7 @@ try {
669
814
  await pool.close();
670
815
  }
671
816
  }
672
- const [name, m] = pickApp(ws);
817
+ const [name, m] = pickDeployment(ws);
673
818
  const adapter = sqliteAdapter();
674
819
  adapter.open();
675
820
  const snapPath = `.ket/schema.${name}.json`;
@@ -678,8 +823,12 @@ try {
678
823
  const ops = planMigration(prev, next, { allowDestructive: flag('allow-destructive') });
679
824
  for (const sql of renderSql(ops, adapter))
680
825
  console.log(sql + ';');
681
- writeFileSync(snapPath, JSON.stringify(next, null, 2));
682
- console.log(`\n-- ${ops.length} operation(s); schema snapshot written to ${snapPath}`);
826
+ if (flag('dry-run'))
827
+ console.log(`\n-- dry run: ${ops.length} operation(s); schema snapshot unchanged`);
828
+ else {
829
+ writeFileSync(snapPath, JSON.stringify(next, null, 2));
830
+ console.log(`\n-- ${ops.length} operation(s); schema snapshot written to ${snapPath}`);
831
+ }
683
832
  adapter.close();
684
833
  }
685
834
  else {