@hydraharness/harness-cmdline 0.0.0-stage → 0.1.1-rc.6

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeepSeek
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,71 @@
1
- # Temporary Holding Version
1
+ # `@hydraharness/harness-cmdline`
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
+ The command line a hydra launcher hands to the app it boots. The launcher parses only its own flags (`--profile`, `--patch`, the config dumps) and hands **everything after them** to the tree verbatim, so an app owns its flag family, its `--help` text, and its parse errors instead of the launcher knowing them.
4
+
5
+ ## The launcher values
6
+
7
+ A launcher calls `provideCmdline(ctx, host)` before any tree entry mounts, which provides:
8
+
9
+ - `ctx.cmdlineArgs` — the invocation's inner arguments. `get()` is the whole interface, and it returns a snapshot: `hydra --profile tui --resume abc` yields `['--resume', 'abc']`.
10
+ - `ctx.appExit` — a bounded process-exit request, wired to the launcher's shutdown controller.
11
+
12
+ An embedding host with no command line provides an empty list; that is the honest answer, not a missing value.
13
+
14
+ ## Ordinary providers and injected config
15
+
16
+ Any app plugin may inject `cmdlineArgs`, parse it, and publish an ordinary app-owned service. `parseCmdline(ctx, program)` is only a commander adapter; the program's own action owns validation and the published service:
17
+
18
+ ```ts ignore
19
+ export const name = 'web-startup'
20
+ export const inject = ['cmdlineArgs']
21
+
22
+ export function apply(ctx: Context): void {
23
+ const program = webCommand()
24
+ program.action(() => ctx.provide('webStartup', webValuesFrom(program)))
25
+ parseCmdline(ctx, program)
26
+ }
27
+ ```
28
+
29
+ Its Loader row carries no launcher marker or special kind:
30
+
31
+ ```yaml
32
+ - id: web-startup
33
+ name: '@hydraharness/harness-web-app/startup'
34
+ ```
35
+
36
+ Every row configured from those values uses ordinary service injection and direct lazy config access:
37
+
38
+ ```yaml
39
+ - id: webserver
40
+ name: '@hydraharness/harness-host-webserver'
41
+ inject: [webStartup]
42
+ config:
43
+ host: !!js ctx.webStartup.host ?? '127.0.0.1'
44
+ port: !!js ctx.webStartup.port ?? 3080
45
+ ```
46
+
47
+ `parseCmdline` refuses at load a program in which no command declares an action, routes every command's exit and output through the launcher (commander copies those settings into subcommands only at registration), and parses the immutable arguments; commander runs the invoked command's synchronous action on success. An action rejects an invalid invocation with `program.error(...)` — before publishing, since statements ahead of the rejection have already run. On `--help`, `--version`, a parse error, or that rejection, the helper writes commander's text and requests exit; the provider publishes nothing, so dependent rows never activate.
48
+
49
+ ### How injection orders config
50
+
51
+ Loader defers a row's `!!js` interpolation until that row's declared injections are active, then evaluates against the row's plugin context. The example above can therefore read `ctx.webStartup` directly: Cordis has already populated that injected service before Loader asks for `webserver`'s config. Include trees preserve nested expression nodes until each target row reaches this point. Provider replacement and live patch reload repeat interpolation against the current injected services, so a launch flag cannot be silently reset.
52
+
53
+ ### Shared immutable arguments
54
+
55
+ `get()` does not consume or mutate argv. Multiple plugins can parse the same snapshot and independently provide services. The launcher does not inspect the composition for a command-line owner; a profile with no reader simply ignores its app arguments.
56
+
57
+ An out-of-tree plugin brings its own commander copy, so commander's control-flow errors are detected structurally rather than by class identity; an identity check would rethrow a printed help as a fatal load failure.
58
+
59
+ ## Model Experience
60
+
61
+ None, as this package resolves the process's own command line before any session exists.
62
+
63
+ #### KV Cache effect
64
+
65
+ None; this package neither assembles nor sends a provider request.
66
+
67
+ ## Known Limitations and Deferred Work
68
+
69
+ - **Launcher flags must precede app arguments.** The split is positional: the first token the launcher does not recognize starts the inner arguments, so `--patch` placed after an app flag belongs to the app. The launcher's parser consumes one `--`, so an app argument that must survive as a literal `--` needs `-- --`.
70
+ - **An app-owned service has no statically declared provider.** Consumer rows name it through ordinary injection; a bundle that omits its provider fails at settlement with pending entries naming the service rather than at load.
71
+ - **A user patch that replaces a row's whole `config` drops its expressions.** A flag beats the value written beside it, not a literal a user wrote in place of the expression; keeping the expression is what keeps the flag winning.
package/lib/index.js ADDED
@@ -0,0 +1,115 @@
1
+ //#region lib/types/index.js
2
+ /**
3
+ * @hydraharness/harness-cmdline — the command line a hydra launcher hands to the app
4
+ * it boots.
5
+ *
6
+ * The launcher parses only its own flags (`--profile`, `--patch`, the config
7
+ * dumps) and hands everything after them to the tree verbatim through the
8
+ * {@link CmdlineArgs} service, so an app owns its flag family, its `--help`
9
+ * text, and its parse errors instead of the launcher knowing them.
10
+ *
11
+ * Any app plugin can inject `cmdlineArgs` and call {@link parseCmdline}. A
12
+ * provider may publish the parsed values as its own service from its program's
13
+ * commander action, and ordinary rows
14
+ * can inject that service and read it from lazily resolved config —
15
+ * `port: !!js ctx.webStartup.port ?? 3080` — so a flag beats the value written
16
+ * beside it. No row has launcher-level command-line status.
17
+ * @module @hydraharness/harness-cmdline
18
+ */
19
+ /**
20
+ * Provide the command line and the exit request on a host context before any
21
+ * tree entry mounts. Both are launcher facts, not config: an embedding host
22
+ * with no command line provides an empty argument list.
23
+ * @param ctx - the host context the tree will mount under.
24
+ * @param host - the invocation's arguments and its exit request.
25
+ */
26
+ function provideCmdline(ctx, host) {
27
+ const snapshot = Object.freeze([...host.args]);
28
+ ctx.provide("cmdlineArgs", { get: () => snapshot });
29
+ ctx.provide("appExit", host.exit);
30
+ }
31
+ /** The process streams commander output is written to; production writes to the process. */
32
+ const internals = {
33
+ stdout: process.stdout,
34
+ stderr: process.stderr
35
+ };
36
+ /**
37
+ * Parse the launcher's immutable argument snapshot with an app's commander
38
+ * program. Commander runs the program's own synchronous action handler on a
39
+ * successful parse; app code there publishes its service and rejects an
40
+ * invalid invocation with `program.error(...)`. This helper has no Loader-row
41
+ * or service ownership semantics.
42
+ *
43
+ * Help, version, and rejected arguments — from the grammar or from an action
44
+ * — are terminal for the process: commander writes the text and the helper
45
+ * requests `ctx.appExit`. The action never runs on help, version, or a
46
+ * grammar rejection; an action must reject before it publishes, because
47
+ * statements before its `program.error(...)` have already run.
48
+ * @param ctx - plugin context carrying `cmdlineArgs` and `appExit`.
49
+ * @param program - the app's commander program, with its flags, description,
50
+ * actions, and any subcommands already declared.
51
+ * @throws when the launcher did not provide the command line and exit request,
52
+ * or when no command in the program declares an action.
53
+ */
54
+ function parseCmdline(ctx, program) {
55
+ const args = ctx.get("cmdlineArgs");
56
+ const exit = ctx.get("appExit");
57
+ if (args === void 0 || exit === void 0) throw new Error(`${program.name()}: the launcher must provide ctx.cmdlineArgs and ctx.appExit before the tree mounts`);
58
+ if (!hasAction(program)) throw new Error(`${program.name()}: no command in the program declares an action; parseCmdline runs the invoked command's action on a successful parse, and app code there publishes its service`);
59
+ configureExitAndOutput(program);
60
+ try {
61
+ program.parse(args.get(), { from: "user" });
62
+ } catch (error) {
63
+ if (!isCommanderError(error)) throw error;
64
+ exit(error.exitCode);
65
+ }
66
+ }
67
+ /**
68
+ * Whether any command in the tree declares an action handler.
69
+ *
70
+ * The `Command` type cannot express the action precondition, so the handler is
71
+ * read structurally (as {@link isCommanderError} reads commander's control-flow
72
+ * errors): without this guard, a program that forgot its action would parse
73
+ * successfully, publish nothing, and surface only as dependent rows pending on
74
+ * the absent service.
75
+ * @param command - the command whose tree is inspected.
76
+ * @returns true when the command or any registered subcommand has an action.
77
+ */
78
+ function hasAction(command) {
79
+ if (typeof command._actionHandler === "function") return true;
80
+ return command.commands.some(hasAction);
81
+ }
82
+ /**
83
+ * Route every command's exit and output through the launcher adapter.
84
+ *
85
+ * Commander copies `exitOverride` and output configuration into a subcommand
86
+ * only at registration, so a root-only override would let an
87
+ * already-registered subcommand's rejection write to the process streams and
88
+ * call `process.exit` directly, bypassing `ctx.appExit`.
89
+ * @param command - the root of the command tree to configure.
90
+ */
91
+ function configureExitAndOutput(command) {
92
+ command.exitOverride().configureOutput({
93
+ writeOut: (text) => void internals.stdout.write(text),
94
+ writeErr: (text) => void internals.stderr.write(text)
95
+ });
96
+ for (const child of command.commands) configureExitAndOutput(child);
97
+ }
98
+ /**
99
+ * Whether a thrown value is commander's own control-flow error (help, version,
100
+ * a parse error, or `program.error`).
101
+ *
102
+ * Detected structurally, not with `instanceof`: an out-of-tree plugin brings
103
+ * its own commander copy, whose `CommanderError` class is a different identity
104
+ * from this package's, and an identity check there would rethrow a printed
105
+ * help as a fatal load failure.
106
+ * @param error - the thrown value.
107
+ * @returns true when the value carries commander's error code and exit code.
108
+ */
109
+ function isCommanderError(error) {
110
+ if (typeof error !== "object" || error === null) return false;
111
+ const candidate = error;
112
+ return typeof candidate.code === "string" && candidate.code.startsWith("commander.") && typeof candidate.exitCode === "number";
113
+ }
114
+ //#endregion
115
+ export { internals, parseCmdline, provideCmdline };
@@ -0,0 +1,25 @@
1
+ //#region lib/types/invariant.js
2
+ /**
3
+ * Package-owned invariant companion for `@hydraharness/harness-cmdline`.
4
+ * @module @hydraharness/harness-cmdline/invariant
5
+ */
6
+ const PACKAGE_NAME = "@hydraharness/harness-cmdline";
7
+ /** Cordis companion plugin name. */
8
+ const name = "cmdline-invariant";
9
+ /** Service required before the companion can register. */
10
+ const inject = ["invariants"];
11
+ /**
12
+ * No runtime invariant: `cmdlineArgs` is an immutable launcher fact that any
13
+ * number of ordinary plugins may read. App-owned providers and consumers use
14
+ * normal Cordis service injection, whose missing dependencies are already
15
+ * reported by Loader settlement.
16
+ */
17
+ const install = () => {};
18
+ /**
19
+ * Register this package's invariant companion.
20
+ * @param ctx - Cordis context carrying the invariant service.
21
+ * @returns the installed registration's disposer after setup succeeds.
22
+ */
23
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
24
+ //#endregion
25
+ export { apply, inject, name };
@@ -0,0 +1,91 @@
1
+ /**
2
+ * @hydraharness/harness-cmdline — the command line a hydra launcher hands to the app
3
+ * it boots.
4
+ *
5
+ * The launcher parses only its own flags (`--profile`, `--patch`, the config
6
+ * dumps) and hands everything after them to the tree verbatim through the
7
+ * {@link CmdlineArgs} service, so an app owns its flag family, its `--help`
8
+ * text, and its parse errors instead of the launcher knowing them.
9
+ *
10
+ * Any app plugin can inject `cmdlineArgs` and call {@link parseCmdline}. A
11
+ * provider may publish the parsed values as its own service from its program's
12
+ * commander action, and ordinary rows
13
+ * can inject that service and read it from lazily resolved config —
14
+ * `port: !!js ctx.webStartup.port ?? 3080` — so a flag beats the value written
15
+ * beside it. No row has launcher-level command-line status.
16
+ * @module @hydraharness/harness-cmdline
17
+ */
18
+ import type { Command } from 'commander';
19
+ import type { Context } from '@hydraharness/cordis';
20
+ /**
21
+ * The invocation's inner arguments: everything after the launcher's own flags,
22
+ * verbatim and in argv order. `hydra --profile tui --resume abc` yields
23
+ * `['--resume', 'abc']`.
24
+ */
25
+ export interface CmdlineArgs {
26
+ /**
27
+ * Read the inner arguments.
28
+ * @returns the arguments in argv order; empty when the invocation carried none.
29
+ */
30
+ get(): readonly string[];
31
+ }
32
+ /** Request bounded process exit; the launcher wires it to its shutdown controller. */
33
+ export interface AppExit {
34
+ /**
35
+ * Request exit once the tree has been disposed.
36
+ * @param code - the process exit code.
37
+ */
38
+ (code: number): void;
39
+ }
40
+ declare module '@hydraharness/cordis' {
41
+ interface Context {
42
+ /** The invocation's inner arguments; provided by a launcher before the tree mounts. */
43
+ cmdlineArgs?: CmdlineArgs;
44
+ /** Bounded process-exit request; provided by a launcher before the tree mounts. */
45
+ appExit?: AppExit;
46
+ }
47
+ }
48
+ /** The launcher facts an app needs. */
49
+ export interface CmdlineHost {
50
+ /** The invocation's inner arguments, in argv order. */
51
+ args: readonly string[];
52
+ /** Bounded process-exit request. */
53
+ exit: AppExit;
54
+ }
55
+ /**
56
+ * Provide the command line and the exit request on a host context before any
57
+ * tree entry mounts. Both are launcher facts, not config: an embedding host
58
+ * with no command line provides an empty argument list.
59
+ * @param ctx - the host context the tree will mount under.
60
+ * @param host - the invocation's arguments and its exit request.
61
+ */
62
+ export declare function provideCmdline(ctx: Context, host: CmdlineHost): void;
63
+ /** The process streams commander output is written to; production writes to the process. */
64
+ export declare const internals: {
65
+ stdout: {
66
+ write(chunk: string): unknown;
67
+ };
68
+ stderr: {
69
+ write(chunk: string): unknown;
70
+ };
71
+ };
72
+ /**
73
+ * Parse the launcher's immutable argument snapshot with an app's commander
74
+ * program. Commander runs the program's own synchronous action handler on a
75
+ * successful parse; app code there publishes its service and rejects an
76
+ * invalid invocation with `program.error(...)`. This helper has no Loader-row
77
+ * or service ownership semantics.
78
+ *
79
+ * Help, version, and rejected arguments — from the grammar or from an action
80
+ * — are terminal for the process: commander writes the text and the helper
81
+ * requests `ctx.appExit`. The action never runs on help, version, or a
82
+ * grammar rejection; an action must reject before it publishes, because
83
+ * statements before its `program.error(...)` have already run.
84
+ * @param ctx - plugin context carrying `cmdlineArgs` and `appExit`.
85
+ * @param program - the app's commander program, with its flags, description,
86
+ * actions, and any subcommands already declared.
87
+ * @throws when the launcher did not provide the command line and exit request,
88
+ * or when no command in the program declares an action.
89
+ */
90
+ export declare function parseCmdline(ctx: Context, program: Command): void;
91
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Package-owned invariant companion for `@hydraharness/harness-cmdline`.
3
+ * @module @hydraharness/harness-cmdline/invariant
4
+ */
5
+ import type { Context } from '@hydraharness/cordis';
6
+ /** Cordis companion plugin name. */
7
+ export declare const name = "cmdline-invariant";
8
+ /** Service required before the companion can register. */
9
+ export declare const inject: string[];
10
+ /**
11
+ * Register this package's invariant companion.
12
+ * @param ctx - Cordis context carrying the invariant service.
13
+ * @returns the installed registration's disposer after setup succeeds.
14
+ */
15
+ export declare const apply: (ctx: Context) => Promise<() => void>;
16
+ //# sourceMappingURL=invariant.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,46 @@
1
1
  {
2
2
  "name": "@hydraharness/harness-cmdline",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "description": "Immutable command-line handoff from a hydra launcher to any app plugin that injects cmdlineArgs",
4
+ "version": "0.1.1-rc.6",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/MaiHongPhong1902/Hydra-Harness.git",
11
+ "directory": "packages/boot/cmdline"
12
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./invariant": {
22
+ "types": "./lib/types/invariant.d.ts",
23
+ "default": "./lib/invariant.js"
24
+ },
25
+ "./src/*": "./src/*",
26
+ "./package.json": "./package.json"
27
+ },
28
+ "files": [
29
+ "lib/index.js",
30
+ "lib/invariant.js",
31
+ "lib/types/**/*.d.ts"
32
+ ],
33
+ "license": "MIT",
34
+ "peerDependencies": {
35
+ "@hydraharness/harness-invariants": "^0.1.1-rc.6",
36
+ "@hydraharness/cordis-plugin-loader": "^1.0.3",
37
+ "@hydraharness/cordis": "^4.0.2"
38
+ },
39
+ "devDependencies": {
40
+ "commander": "^15.0.0",
41
+ "@hydraharness/cordis-plugin-include": "^1.0.7",
42
+ "@hydraharness/cordis-plugin-loader": "^1.0.3",
43
+ "@hydraharness/harness-invariants": "^0.1.1-rc.6",
44
+ "@hydraharness/cordis": "^4.0.2"
45
+ }
6
46
  }