@zmdb/cli 1.0.0-beta.1

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 (207) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +19 -0
  3. package/dist/__fixtures__/http-client/src/contract.d.ts +38 -0
  4. package/dist/__fixtures__/http-client/src/contract.d.ts.map +1 -0
  5. package/dist/__fixtures__/http-client/src/contract.js +121 -0
  6. package/dist/__fixtures__/http-client/src/contract.js.map +1 -0
  7. package/dist/__fixtures__/http-client/src/models.d.ts +11 -0
  8. package/dist/__fixtures__/http-client/src/models.d.ts.map +1 -0
  9. package/dist/__fixtures__/http-client/src/models.js +2 -0
  10. package/dist/__fixtures__/http-client/src/models.js.map +1 -0
  11. package/dist/__fixtures__/http-client/src/schema.d.ts +4 -0
  12. package/dist/__fixtures__/http-client/src/schema.d.ts.map +1 -0
  13. package/dist/__fixtures__/http-client/src/schema.js +2 -0
  14. package/dist/__fixtures__/http-client/src/schema.js.map +1 -0
  15. package/dist/__fixtures__/http-client/src/unrelated.d.ts +2 -0
  16. package/dist/__fixtures__/http-client/src/unrelated.d.ts.map +1 -0
  17. package/dist/__fixtures__/http-client/src/unrelated.js +2 -0
  18. package/dist/__fixtures__/http-client/src/unrelated.js.map +1 -0
  19. package/dist/__fixtures__/http-client/zmdb.config.d.ts +16 -0
  20. package/dist/__fixtures__/http-client/zmdb.config.d.ts.map +1 -0
  21. package/dist/__fixtures__/http-client/zmdb.config.js +12 -0
  22. package/dist/__fixtures__/http-client/zmdb.config.js.map +1 -0
  23. package/dist/__fixtures__/project/src/schema.d.ts +6 -0
  24. package/dist/__fixtures__/project/src/schema.d.ts.map +1 -0
  25. package/dist/__fixtures__/project/src/schema.js +2 -0
  26. package/dist/__fixtures__/project/src/schema.js.map +1 -0
  27. package/dist/__fixtures__/project/zmdb.config.d.ts +22 -0
  28. package/dist/__fixtures__/project/zmdb.config.d.ts.map +1 -0
  29. package/dist/__fixtures__/project/zmdb.config.js +55 -0
  30. package/dist/__fixtures__/project/zmdb.config.js.map +1 -0
  31. package/dist/application-loader.d.ts +4 -0
  32. package/dist/application-loader.d.ts.map +1 -0
  33. package/dist/application-loader.js +102 -0
  34. package/dist/application-loader.js.map +1 -0
  35. package/dist/args.d.ts +23 -0
  36. package/dist/args.d.ts.map +1 -0
  37. package/dist/args.js +353 -0
  38. package/dist/args.js.map +1 -0
  39. package/dist/atomic.d.ts +13 -0
  40. package/dist/atomic.d.ts.map +1 -0
  41. package/dist/atomic.js +28 -0
  42. package/dist/atomic.js.map +1 -0
  43. package/dist/bin.d.ts +3 -0
  44. package/dist/bin.d.ts.map +1 -0
  45. package/dist/bin.js +4 -0
  46. package/dist/bin.js.map +1 -0
  47. package/dist/commands/check.d.ts +5 -0
  48. package/dist/commands/check.d.ts.map +1 -0
  49. package/dist/commands/check.js +7 -0
  50. package/dist/commands/check.js.map +1 -0
  51. package/dist/commands/client.d.ts +38 -0
  52. package/dist/commands/client.d.ts.map +1 -0
  53. package/dist/commands/client.js +347 -0
  54. package/dist/commands/client.js.map +1 -0
  55. package/dist/commands/codegen.d.ts +9 -0
  56. package/dist/commands/codegen.d.ts.map +1 -0
  57. package/dist/commands/codegen.js +56 -0
  58. package/dist/commands/codegen.js.map +1 -0
  59. package/dist/commands/embed.d.ts +8 -0
  60. package/dist/commands/embed.d.ts.map +1 -0
  61. package/dist/commands/embed.js +17 -0
  62. package/dist/commands/embed.js.map +1 -0
  63. package/dist/commands/export.d.ts +5 -0
  64. package/dist/commands/export.d.ts.map +1 -0
  65. package/dist/commands/export.js +6 -0
  66. package/dist/commands/export.js.map +1 -0
  67. package/dist/commands/generate.d.ts +5 -0
  68. package/dist/commands/generate.d.ts.map +1 -0
  69. package/dist/commands/generate.js +6 -0
  70. package/dist/commands/generate.js.map +1 -0
  71. package/dist/commands/migrate.d.ts +9 -0
  72. package/dist/commands/migrate.d.ts.map +1 -0
  73. package/dist/commands/migrate.js +18 -0
  74. package/dist/commands/migrate.js.map +1 -0
  75. package/dist/commands/new.d.ts +4 -0
  76. package/dist/commands/new.d.ts.map +1 -0
  77. package/dist/commands/new.js +35 -0
  78. package/dist/commands/new.js.map +1 -0
  79. package/dist/commands/pull.d.ts +5 -0
  80. package/dist/commands/pull.d.ts.map +1 -0
  81. package/dist/commands/pull.js +14 -0
  82. package/dist/commands/pull.js.map +1 -0
  83. package/dist/commands/push.d.ts +6 -0
  84. package/dist/commands/push.d.ts.map +1 -0
  85. package/dist/commands/push.js +11 -0
  86. package/dist/commands/push.js.map +1 -0
  87. package/dist/commands/studio.d.ts +31 -0
  88. package/dist/commands/studio.d.ts.map +1 -0
  89. package/dist/commands/studio.js +154 -0
  90. package/dist/commands/studio.js.map +1 -0
  91. package/dist/commands/upgrade.d.ts +5 -0
  92. package/dist/commands/upgrade.d.ts.map +1 -0
  93. package/dist/commands/upgrade.js +11 -0
  94. package/dist/commands/upgrade.js.map +1 -0
  95. package/dist/database.d.ts +4 -0
  96. package/dist/database.d.ts.map +1 -0
  97. package/dist/database.js +8 -0
  98. package/dist/database.js.map +1 -0
  99. package/dist/errors.d.ts +7 -0
  100. package/dist/errors.d.ts.map +1 -0
  101. package/dist/errors.js +20 -0
  102. package/dist/errors.js.map +1 -0
  103. package/dist/index.d.ts +34 -0
  104. package/dist/index.d.ts.map +1 -0
  105. package/dist/index.js +626 -0
  106. package/dist/index.js.map +1 -0
  107. package/dist/lifecycle.d.ts +6 -0
  108. package/dist/lifecycle.d.ts.map +1 -0
  109. package/dist/lifecycle.js +75 -0
  110. package/dist/lifecycle.js.map +1 -0
  111. package/dist/migration-project.d.ts +14 -0
  112. package/dist/migration-project.d.ts.map +1 -0
  113. package/dist/migration-project.js +74 -0
  114. package/dist/migration-project.js.map +1 -0
  115. package/dist/output.d.ts +27 -0
  116. package/dist/output.d.ts.map +1 -0
  117. package/dist/output.js +60 -0
  118. package/dist/output.js.map +1 -0
  119. package/dist/repl.d.ts +48 -0
  120. package/dist/repl.d.ts.map +1 -0
  121. package/dist/repl.js +265 -0
  122. package/dist/repl.js.map +1 -0
  123. package/dist/scaffold.d.ts +28 -0
  124. package/dist/scaffold.d.ts.map +1 -0
  125. package/dist/scaffold.js +414 -0
  126. package/dist/scaffold.js.map +1 -0
  127. package/dist/studio/index.d.ts +20 -0
  128. package/dist/studio/index.d.ts.map +1 -0
  129. package/dist/studio/index.js +607 -0
  130. package/dist/studio/index.js.map +1 -0
  131. package/dist/templates/command.d.ts +3 -0
  132. package/dist/templates/command.d.ts.map +1 -0
  133. package/dist/templates/command.js +65 -0
  134. package/dist/templates/command.js.map +1 -0
  135. package/dist/templates/controller.d.ts +3 -0
  136. package/dist/templates/controller.d.ts.map +1 -0
  137. package/dist/templates/controller.js +41 -0
  138. package/dist/templates/controller.js.map +1 -0
  139. package/dist/templates/index.d.ts +4 -0
  140. package/dist/templates/index.d.ts.map +1 -0
  141. package/dist/templates/index.js +18 -0
  142. package/dist/templates/index.js.map +1 -0
  143. package/dist/templates/module.d.ts +3 -0
  144. package/dist/templates/module.d.ts.map +1 -0
  145. package/dist/templates/module.js +41 -0
  146. package/dist/templates/module.js.map +1 -0
  147. package/dist/templates/project.d.ts +3 -0
  148. package/dist/templates/project.d.ts.map +1 -0
  149. package/dist/templates/project.js +280 -0
  150. package/dist/templates/project.js.map +1 -0
  151. package/dist/templates/repository.d.ts +3 -0
  152. package/dist/templates/repository.d.ts.map +1 -0
  153. package/dist/templates/repository.js +66 -0
  154. package/dist/templates/repository.js.map +1 -0
  155. package/dist/templates/schema.d.ts +3 -0
  156. package/dist/templates/schema.d.ts.map +1 -0
  157. package/dist/templates/schema.js +36 -0
  158. package/dist/templates/schema.js.map +1 -0
  159. package/dist/templates/types.d.ts +25 -0
  160. package/dist/templates/types.d.ts.map +1 -0
  161. package/dist/templates/types.js +17 -0
  162. package/dist/templates/types.js.map +1 -0
  163. package/package.json +72 -0
  164. package/src/__fixtures__/http-client/package.json +5 -0
  165. package/src/__fixtures__/http-client/src/contract.ts +85 -0
  166. package/src/__fixtures__/http-client/src/models.ts +12 -0
  167. package/src/__fixtures__/http-client/src/schema.ts +3 -0
  168. package/src/__fixtures__/http-client/src/unrelated.ts +1 -0
  169. package/src/__fixtures__/http-client/tsconfig.json +17 -0
  170. package/src/__fixtures__/http-client/zmdb.config.ts +12 -0
  171. package/src/__fixtures__/project/package.json +5 -0
  172. package/src/__fixtures__/project/src/schema.ts +6 -0
  173. package/src/__fixtures__/project/tsconfig.json +24 -0
  174. package/src/__fixtures__/project/zmdb.config.ts +57 -0
  175. package/src/application-loader.ts +108 -0
  176. package/src/args.ts +391 -0
  177. package/src/atomic.ts +43 -0
  178. package/src/bin.ts +5 -0
  179. package/src/commands/check.ts +11 -0
  180. package/src/commands/client.ts +415 -0
  181. package/src/commands/codegen.ts +73 -0
  182. package/src/commands/embed.ts +36 -0
  183. package/src/commands/export.ts +10 -0
  184. package/src/commands/generate.ts +14 -0
  185. package/src/commands/migrate.ts +38 -0
  186. package/src/commands/new.ts +48 -0
  187. package/src/commands/pull.ts +32 -0
  188. package/src/commands/push.ts +21 -0
  189. package/src/commands/studio.ts +200 -0
  190. package/src/commands/upgrade.ts +14 -0
  191. package/src/database.ts +9 -0
  192. package/src/errors.ts +19 -0
  193. package/src/index.ts +710 -0
  194. package/src/lifecycle.ts +73 -0
  195. package/src/migration-project.ts +91 -0
  196. package/src/output.ts +77 -0
  197. package/src/repl.ts +328 -0
  198. package/src/scaffold.ts +485 -0
  199. package/src/studio/index.ts +674 -0
  200. package/src/templates/command.ts +66 -0
  201. package/src/templates/controller.ts +42 -0
  202. package/src/templates/index.ts +29 -0
  203. package/src/templates/module.ts +42 -0
  204. package/src/templates/project.ts +281 -0
  205. package/src/templates/repository.ts +67 -0
  206. package/src/templates/schema.ts +37 -0
  207. package/src/templates/types.ts +46 -0
