@databehave/cli 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ Upcoming package version: **0.2.0**. This section describes planned release contents, not a published release.
6
+
7
+ ### Runtime requirements
8
+
9
+ - Require Node.js 24 or 26; support and test majors 24 and 26. Node.js 18, 20 and 22 are unsupported.
10
+
11
+ ### Added
12
+
13
+ - `databehave` executable for explicit ESM configuration factories.
14
+ - Public `defineConfig` and `run` APIs, signal-driven shutdown, idle cleanup and failure exit codes.
15
+ - CLI ownership extracted from the previous server package; no implicit plugin discovery or installation.
16
+
17
+ See [migration](../../MIGRATION.md) and [release procedures](../../docs/releasing.md).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ochairo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,28 @@
1
- # Temporary Holding Version
1
+ # @databehave/cli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Requires Node.js 24 or 26; supported majors are 24 and 26. See the
4
+ [workspace quickstart](../../README.md#start-a-mock-server) and [migration guide](../../MIGRATION.md).
5
+
6
+ Run an explicit ESM plugin configuration with `databehave ./databehave.mjs`.
7
+ Export a factory so repeated runs create fresh plugin instances:
8
+
9
+ ```js
10
+ import { defineConfig } from '@databehave/cli'
11
+ import { httpPlugin, nodeHttpPlugin, routesPlugin } from '@databehave/server'
12
+
13
+ export default defineConfig(() => ({
14
+ plugins: [
15
+ httpPlugin(),
16
+ routesPlugin({ 'GET /hello': () => ({ json: { hello: 'world' } }) }),
17
+ nodeHttpPlugin({ host: '127.0.0.1', port: 8000 }),
18
+ ],
19
+ }))
20
+ ```
21
+
22
+ Configuration and plugins execute trusted application code. Install packages explicitly through your package
23
+ manager. The CLI does not discover, download or install plugins. Use ordinary JavaScript imports and validate
24
+ plugin options at their owning boundary. JSONC configurations must be migrated; see the repository migration guide.
25
+
26
+ `run({ config, signal? })` returns `{ host, close }` after startup. Shutdown cancels/drains work and reports cleanup
27
+ errors. The executable handles SIGINT/SIGTERM and exits with 2 for invalid usage or 1 for startup/cleanup failure.
28
+ When no active resources remain, it runs cleanup and exits successfully instead of keeping an idle process alive.
package/dist/bin.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/bin.js ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+ import { parseArguments, run } from './index.js';
3
+ const usage = 'Usage: databehave <config.mjs>\n databehave --help\n';
4
+ const arguments_ = parseArguments(process.argv.slice(2));
5
+ if (arguments_.kind === 'help')
6
+ process.stdout.write(usage);
7
+ else if (arguments_.kind === 'error') {
8
+ process.stderr.write(`${arguments_.message}\n${usage}`);
9
+ process.exitCode = 2;
10
+ }
11
+ else {
12
+ const lifetime = new AbortController();
13
+ const stop = () => lifetime.abort();
14
+ process.once('SIGINT', stop);
15
+ process.once('SIGTERM', stop);
16
+ // A composition with no remaining active resources should dispose and exit successfully.
17
+ process.once('beforeExit', stop);
18
+ try {
19
+ const handle = await run({ config: arguments_.config, signal: lifetime.signal });
20
+ process.stdout.write('[databehave] ready\n');
21
+ if (!lifetime.signal.aborted) {
22
+ await new Promise(resolve => lifetime.signal.addEventListener('abort', () => resolve(), { once: true }));
23
+ }
24
+ await handle.close();
25
+ }
26
+ catch (error) {
27
+ if (!(lifetime.signal.aborted && error instanceof Error &&
28
+ (error.cause === 'aborted' || error.message === 'Host closed during startup'))) {
29
+ console.error('[databehave] failed:', error);
30
+ process.exitCode = 1;
31
+ }
32
+ }
33
+ finally {
34
+ process.removeListener('SIGINT', stop);
35
+ process.removeListener('SIGTERM', stop);
36
+ process.removeListener('beforeExit', stop);
37
+ }
38
+ }
@@ -0,0 +1,32 @@
1
+ import { type Host, type Plugin } from '@databehave/core';
2
+ /** A fresh composition for one application instance. */
3
+ export interface Configuration {
4
+ readonly plugins: readonly Plugin[];
5
+ }
6
+ /** ESM configuration factories create independent plugin instances on each invocation. */
7
+ export type ConfigurationFactory = () => Configuration | Promise<Configuration>;
8
+ /** Type-check a configuration factory without evaluating it or acquiring resources. */
9
+ export declare const defineConfig: (factory: ConfigurationFactory) => ConfigurationFactory;
10
+ /** Explicit local configuration module and optional application lifetime cancellation. */
11
+ export interface RunOptions {
12
+ readonly config: string;
13
+ readonly signal?: AbortSignal;
14
+ }
15
+ /** Running host and cleanup of the caller's cancellation subscription. */
16
+ export interface RunHandle {
17
+ readonly host: Host;
18
+ close(): Promise<void>;
19
+ }
20
+ /** Import only the explicitly selected local ESM module; plugins are trusted application code. */
21
+ export declare const run: (options: RunOptions) => Promise<RunHandle>;
22
+ /** Validated CLI arguments; feature options belong to the selected plugins. */
23
+ export type CliArguments = {
24
+ readonly kind: 'help';
25
+ } | {
26
+ readonly kind: 'run';
27
+ readonly config: string;
28
+ } | {
29
+ readonly kind: 'error';
30
+ readonly message: string;
31
+ };
32
+ export declare const parseArguments: (arguments_: readonly string[]) => CliArguments;
package/dist/index.js ADDED
@@ -0,0 +1,75 @@
1
+ import { resolve } from 'node:path';
2
+ import { pathToFileURL } from 'node:url';
3
+ import { createHost } from '@databehave/core';
4
+ /** Type-check a configuration factory without evaluating it or acquiring resources. */
5
+ export const defineConfig = (factory) => factory;
6
+ const isConfiguration = (value) => {
7
+ if (value === null || typeof value !== 'object' || !('plugins' in value))
8
+ return false;
9
+ const plugins = value.plugins;
10
+ return Array.isArray(plugins) && plugins.every((plugin) => plugin !== null && typeof plugin === 'object' &&
11
+ 'id' in plugin && typeof plugin.id === 'string' &&
12
+ 'apiVersion' in plugin && plugin.apiVersion === 1 &&
13
+ 'setup' in plugin && typeof plugin.setup === 'function');
14
+ };
15
+ const aborted = () => new Error('Application startup was cancelled', { cause: 'aborted' });
16
+ /** Import only the explicitly selected local ESM module; plugins are trusted application code. */
17
+ export const run = async (options) => {
18
+ if (options.signal?.aborted)
19
+ throw aborted();
20
+ const path = resolve(options.config);
21
+ if (!path.endsWith('.mjs') && !path.endsWith('.js')) {
22
+ throw new Error('Use an ESM .mjs or .js configuration exporting a default configuration factory; see the migration guide');
23
+ }
24
+ const module = await import(pathToFileURL(path).href);
25
+ if (typeof module.default !== 'function') {
26
+ throw new Error('Configuration must export a default factory returning { plugins: [...] }');
27
+ }
28
+ const configuration = await module.default();
29
+ if (!isConfiguration(configuration))
30
+ throw new Error('Configuration factory must return valid plugins');
31
+ if (options.signal?.aborted)
32
+ throw aborted();
33
+ const host = createHost(configuration);
34
+ let closing;
35
+ const close = () => {
36
+ if (!closing) {
37
+ options.signal?.removeEventListener('abort', onAbort);
38
+ closing = host.close();
39
+ }
40
+ return closing;
41
+ };
42
+ const onAbort = () => {
43
+ // Observe rejection immediately; run()/the returned handle still retain and report it.
44
+ void close().catch(() => { });
45
+ };
46
+ options.signal?.addEventListener('abort', onAbort, { once: true });
47
+ try {
48
+ await host.start();
49
+ if (options.signal?.aborted) {
50
+ await close();
51
+ throw aborted();
52
+ }
53
+ return { host, close };
54
+ }
55
+ catch (error) {
56
+ options.signal?.removeEventListener('abort', onAbort);
57
+ try {
58
+ await close();
59
+ }
60
+ catch (cleanupError) {
61
+ if (cleanupError === error)
62
+ throw error;
63
+ throw new AggregateError([error, cleanupError], 'Application startup and cleanup failed', { cause: error });
64
+ }
65
+ throw error;
66
+ }
67
+ };
68
+ export const parseArguments = (arguments_) => {
69
+ if (arguments_.length === 1 && (arguments_[0] === '--help' || arguments_[0] === '-h'))
70
+ return { kind: 'help' };
71
+ if (arguments_.length !== 1 || !arguments_[0] || arguments_[0].startsWith('-')) {
72
+ return { kind: 'error', message: 'Expected one ESM configuration path' };
73
+ }
74
+ return { kind: 'run', config: arguments_[0] };
75
+ };
package/package.json CHANGED
@@ -1,6 +1,49 @@
1
1
  {
2
2
  "name": "@databehave/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0",
4
+ "description": "Explicit plugin composition for Databehave",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/ochairo/databehave.git",
10
+ "directory": "packages/cli"
11
+ },
12
+ "main": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js"
18
+ }
19
+ },
20
+ "bin": {
21
+ "databehave": "./dist/bin.js"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "README.md",
26
+ "LICENSE",
27
+ "CHANGELOG.md"
28
+ ],
29
+ "engines": {
30
+ "node": "^24.0.0 || ^26.0.0"
31
+ },
32
+ "dependencies": {
33
+ "@databehave/core": "^0.1.0"
34
+ },
35
+ "devDependencies": {
36
+ "typescript": "6.0.3",
37
+ "@types/node": "25.9.1"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public",
41
+ "provenance": true
42
+ },
43
+ "scripts": {
44
+ "build": "tsc -p tsconfig.build.json && chmod +x dist/bin.js",
45
+ "typecheck": "tsc --noEmit",
46
+ "test": "tsc -p tsconfig.test.json && node --test dist-test/test/*.test.js",
47
+ "clean": "rm -rf dist dist-test"
48
+ }
6
49
  }