@znight/cli 0.6.2 → 0.6.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@znight/cli",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "description": "Znight CLI: scaffold projects, generate code, run dev server and builds",
5
5
  "license": "MIT",
6
6
  "author": "Ugho STEPHAN",
@@ -41,7 +41,7 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@clack/prompts": "^1.8.1",
44
- "@znight/core": "0.6.2",
44
+ "@znight/core": "0.6.3",
45
45
  "citty": "^0.2.2",
46
46
  "picocolors": "^1.1.1"
47
47
  },
@@ -50,14 +50,14 @@
50
50
  },
51
51
  "devDependencies": {
52
52
  "vite": "8.3.5",
53
- "@znight/compiler": "0.6.2",
54
- "@znight/openapi": "0.6.2",
55
- "@znight/schedule": "0.6.2"
53
+ "@znight/compiler": "0.6.3",
54
+ "@znight/openapi": "0.6.3",
55
+ "@znight/schedule": "0.6.3"
56
56
  },
57
57
  "peerDependencies": {
58
- "@znight/compiler": "^0.6.2",
58
+ "@znight/compiler": "^0.6.3",
59
59
  "vite": "^8.0.0",
60
- "@znight/openapi": "^0.6.2"
60
+ "@znight/openapi": "^0.6.3"
61
61
  },