@@ -0,0 +1,73 @@
1
+ import type { ResolvedConfig } from '@zmdb/compiler/config';
2
+
3
+ /** Own only the driver requested during this invocation, including non-enumerable disposal methods. */
4
+ export async function withConfiguredDriver<T>(
5
+ config: ResolvedConfig,
6
+ run: (config: ResolvedConfig) => Promise<T>,
7
+ ): Promise<T> {
8
+ const create = config.driver;
9
+ if (create === undefined) return run(config);
10
+ let opened: Awaited<ReturnType<typeof create>> | undefined;
11
+ let pending: Promise<Awaited<ReturnType<typeof create>>> | undefined;
12
+ let outcome: { readonly ok: true; readonly value: T } | { readonly ok: false; readonly error: unknown };
13
+ try {
14
+ outcome = {
15
+ ok: true,
16
+ value: await run({
17
+ ...config,
18
+ driver() {
19
+ pending ??= Promise.resolve()
20
+ .then(create)
21
+ .then(driver => {
22
+ opened = driver;
23
+ return driver;
24
+ });
25
+ return pending;
26
+ },
27
+ }),
28
+ };
29
+ } catch (error) {
30
+ outcome = { ok: false, error };
31
+ }
32
+ try {
33
+ if (opened !== undefined) {
34
+ const disposeAsync: unknown = Reflect.get(opened, Symbol.asyncDispose);
35
+ const dispose: unknown = Reflect.get(opened, Symbol.dispose);
36
+ if (typeof disposeAsync === 'function') await disposeAsync.call(opened);
37
+ else if (typeof dispose === 'function') dispose.call(opened);
38
+ }
39
+ } catch (cleanupError) {
40
+ if (!outcome.ok) throw new SuppressedError(cleanupError, outcome.error, 'command and driver cleanup failed');
41
+ throw cleanupError;
42
+ }
43
+ if (!outcome.ok) throw outcome.error;
44
+ return outcome.value;
45
+ }
46
+
47
+ /** Translate process termination into the command's existing cancellable lifetime. */
48
+ export async function withSignals<T>(run: (until: Promise<void>) => Promise<T>): Promise<T | undefined> {
49
+ const until = Promise.withResolvers<void>();
50
+ let interrupted = false;
51
+ const stop = (): void => {
52
+ interrupted = true;
53
+ until.resolve();
54
+ };
55
+ process.once('SIGINT', stop);
56
+ process.once('SIGTERM', stop);
57
+ try {
58
+ return await run(until.promise);
59
+ } catch (error) {
60
+ // A terminal signal also terminates the TypeScript child in the same process group.
61
+ // The watch has already run its cleanup before this closed-channel error arrives.
62
+ if (
63
+ interrupted &&
64
+ error instanceof Error &&
65
+ error.message.startsWith('Unexpected EOF while reading from child process')
66
+ )
67
+ return undefined;
68
+ throw error;
69
+ } finally {
70
+ process.off('SIGINT', stop);
71
+ process.off('SIGTERM', stop);
72
+ }
73
+ }
@@ -0,0 +1,91 @@
1
+ import type { ResolvedConfig } from '@zmdb/compiler/config';
2
+ import { schemasFromFiles } from '@zmdb/compiler/testing';
3
+ import type { MigrationDriver, MigrationProject, SnapshotableSchema } from '@zmdb/migrations';
4
+ import { emitDeclarations } from '@zmdb/migrations/declarations';
5
+ import { dialectName, type Introspector } from '@zmdb/sql';
6
+ import type { FormatConfig } from 'oxfmt';
7
+
8
+ import { configuredIntrospector } from './database.js';
9
+ import { CliInvocationError } from './errors.js';
10
+
11
+ const FORMAT_OPTIONS: FormatConfig = {
12
+ arrowParens: 'avoid',
13
+ bracketSpacing: true,
14
+ endOfLine: 'lf',
15
+ insertFinalNewline: true,
16
+ objectWrap: 'preserve',
17
+ printWidth: 120,
18
+ quoteProps: 'as-needed',
19
+ semi: true,
20
+ singleQuote: true,
21
+ sortImports: true,
22
+ sortPackageJson: true,
23
+ tabWidth: 2,
24
+ trailingComma: 'all',
25
+ useTabs: false,
26
+ };
27
+
28
+ interface MigrationProjectOptions {
29
+ readonly schemas?: readonly SnapshotableSchema[];
30
+ readonly driver?: MigrationDriver;
31
+ readonly introspector?: Introspector;
32
+ }
33
+
34
+ export function migrationProject(config: ResolvedConfig, options: MigrationProjectOptions = {}): MigrationProject {
35
+ return {
36
+ configPath: config.configPath,
37
+ outDir: config.outDir,
38
+ dialect: config.dialect,
39
+ schemas: options.schemas ?? [],
40
+ emitDeclarations,
41
+ formatSource,
42
+ ...(config.migrations === undefined ? {} : { migrations: config.migrations }),
43
+ ...(config.introspect === undefined ? {} : { introspect: config.introspect }),
44
+ ...(options.driver === undefined ? {} : { driver: options.driver }),
45
+ ...(options.introspector === undefined ? {} : { introspector: options.introspector }),
46
+ };
47
+ }
48
+
49
+ async function formatSource(path: string, source: string): Promise<string> {
50
+ const { format } = await import('oxfmt');
51
+ const result = await format(path, source, FORMAT_OPTIONS);
52
+ if (result.errors.length > 0) {
53
+ throw new TypeError(`oxfmt could not format generated ${path}: ${JSON.stringify(result.errors)}`);
54
+ }
55
+ return result.code;
56
+ }
57
+
58
+ export function reflectedMigrationProject(config: ResolvedConfig, driver?: MigrationDriver): MigrationProject {
59
+ const schemas = schemasFromFiles(config.schemaFiles, {
60
+ project: config.project,
61
+ naming: config.resolvedNaming,
62
+ });
63
+ return migrationProject(config, {
64
+ schemas,
65
+ ...(driver === undefined ? {} : { driver }),
66
+ ...(driver === undefined ? {} : { introspector: configuredIntrospector(driver.dialect) }),
67
+ });
68
+ }
69
+
70
+ export async function configuredMigrationDriver(config: ResolvedConfig): Promise<MigrationDriver> {
71
+ if (config.driver === undefined) {
72
+ throw new CliInvocationError(`config ${config.configPath} needs a driver for this command`);
73
+ }
74
+ const driver = await config.driver();
75
+ if (driver.dialect === undefined) {
76
+ throw new CliInvocationError(`config ${config.configPath} driver needs an explicit database dialect object`);
77
+ }
78
+ if (dialectName(driver.dialect) !== dialectName(config.dialect)) {
79
+ throw new CliInvocationError(
80
+ `config ${config.configPath} declares ${dialectName(config.dialect)} but its driver declares ${dialectName(driver.dialect)}`,
81
+ );
82
+ }
83
+ return { ...driver, dialect: driver.dialect };
84
+ }
85
+
86
+ export function rethrowProjectError(error: unknown): never {
87
+ if (error instanceof Error && error.name === 'MigrationProjectError') {
88
+ throw new CliInvocationError(error.message);
89
+ }
90
+ throw error;
91
+ }
package/src/output.ts ADDED
@@ -0,0 +1,77 @@
1
+ export interface CliResult<T> {
2
+ readonly ok: boolean;
3
+ readonly command: string;
4
+ readonly config: string;
5
+ readonly result?: T;
6
+ readonly errors?: readonly { readonly message: string; readonly path?: string }[];
7
+ }
8
+
9
+ export interface OutputStreams {
10
+ readonly stdout: (text: string) => void;
11
+ readonly stderr: (text: string) => void;
12
+ }
13
+
14
+ /** The only place a non-interactive command decides which stream receives text. */
15
+ export class CliOutput {
16
+ readonly #command: string;
17
+ readonly #config: string;
18
+ readonly #json: boolean;
19
+ readonly #streams: OutputStreams;
20
+
21
+ constructor(command: string, config: string, json: boolean, streams: OutputStreams) {
22
+ this.#command = command;
23
+ this.#config = config;
24
+ this.#json = json;
25
+ this.#streams = streams;
26
+ }
27
+
28
+ withConfig(config: string): CliOutput {
29
+ return new CliOutput(this.#command, config, this.#json, this.#streams);
30
+ }
31
+
32
+ help(text: string): number {
33
+ return this.result({ help: text }, text);
34
+ }
35
+
36
+ writeStdout(text: string): void {
37
+ this.#streams.stdout(text);
38
+ }
39
+
40
+ writeStderr(text: string): void {
41
+ this.#streams.stderr(text);
42
+ }
43
+
44
+ progress(text: string): void {
45
+ if (this.#json) this.writeStderr(text);
46
+ else this.writeStdout(text);
47
+ }
48
+
49
+ result<T>(value: T, human: string, exitCode = 0): number {
50
+ if (this.#json) {
51
+ const body: CliResult<T> = {
52
+ ok: exitCode === 0,
53
+ command: this.#command,
54
+ config: this.#config,
55
+ result: value,
56
+ };
57
+ this.writeStdout(`${JSON.stringify(body)}\n`);
58
+ } else {
59
+ this.writeStdout(human);
60
+ }
61
+ return exitCode;
62
+ }
63
+
64
+ failure(message: string, exitCode: 1 | 2, path?: string): number {
65
+ if (this.#json) {
66
+ const body: CliResult<never> = {
67
+ ok: false,
68
+ command: this.#command,
69
+ config: this.#config,
70
+ errors: [{ message, ...(path === undefined ? {} : { path }) }],
71
+ };
72
+ this.writeStdout(`${JSON.stringify(body)}\n`);
73
+ }
74
+ this.writeStderr(`zmdb ${this.#command}: ${message}\n`);
75
+ return exitCode;
76
+ }
77
+ }
package/src/repl.ts ADDED
@@ -0,0 +1,328 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { homedir } from 'node:os';
4
+ import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
5
+ import { start, type REPLServer } from 'node:repl';
6
+
7
+ import type { Token } from '@zmdb/app/di';
8
+ import { moduleDefOf, type ModuleClass } from '@zmdb/app/modules';
9
+ import { createApp, type WebApplication } from '@zmdb/web/app';
10
+ import { describeGraph, renderTree, type GraphDescription } from '@zmdb/web/devtools';
11
+ import type { WebRequest, WebResponse } from '@zmdb/web/pipeline';
12
+
13
+ export interface ReplGet {
14
+ <T>(token: Token<T>): T;
15
+ (description: string): unknown;
16
+ }
17
+
18
+ export interface ReplScope {
19
+ readonly app: WebApplication;
20
+ readonly container: WebApplication['container'];
21
+ readonly get: ReplGet;
22
+ readonly tokens: readonly string[];
23
+ readonly describe: () => string;
24
+ readonly request: (request: WebRequest | string) => Promise<WebResponse>;
25
+ readonly load: (name: string) => Promise<void>;
26
+ }
27
+
28
+ export interface ReplSessionOptions {
29
+ readonly configPath: string;
30
+ readonly moduleSpec: string;
31
+ readonly cwd?: string;
32
+ readonly input?: NodeJS.ReadableStream;
33
+ readonly output?: NodeJS.WritableStream;
34
+ readonly stderr?: (text: string) => void;
35
+ readonly historyPath?: string | null;
36
+ readonly terminal?: boolean;
37
+ }
38
+
39
+ export interface ReplSession extends AsyncDisposable {
40
+ readonly app: WebApplication;
41
+ readonly graph: GraphDescription;
42
+ readonly scope: ReplScope;
43
+ readonly server: REPLServer;
44
+ readonly closed: Promise<void>;
45
+ evaluate(source: string): Promise<unknown>;
46
+ }
47
+
48
+ interface TokenRecord {
49
+ readonly token: Token<unknown>;
50
+ readonly description: string;
51
+ }
52
+
53
+ /**
54
+ * Boot the real application and start a local, stream-only Node REPL.
55
+ *
56
+ * No socket or adapter is created. The caller owns the returned async resource;
57
+ * disposing it closes the prompt and runs the application's normal shutdown
58
+ * lifecycle.
59
+ */
60
+ export async function createReplSession(rootModule: ModuleClass, options: ReplSessionOptions): Promise<ReplSession> {
61
+ const input = options.input ?? process.stdin;
62
+ const output = options.output ?? process.stdout;
63
+ const stderr = options.stderr ?? (text => process.stderr.write(text));
64
+ const cwd = options.cwd ?? process.cwd();
65
+ const graph = describeGraph(rootModule);
66
+ const tokenRecords = providerTokensOf(rootModule);
67
+ const app = createApp(rootModule);
68
+
69
+ try {
70
+ await app.init();
71
+ } catch (error) {
72
+ try {
73
+ await app[Symbol.asyncDispose]();
74
+ } catch (cleanupError) {
75
+ throw new SuppressedError(cleanupError, error, 'REPL operation and application cleanup failed');
76
+ }
77
+ throw error;
78
+ }
79
+
80
+ let server: REPLServer | undefined;
81
+ try {
82
+ const scope = replScope(app, graph, tokenRecords);
83
+ stderr(
84
+ [
85
+ 'zmdb repl — development shell with the application container',
86
+ `config: ${options.configPath}`,
87
+ 'dialect: application-owned (the application does not read zmdb.config.ts)',
88
+ 'database: application-owned (provider values are not inspected or printed)',
89
+ `module: ${options.moduleSpec} (${rootModule.name || '<anonymous>'})`,
90
+ 'scope: app, container, get(tokenOrDescription), tokens, describe(), request(req), load(name)',
91
+ `tokens: ${scope.tokens.length === 0 ? '(none)' : scope.tokens.join(', ')}`,
92
+ `history: ${options.historyPath === null ? 'disabled' : (options.historyPath ?? 'disabled')}`,
93
+ '',
94
+ ].join('\n'),
95
+ );
96
+
97
+ server = start({
98
+ prompt: 'zmdb> ',
99
+ input,
100
+ output,
101
+ terminal: options.terminal,
102
+ ignoreUndefined: true,
103
+ });
104
+ const activeServer = server;
105
+ let closed = false;
106
+ const enteredLines: string[] = [];
107
+ if (options.historyPath !== null && options.historyPath !== undefined) {
108
+ activeServer.on('line', line => {
109
+ if (line.length > 0 && line !== '.exit') {
110
+ enteredLines.unshift(line);
111
+ }
112
+ });
113
+ }
114
+ const historyReady = Promise.withResolvers<() => Promise<void>>();
115
+ const whenClosed = new Promise<void>((resolveClosed, rejectClosed) => {
116
+ activeServer.once('exit', () => {
117
+ closed = true;
118
+ void historyReady.promise.then(flush => flush()).then(resolveClosed, rejectClosed);
119
+ });
120
+ });
121
+ Object.assign(activeServer.context, scope);
122
+
123
+ try {
124
+ const flushHistory =
125
+ options.historyPath === null || options.historyPath === undefined
126
+ ? async (): Promise<void> => {}
127
+ : await configureHistory(activeServer, options.historyPath, cwd, enteredLines);
128
+ historyReady.resolve(flushHistory);
129
+ } catch (error) {
130
+ historyReady.resolve(async (): Promise<void> => {});
131
+ throw error;
132
+ }
133
+
134
+ let disposed = false;
135
+ return {
136
+ app,
137
+ graph,
138
+ scope,
139
+ server: activeServer,
140
+ closed: whenClosed,
141
+ evaluate: source => evaluate(activeServer, source),
142
+ [Symbol.asyncDispose]: async () => {
143
+ if (disposed) {
144
+ return;
145
+ }
146
+ disposed = true;
147
+ if (!closed) {
148
+ activeServer.close();
149
+ }
150
+ let outcome: { readonly ok: true } | { readonly ok: false; readonly error: unknown };
151
+ try {
152
+ await whenClosed;
153
+ outcome = { ok: true };
154
+ } catch (error) {
155
+ outcome = { ok: false, error };
156
+ }
157
+ try {
158
+ await app[Symbol.asyncDispose]();
159
+ } catch (cleanupError) {
160
+ if (!outcome.ok)
161
+ throw new SuppressedError(cleanupError, outcome.error, 'REPL close and application cleanup failed');
162
+ throw cleanupError;
163
+ }
164
+ if (!outcome.ok) throw outcome.error;
165
+ },
166
+ };
167
+ } catch (error) {
168
+ server?.close();
169
+ try {
170
+ await app[Symbol.asyncDispose]();
171
+ } catch (cleanupError) {
172
+ throw new SuppressedError(cleanupError, error, 'REPL operation and application cleanup failed');
173
+ }
174
+ throw error;
175
+ }
176
+ }
177
+
178
+ /** Resolve the default history path without ever making it relative to the project. */
179
+ export function replHistoryPath(
180
+ environment: Readonly<Record<string, string | undefined>> = process.env,
181
+ homeDirectory: string = homedir(),
182
+ ): string {
183
+ const configured = environment['ZMDB_REPL_HISTORY'];
184
+ if (configured === undefined || configured.length === 0) {
185
+ return resolve(homeDirectory, '.zmdb_repl_history');
186
+ }
187
+ return isAbsolute(configured) ? configured : resolve(homeDirectory, configured);
188
+ }
189
+
190
+ function replScope(app: WebApplication, graph: GraphDescription, records: readonly TokenRecord[]): ReplScope {
191
+ const get = createGet(app, records);
192
+ const tokens = graph.providers.map(provider => provider.token).toSorted();
193
+ return {
194
+ app,
195
+ container: app.container,
196
+ get,
197
+ tokens,
198
+ describe: () => renderTree(graph, graph.providers.length <= 50 ? { providers: true } : undefined),
199
+ request: request => {
200
+ const input: WebRequest = typeof request === 'string' ? { method: 'GET', path: request, headers: {} } : request;
201
+ return app.handle(input);
202
+ },
203
+ load: async name => {
204
+ const matches = app.lazy.filter(handle => handle.name === name);
205
+ if (matches.length === 0) {
206
+ throw new Error(`zmdb repl: no lazy module named "${name}"`);
207
+ }
208
+ if (matches.length > 1) {
209
+ throw new Error(`zmdb repl: lazy module name "${name}" is ambiguous (${String(matches.length)} handles)`);
210
+ }
211
+ await matches[0]?.load();
212
+ },
213
+ };
214
+ }
215
+
216
+ function createGet(app: WebApplication, records: readonly TokenRecord[]): ReplGet {
217
+ return <T>(tokenOrDescription: Token<T> | string): T | unknown => {
218
+ if (typeof tokenOrDescription !== 'string') {
219
+ return app.container.resolve(tokenOrDescription);
220
+ }
221
+ const matches = records.filter(record => record.description === tokenOrDescription);
222
+ if (matches.length === 0) {
223
+ throw new Error(`zmdb repl: no provider token is described as "${tokenOrDescription}"`);
224
+ }
225
+ if (matches.length > 1) {
226
+ throw new Error(
227
+ `zmdb repl: token description "${tokenOrDescription}" is ambiguous (${String(matches.length)} distinct tokens)`,
228
+ );
229
+ }
230
+ const record = matches[0];
231
+ if (record === undefined) {
232
+ throw new Error(`zmdb repl: no provider token is described as "${tokenOrDescription}"`);
233
+ }
234
+ return app.container.resolve(record.token);
235
+ };
236
+ }
237
+
238
+ function providerTokensOf(rootModule: ModuleClass): readonly TokenRecord[] {
239
+ const seenModules = new Set<ModuleClass>();
240
+ const seenTokens = new Set<Token<unknown>>();
241
+ const records: TokenRecord[] = [];
242
+
243
+ const visit = (moduleClass: ModuleClass): void => {
244
+ if (seenModules.has(moduleClass)) {
245
+ return;
246
+ }
247
+ seenModules.add(moduleClass);
248
+ const definition = moduleDefOf(moduleClass);
249
+ for (const provider of definition?.providers ?? []) {
250
+ if (!seenTokens.has(provider.token)) {
251
+ seenTokens.add(provider.token);
252
+ records.push({ token: provider.token, description: provider.token.description });
253
+ }
254
+ }
255
+ for (const imported of definition?.imports ?? []) {
256
+ visit(typeof imported === 'function' ? imported : imported.module);
257
+ }
258
+ };
259
+
260
+ visit(rootModule);
261
+ return records;
262
+ }
263
+
264
+ async function configureHistory(
265
+ server: REPLServer,
266
+ historyPath: string,
267
+ cwd: string,
268
+ enteredLines: readonly string[],
269
+ ): Promise<() => Promise<void>> {
270
+ if (isWithin(projectRootOf(cwd), historyPath)) {
271
+ throw new Error(`zmdb repl: history path "${historyPath}" must be outside the project tree`);
272
+ }
273
+ await mkdir(dirname(historyPath), { recursive: true, mode: 0o700 });
274
+ let existing = '';
275
+ try {
276
+ existing = await readFile(historyPath, 'utf8');
277
+ } catch (error) {
278
+ if (!isNotFound(error)) {
279
+ throw error;
280
+ }
281
+ }
282
+ await writeFile(historyPath, existing, { encoding: 'utf8', mode: 0o600 });
283
+ await chmod(historyPath, 0o600);
284
+ const existingLines = existing.split(/\r?\n/).filter(line => line.length > 0);
285
+ Reflect.set(server, 'history', existingLines);
286
+
287
+ return async () => {
288
+ const lines = [...enteredLines, ...existingLines];
289
+ await writeFile(historyPath, lines.join('\n'), { encoding: 'utf8', mode: 0o600 });
290
+ await chmod(historyPath, 0o600);
291
+ };
292
+ }
293
+
294
+ function projectRootOf(cwd: string): string {
295
+ let current = resolve(cwd);
296
+ while (true) {
297
+ if (existsSync(resolve(current, 'package.json'))) {
298
+ return current;
299
+ }
300
+ const parent = dirname(current);
301
+ if (parent === current) {
302
+ return resolve(cwd);
303
+ }
304
+ current = parent;
305
+ }
306
+ }
307
+
308
+ function isWithin(base: string, target: string): boolean {
309
+ const path = relative(resolve(base), resolve(target));
310
+ return path === '' || (!path.startsWith(`..${sep}`) && path !== '..' && !isAbsolute(path));
311
+ }
312
+
313
+ function isNotFound(error: unknown): boolean {
314
+ return error instanceof Error && 'code' in error && error.code === 'ENOENT';
315
+ }
316
+
317
+ function evaluate(server: REPLServer, source: string): Promise<unknown> {
318
+ const command = source.endsWith('\n') ? source : `${source}\n`;
319
+ return new Promise<unknown>((resolveResult, rejectResult) => {
320
+ server.eval(command, server.context, 'zmdb-repl', (error, result) => {
321
+ if (error === null) {
322
+ resolveResult(result);
323
+ } else {
324
+ rejectResult(error);
325
+ }
326
+ });
327
+ });
328
+ }