62
62
  "peerDependenciesMeta": {
63
63
  "@znight/compiler": {
@@ -16,7 +16,7 @@ import { resolve } from 'node:path'
16
16
  import { defineCommand } from '../command.ts'
17
17
  import { ORPHAN_GUARD_POLL_MS, orphanGuard } from '../dev/orphan.ts'
18
18
  import { AppSupervisor } from '../dev/supervisor.ts'
19
- import { CliError } from '../errors.ts'
19
+ import { CliError, errorMessage } from '../errors.ts'
20
20
  import { fallbackFormatDiagnostics } from '../integrations/compiler.ts'
21
21
  import { exitCodeOf } from '../integrations/process.ts'
22
22
  import { createOutputTracker, formatBuildError, watchBuild } from '../integrations/vite.ts'
@@ -221,6 +221,16 @@ async function printDiagnostics(cli: CliContext, root: string, diagnostics: read
221
221
  cli.reporter.logError(text.trimEnd())
222
222
  }
223
223
 
224
+ /**
225
+ * The signal stopping the application when the CLI received `signal`. On Windows the application
226
+ * shares the console: it received Ctrl+C and the console closing itself, and `kill()` would be a
227
+ * forced termination, so it is only waited for (`null`).
228
+ */
229
+ function stopSignal(cli: CliContext, signal: NodeJS.Signals): NodeJS.Signals | null {
230
+ if (cli.runtime.platform === 'win32' && (signal === 'SIGINT' || signal === 'SIGHUP')) return null
231
+ return signal === 'SIGINT' ? 'SIGINT' : 'SIGTERM'
232
+ }
233
+
224
234
  function waitForTermination(cli: CliContext, watcher: BuildWatcher, supervisor: AppSupervisor): Promise<number> {
225
235
  return new Promise<number>(resolveExit => {
226
236
  const dispose = onTermination(
@@ -229,14 +239,21 @@ function waitForTermination(cli: CliContext, watcher: BuildWatcher, supervisor:
229
239
  // SIGHUP: the terminal is gone, nothing can be printed
230
240
  if (signal !== 'SIGHUP') cli.reporter.info(cli.colors.dim('Stopping... (press Ctrl+C again to force)'))
231
241
  void (async () => {
242
+ // the application stops gracefully on SIGINT or SIGTERM; stop() refuses new starts right
243
+ // away, so a restart still debounced cannot start while Vite closes its watcher
244
+ const stopped = supervisor.stop(stopSignal(cli, signal))
232
245
  try {
233
246
  await watcher.close()
234
- } finally {
235
- // the application stops gracefully on SIGINT or SIGTERM
236
- await supervisor.stop(signal === 'SIGINT' ? 'SIGINT' : 'SIGTERM')
237
- dispose()
238
- resolveExit(0)
247
+ } catch (error) {
248
+ cli.reporter.error(`Cannot close the Vite build watcher: ${errorMessage(error)}`)
249
+ }
250
+ try {
251
+ await stopped
252
+ } catch (error) {
253
+ cli.reporter.error(`Cannot stop the application: ${errorMessage(error)}`)
239
254
  }
255
+ dispose()
256
+ resolveExit(0)
240
257
  })()
241
258
  },
242
259
  () => {
@@ -248,11 +248,14 @@ async function watch(cli: CliContext, compiler: CompilerApi, options: ZnightOpti
248
248
  const incremental: IncrementalCompiler = compiler.createIncremental(options)
249
249
  let state: WatchState = { known: new Set(), roots: [] }
250
250
  let pending = new Set<string>()
251
- let running = false
251
+ let running: Promise<void> | undefined
252
+ // set on Ctrl+C: a generation in flight must neither watch again nor loop
253
+ let closed = false
252
254
  let timer: ReturnType<typeof setTimeout> | undefined
253
255
 
254
256
  // which events matter is the compiler's call: the CLI only tells their kind, which fs.watch does not
255
257
  const watchers = new TargetWatchers(cli.services.watch, file => {
258
+ if (closed) return
256
259
  const path = realPath(file)
257
260
  if (!incremental.isRelevantChange(changeKind(path, state.known), path, state.known, state.roots)) return
258
261
  pending.add(path)
@@ -263,33 +266,36 @@ async function watch(cli: CliContext, compiler: CompilerApi, options: ZnightOpti
263
266
  }, 50)
264
267
  })
265
268
 
266
- const run = async (initial: boolean): Promise<void> => {
267
- if (running) return
268
- running = true
269
+ const run = (initial: boolean): Promise<void> => {
270
+ if (running !== undefined || closed) return running ?? Promise.resolve()
271
+ running = generations(initial).finally(() => {
272
+ running = undefined
273
+ })
274
+ return running
275
+ }
276
+
277
+ const generations = async (initial: boolean): Promise<void> => {
269
278
  let first = initial
270
- try {
271
- do {
272
- const files = pending
273
- pending = new Set()
274
- const started = performance.now()
275
- let known = state.known
276
- try {
277
- const result = await incremental.generate(first ? undefined : classifyChanges(files, state.known))
278
- if (result.watchFiles !== undefined) {
279
- known = new Set(result.watchFiles.map(file => toPosixPath(resolve(file))))
280
- }
281
- report(cli, compiler, result, root, performance.now() - started)
282
- } catch (error) {
283
- cli.reporter.error(`Generation failed: ${errorMessage(error)}`)
279
+ do {
280
+ const files = pending
281
+ pending = new Set()
282
+ const started = performance.now()
283
+ let known = state.known
284
+ try {
285
+ const result = await incremental.generate(first ? undefined : classifyChanges(files, state.known))
286
+ if (result.watchFiles !== undefined) {
287
+ known = new Set(result.watchFiles.map(file => toPosixPath(resolve(file))))
284
288
  }
285
- // the include directories of the source libraries are known once compiled
286
- state = { known, roots: incremental.includeRoots().map(realPath) }
287
- watchers.update(watchTargets(realRoot, state.roots, state.known), files)
288
- first = false
289
- } while (pending.size > 0)
290
- } finally {
291
- running = false
292
- }
289
+ report(cli, compiler, result, root, performance.now() - started)
290
+ } catch (error) {
291
+ cli.reporter.error(`Generation failed: ${errorMessage(error)}`)
292
+ }
293
+ if (closed) return
294
+ // the include directories of the source libraries are known once compiled
295
+ state = { known, roots: incremental.includeRoots().map(realPath) }
296
+ watchers.update(watchTargets(realRoot, state.roots, state.known), files)
297
+ first = false
298
+ } while (pending.size > 0 && !closed)
293
299
  }
294
300
 
295
301
  await run(true)
@@ -299,10 +305,13 @@ async function watch(cli: CliContext, compiler: CompilerApi, options: ZnightOpti
299
305
  const dispose = onTermination(
300
306
  cli,
301
307
  () => {
308
+ closed = true
302
309
  if (timer !== undefined) clearTimeout(timer)
303
310
  watchers.close()
304
311
  dispose()
305
- resolveExit(0)
312
+ // the generation in flight finishes writing its files first
313
+ const exit = (): void => resolveExit(0)
314
+ void (running ?? Promise.resolve()).then(exit, exit)
306
315
  },
307
316
  () => undefined,
308
317
  )
@@ -2,7 +2,7 @@
2
2
  import type { Command } from '../command.ts'
3
3
  import type { CliContext } from '../context.ts'
4
4
  import type { Prompts } from '../integrations/prompts.ts'
5
- import type { Feature, HttpAdapterName, PackageManager } from '../scaffold/options.ts'
5
+ import type { Feature, HttpAdapterName, OpenApiUiName, PackageManager } from '../scaffold/options.ts'
6
6
 
7
7
  import { basename, resolve } from 'node:path'
8
8
 
@@ -12,12 +12,16 @@ import { toPackageName, unscopedName, validatePackageName } from '../names.ts'
12
12
  import { initGit, installDependencies, targetState, writeProject } from '../scaffold/create.ts'
13
13
  import {
14
14
  ADAPTER_LABELS,
15
+ DEFAULT_ADAPTER,
15
16
  DEFAULT_FEATURES,
17
+ DEFAULT_OPENAPI_UI,
16
18
  detectPackageManager,
17
19
  FEATURE_LABELS,
18
20
  FEATURES,
19
21
  HTTP_ADAPTERS,
20
22
  installCommand,
23
+ OPENAPI_UI_LABELS,
24
+ OPENAPI_UIS,
21
25
  PACKAGE_MANAGERS,
22
26
  parseFeatures,
23
27
  runScript,
@@ -34,7 +38,8 @@ export const newCommand: Command = defineCommand({
34
38
  aliases: ['create', 'init'],
35
39
  examples: [
36
40
  'znight new my-service',
37
- 'znight new my-service --yes --pm pnpm --adapter fastify --features config,observability,messaging',
41
+ 'znight new my-service --yes --pm pnpm --adapter hono --features config,observability,messaging',
42
+ 'znight new my-api --yes --features config,observability,openapi,graphql,websocket --openapi-ui swagger',
38
43
  'znight new my-worker --yes --features config,schedule',
39
44
  'znight new . --yes --no-install',
40
45
  ],
@@ -53,15 +58,21 @@ export const newCommand: Command = defineCommand({
53
58
  },
54
59
  adapter: {
55
60
  type: 'enum',
56
- options: ['node', 'fastify', 'express', 'hono'],
57
- description: 'HTTP adapter (default: node, the built-in node:http server)',
61
+ options: ['fastify', 'node', 'express', 'hono'],
62
+ description: 'HTTP adapter (default: fastify; node is the built-in node:http server)',
58
63
  },
59
64
  features: {
60
65
  type: 'string',
61
66
  valueHint: 'list',
62
67
  description:
63
- 'Comma-separated features: config, observability, messaging, schedule, openapi, or "all" / "none" (default: config,observability)',
68
+ 'Comma-separated features: config, observability, messaging, schedule, openapi, graphql, websocket, or "all" / "none" (default: config,observability)',
64
69
  },
70
+ 'openapi-ui': {
71
+ type: 'enum',
72
+ options: ['scalar', 'swagger'],
73
+ description: 'API reference UI of the openapi feature, served at /docs (default: scalar)',
74
+ },
75
+ docker: { type: 'boolean', default: true, description: 'Do not generate a Dockerfile' },
65
76
  install: { type: 'boolean', default: true, description: 'Do not install the dependencies' },
66
77
  git: { type: 'boolean', default: true, description: 'Do not initialise a git repository' },
67
78
  dir: { type: 'string', valueHint: 'path', description: 'Target directory (default: ./<name>)' },
@@ -112,10 +123,24 @@ export const newCommand: Command = defineCommand({
112
123
  (await prompts?.select({
113
124
  message: 'HTTP adapter?',
114
125
  choices: HTTP_ADAPTERS.map(value => ({ value, label: ADAPTER_LABELS[value] })),
115
- initialValue: 'node',
126
+ initialValue: DEFAULT_ADAPTER,
116
127
  })) ??
117
- 'node'
128
+ DEFAULT_ADAPTER
118
129
  const features = await chooseFeatures(options.features, prompts)
130
+ const openapiUi: OpenApiUiName =
131
+ options['openapi-ui'] ??
132
+ (features.includes('openapi')
133
+ ? await prompts?.select({
134
+ message: 'OpenAPI reference UI (served at /docs)?',
135
+ choices: OPENAPI_UIS.map(value => ({ value, label: OPENAPI_UI_LABELS[value] })),
136
+ initialValue: DEFAULT_OPENAPI_UI,
137
+ })
138
+ : undefined) ??
139
+ DEFAULT_OPENAPI_UI
140
+ const docker =
141
+ prompts === undefined || hasFlag(rawArgs, 'docker')
142
+ ? options.docker !== false
143
+ : await prompts.confirm({ message: 'Add a Dockerfile (production image)?', initialValue: true })
119
144
  const install =
120
145
  prompts === undefined || hasFlag(rawArgs, 'install')
121
146
  ? options.install !== false
@@ -126,7 +151,15 @@ export const newCommand: Command = defineCommand({
126
151
  : await prompts.confirm({ message: 'Initialise a git repository?', initialValue: true })
127
152
 
128
153
  // files
129
- const files = renderProject({ name, packageManager, adapter, features, znightVersion: cli.version })
154
+ const files = renderProject({
155
+ name,
156
+ packageManager,
157
+ adapter,
158
+ features,
159
+ openapiUi,
160
+ docker,
161
+ znightVersion: cli.version,
162
+ })
130
163
  writeProject(directory, files)
131
164
  const where = displayPath(directory, cli.cwd)
132
165
  const summary = `${cli.colors.bold(name)} (${packageManager}, ${adapter}${features.length > 0 ? `, ${features.join(', ')}` : ''})`
@@ -2,7 +2,8 @@
2
2
  * `znight start`: runs the production build with Node.js (or Bun). On POSIX the CLI process is
3
3
  * replaced by the runtime (`execve`): same PID, signals and exit code are native (ideal as a
4
4
  * container entry point). Elsewhere the application runs as a child process; signals are forwarded
5
- * and its exit code is returned.
5
+ * (except, on Windows, Ctrl+C and the console closing, which reach it directly) and its exit code is
6
+ * returned.
6
7
  */
7
8
  import type { Command } from '../command.ts'
8
9
 
@@ -63,8 +64,12 @@ export const startCommand: Command = defineCommand({
63
64
  return 0
64
65
  }
65
66
  const child = processes.spawn(executable, argv, { cwd: root, env, detached: true })
67
+ // on Windows the application shares the console: Ctrl+C and the console closing reach it
68
+ // directly, and child.kill() would be TerminateProcess, cutting its graceful shutdown short
69
+ const consoleEvent = (signal: NodeJS.Signals): boolean =>
70
+ cli.runtime.platform === 'win32' && (signal === 'SIGINT' || signal === 'SIGHUP')
66
71
  const forward = (signal: NodeJS.Signals) => (): void => {
67
- child.kill(signal)
72
+ if (!consoleEvent(signal)) child.kill(signal)
68
73
  }
69
74
  const listeners = (['SIGINT', 'SIGTERM', 'SIGHUP'] as const).map(signal => [signal, forward(signal)] as const)
70
75
  for (const [signal, listener] of listeners) cli.signals.on(signal, listener)
@@ -3,7 +3,8 @@
3
3
  * after each successful build. Stopping sends SIGTERM, waits for the exit and sends SIGKILL after a
4
4
  * timeout. Restarts are serialised and coalesced: a burst of builds causes one restart with the
5
5
  * latest output. A failed build simply does not schedule a restart (and cancels a pending one when
6
- * its output is broken), so the last good process keeps running.
6
+ * its output is broken, even one already stopping the previous process), so nothing starts until a
7
+ * build compiles.
7
8
  */
8
9
  import type { ChildHandle, ExitStatus } from '../integrations/process.ts'
9
10
 
@@ -41,6 +42,12 @@ export class AppSupervisor {
41
42
  #debounce: ReturnType<typeof setTimeout> | undefined
42
43
  #queue: Promise<void> = Promise.resolve()
43
44
  #queued = false
45
+ /** A restart is stopping the previous process before starting the next one. */
46
+ #restarting = false
47
+ /** Bumped by {@link cancelRestart}: a restart requested in an older epoch does not start. */
48
+ #epoch = 0
49
+ /** Epoch of the latest restart request. */
50
+ #requested = 0
44
51
  #closed = false
45
52
  #started = 0
46
53
 
@@ -77,14 +84,18 @@ export class AppSupervisor {
77
84
  }
78
85
 
79
86
  /**
80
- * Cancels a restart scheduled by {@link scheduleRestart} and not started yet (the output it would
81
- * run turned out to be broken, e.g. a failed compilation). Returns whether one was pending.
87
+ * Cancels the restarts that did not start their process yet (the output they would run turned out
88
+ * to be broken, e.g. a failed compilation): scheduled, queued, or stopping the previous process
89
+ * (which still stops). Returns whether one was pending.
82
90
  */
83
91
  cancelRestart(): boolean {
84
- if (this.#debounce === undefined) return false
85
- clearTimeout(this.#debounce)
86
- this.#debounce = undefined
87
- return true
92
+ const pending = this.#debounce !== undefined || this.#queued || this.#restarting
93
+ if (this.#debounce !== undefined) {
94
+ clearTimeout(this.#debounce)
95
+ this.#debounce = undefined
96
+ }
97
+ this.#epoch++
98
+ return pending
88
99
  }
89
100
 
90
101
  /**
@@ -93,14 +104,23 @@ export class AppSupervisor {
93
104
  */
94
105
  restart(): Promise<void> {
95
106
  if (this.#closed) return this.#queue
107
+ this.#requested = this.#epoch
96
108
  if (this.#queued) return this.#queue
97
109
  this.#queued = true
98
110
  this.#queue = this.#queue
99
111
  .then(async () => {
100
112
  this.#queued = false
101
- if (this.#closed) return
102
- await this.#stopCurrent('SIGTERM')
103
- if (this.#closed) return
113
+ // cancelled while it waited for the previous restart
114
+ if (this.#closed || this.#requested !== this.#epoch) return
115
+ const epoch = this.#epoch
116
+ this.#restarting = true
117
+ try {
118
+ await this.#stopCurrent('SIGTERM')
119
+ } finally {
120
+ this.#restarting = false
121
+ }
122
+ // cancelled while the previous process was stopping
123
+ if (this.#closed || epoch !== this.#epoch) return
104
124
  this.#startNew()
105
125
  })
106
126
  .catch((error: unknown) => {
@@ -111,8 +131,12 @@ export class AppSupervisor {
111
131
  return this.#queue
112
132
  }
113
133
 
114
- /** Stops the current process with `signal` (SIGKILL after the timeout) and refuses new starts. */
115
- async stop(signal: NodeJS.Signals = 'SIGTERM'): Promise<void> {
134
+ /**
135
+ * Stops the current process with `signal` (SIGKILL after the timeout) and refuses new starts.
136
+ * `null`: the process already received the signal (a console Ctrl+C on Windows, where `kill()` is
137
+ * a forced termination): it is only waited for, then SIGKILLed after the timeout.
138
+ */
139
+ async stop(signal: NodeJS.Signals | null = 'SIGTERM'): Promise<void> {
116
140
  this.#closed = true
117
141
  if (this.#debounce !== undefined) {
118
142
  clearTimeout(this.#debounce)
@@ -149,13 +173,13 @@ export class AppSupervisor {
149
173
  this.#events.onStart?.(child, restart)
150
174
  }
151
175
 
152
- async #stopCurrent(signal: NodeJS.Signals): Promise<void> {
176
+ async #stopCurrent(signal: NodeJS.Signals | null): Promise<void> {
153
177
  const child = this.#current
154
178
  if (child === undefined) return
155
179
  this.#current = undefined
156
180
  this.#stopping.add(child)
157
181
  try {
158
- child.kill(signal)
182
+ if (signal !== null) child.kill(signal)
159
183
  let timer: ReturnType<typeof setTimeout> | undefined
160
184
  const timeout = new Promise<'timeout'>(resolve => {
161
185
  timer = setTimeout(() => resolve('timeout'), this.#killTimeoutMs)
@@ -7,24 +7,44 @@ export type PackageManager = 'bun' | 'npm' | 'pnpm' | 'yarn'
7
7
  export type HttpAdapterName = 'node' | 'fastify' | 'express' | 'hono'
8
8
 
9
9
  /** Optional features of a new project. */
10
- export type Feature = 'config' | 'observability' | 'messaging' | 'schedule' | 'openapi'
10
+ export type Feature = 'config' | 'observability' | 'messaging' | 'schedule' | 'openapi' | 'graphql' | 'websocket'
11
+
12
+ /** API reference UIs of `@znight/openapi` (its `ui` option). */
13
+ export type OpenApiUiName = 'scalar' | 'swagger'
11
14
 
12
15
  /** Every package manager, in display order. */
13
16
  export const PACKAGE_MANAGERS: readonly PackageManager[] = ['bun', 'npm', 'pnpm', 'yarn']
14
17
 
15
- /** Every HTTP adapter, in display order. */
16
- export const HTTP_ADAPTERS: readonly HttpAdapterName[] = ['node', 'fastify', 'express', 'hono']
18
+ /** Every HTTP adapter, in display order (the default first). */
19
+ export const HTTP_ADAPTERS: readonly HttpAdapterName[] = ['fastify', 'node', 'express', 'hono']
20
+
21
+ /** HTTP adapter used when none is given. */
22
+ export const DEFAULT_ADAPTER: HttpAdapterName = 'fastify'
17
23
 
18
24
  /** Every feature, in display (and canonical) order. */
19
- export const FEATURES: readonly Feature[] = ['config', 'observability', 'messaging', 'schedule', 'openapi']
25
+ export const FEATURES: readonly Feature[] = [
26
+ 'config',
27
+ 'observability',
28
+ 'messaging',
29
+ 'schedule',
30
+ 'openapi',
31
+ 'graphql',
32
+ 'websocket',
33
+ ]
34
+
35
+ /** Every OpenAPI reference UI, in display order (the default first). */
36
+ export const OPENAPI_UIS: readonly OpenApiUiName[] = ['scalar', 'swagger']
37
+
38
+ /** OpenAPI reference UI used when none is given. */
39
+ export const DEFAULT_OPENAPI_UI: OpenApiUiName = 'scalar'
20
40
 
21
41
  /** Features selected when none are given. */
22
42
  export const DEFAULT_FEATURES: readonly Feature[] = ['config', 'observability']
23
43
 
24
44
  /** Labels shown by the prompts and the help. */
25
45
  export const ADAPTER_LABELS: Readonly<Record<HttpAdapterName, string>> = {
26
- node: 'node:http (built-in, zero dependency)',
27
46
  fastify: 'Fastify',
47
+ node: 'node:http (built-in, zero dependency)',
28
48
  express: 'Express',
29
49
  hono: 'Hono',
30
50
  }
@@ -36,6 +56,14 @@ export const FEATURE_LABELS: Readonly<Record<Feature, string>> = {
36
56
  messaging: 'Messaging: message consumers and publishing (@znight/messaging)',
37
57
  schedule: 'Schedule: cron, interval and timeout jobs (@znight/schedule)',
38
58
  openapi: 'OpenAPI: openapi.json and an API reference at /docs (@znight/openapi)',
59
+ graphql: 'GraphQL: code-first schema derived from the TypeScript types (@znight/graphql)',
60
+ websocket: 'WebSocket: gateways served on the HTTP port (@znight/websocket)',
61
+ }
62
+
63
+ /** Labels shown by the prompts. */
64
+ export const OPENAPI_UI_LABELS: Readonly<Record<OpenApiUiName, string>> = {
65
+ scalar: 'Scalar',
66
+ swagger: 'Swagger UI',
39
67
  }
40
68
 
41
69
  /** Everything that determines the files of a new project. */
@@ -46,6 +74,10 @@ export interface ProjectOptions {
46
74
  readonly adapter: HttpAdapterName
47
75
  /** Selected features, in canonical order. */
48
76
  readonly features: readonly Feature[]
77
+ /** API reference UI served at `/docs` with the `openapi` feature. Default: `'scalar'`. */
78
+ readonly openapiUi?: OpenApiUiName
79
+ /** Generate a multi-stage `Dockerfile` and a `.dockerignore`. Default: `true`. */
80
+ readonly docker?: boolean
49
81
  /** Version range base of the `@znight/*` packages (the CLI version). */
50
82
  readonly znightVersion: string
51
83
  }