@jini-ai/cli 0.3.1 → 0.4.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 +95 -0
- package/README.md +27 -10
- package/dist/command-registry.d.ts +19 -7
- package/dist/command-registry.js +8 -7
- package/dist/daemon-command.d.ts +22 -7
- package/dist/daemon-command.js +34 -40
- package/dist/daemon-url.d.ts +16 -6
- package/dist/daemon-url.js +9 -24
- package/dist/errors.d.ts +16 -6
- package/dist/errors.js +17 -16
- package/dist/flags.d.ts +11 -5
- package/dist/flags.js +5 -5
- package/dist/http.d.ts +25 -9
- package/dist/http.js +27 -24
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/introspection.d.ts +75 -0
- package/dist/introspection.js +63 -0
- package/dist/local-daemon-discovery.d.ts +7 -15
- package/dist/local-daemon-discovery.js +7 -7
- package/dist/main.d.ts +12 -4
- package/dist/main.js +32 -34
- package/dist/prompt.d.ts +17 -7
- package/dist/prompt.js +13 -13
- package/dist/redact.d.ts +5 -20
- package/dist/redact.js +5 -97
- package/dist/run-command.d.ts +38 -10
- package/dist/run-command.js +68 -84
- package/dist/tokens.js +1 -1
- package/dist/usage.d.ts +4 -2
- package/dist/usage.js +8 -8
- package/dist/version-command.d.ts +18 -6
- package/dist/version-command.js +13 -14
- package/package.json +19 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# @jini-ai/cli
|
|
2
|
+
|
|
3
|
+
## 0.4.0 — 2026-10-02
|
|
4
|
+
|
|
5
|
+
### BREAKING
|
|
6
|
+
|
|
7
|
+
- Shared sanitization now comes from core/text and loopback policy from platform/net. Public helpers use required/optional argument objects.
|
|
8
|
+
- Distribution includes runtime output, release documentation and required assets only. Process records and per-job neutrality checks are no longer part of the package surface.
|
|
9
|
+
|
|
10
|
+
## Unreleased
|
|
11
|
+
|
|
12
|
+
- Fix command and discovery test calls to match required/optional argument objects, and create the command-registry token with core's `{ id }` argument.
|
|
13
|
+
|
|
14
|
+
- **BREAKING:** Remove CLI sanitizeUntrustedText/SanitizeTextOptions/stripControlSequences exports;
|
|
15
|
+
callers import core/text. Remove redactSecretLike in favor of core redactSecrets with explicit
|
|
16
|
+
aggressive policy for former direct opaque-token users. Its category markers and PII masking
|
|
17
|
+
are covered by migration vectors; core's default remains conservative. Terminal transport and
|
|
18
|
+
recursive JSON sanitization keep their frozen lowercase output.
|
|
19
|
+
- Depend on platform/net for daemon URL loopback checks; recognize full IPv4 loopback range and
|
|
20
|
+
dotted localhost. Mapped IPv6 retains remote-warning behavior. Tests are written, not run.
|
|
21
|
+
|
|
22
|
+
- Emit structured JSON HTTP failures through injected ports for unknown codes, response-size and
|
|
23
|
+
body-read failures. Include the truncation marker within diagnostic maxLength and correct the
|
|
24
|
+
binary version rationale. Regression tests are written, not run (owner directive).
|
|
25
|
+
|
|
26
|
+
- **BREAKING:** Public helpers, command handlers/registrations and injected I/O ports now take required and optional argument objects. Output, exit, warning and file-reader ports receive `{ text }`, `{ code }`, `{ message }` and `{ path }`. See `API.md` and the archived migration inventory.
|
|
27
|
+
- Expose introspection from the library barrel and declare its universal runtime without importing the binary entrypoint.
|
|
28
|
+
|
|
29
|
+
- Add an introspection subpath for structural Commander trees and MCP schemas with required host exclusions and tool-name prefix.
|
|
30
|
+
|
|
31
|
+
## 0.1.2
|
|
32
|
+
|
|
33
|
+
### Patch Changes
|
|
34
|
+
|
|
35
|
+
- Add top-level `main`/`types` fields alongside the existing `exports` map. A consumer on
|
|
36
|
+
TypeScript's classic `moduleResolution: "node"` (node10) — which ignores `package.json#exports`
|
|
37
|
+
entirely — could not resolve this package's types at all (`TS2307: Cannot find module`) even
|
|
38
|
+
after the previous exports-map fix restored `require()` at runtime; type resolution and runtime
|
|
39
|
+
resolution are separate algorithms. Verified against a real external consumer (whose
|
|
40
|
+
tsconfig uses this legacy resolution mode): adding these two fields, with its tsconfig completely
|
|
41
|
+
unchanged, made the error disappear. Also fixes absolute-path `require()` (distinct from a bare
|
|
42
|
+
specifier, which already worked) for the same reason — `main` was previously absent.
|
|
43
|
+
|
|
44
|
+
Purely additive: every modern resolver (Node's own runtime `exports` resolution, TypeScript's
|
|
45
|
+
`bundler`/`node16`/`nodenext`) prefers `exports` over `main`/`types` when both are present, so
|
|
46
|
+
this changes nothing for a consumer already on a modern resolver.
|
|
47
|
+
|
|
48
|
+
- Updated dependencies
|
|
49
|
+
- @jini-ai/core@0.1.2
|
|
50
|
+
- @jini-ai/sidecar@0.1.2
|
|
51
|
+
|
|
52
|
+
## 0.1.1
|
|
53
|
+
|
|
54
|
+
### Patch Changes
|
|
55
|
+
|
|
56
|
+
- Add a `"default"` export condition to every published package's `exports` map — every one of
|
|
57
|
+
them lacked it, which meant `require()` failed with `ERR_PACKAGE_PATH_NOT_EXPORTED` for any
|
|
58
|
+
CommonJS consumer (found via a real external integration attempt; Node needs `require(esm)`
|
|
59
|
+
support, i.e. Node >=22.12, for this to resolve).
|
|
60
|
+
|
|
61
|
+
`@jini-ai/agent-runtime`:
|
|
62
|
+
|
|
63
|
+
- **New**: `RuntimeBuildOptions.permissionMode` (`'bypass' | 'restricted'`) lets a caller opt a
|
|
64
|
+
run OUT of the auto-approve-every-permission-prompt flag every def with one
|
|
65
|
+
(`bypassPermissions` / `--yolo` / `--dangerously-skip-permissions`) previously pushed
|
|
66
|
+
unconditionally, with no way to turn it off. Omitting it keeps today's default (bypass)
|
|
67
|
+
behavior unchanged.
|
|
68
|
+
- **New**: `ClaudeStreamEvent`, `CopilotStreamEvent`, and `QoderEvent` are now real exported
|
|
69
|
+
discriminated unions instead of `Record<string, unknown>` — a real external consumer guessed a
|
|
70
|
+
nonexistent field name (`event.text` instead of the actual `event.delta`) against the old
|
|
71
|
+
untyped sink and silently lost every streamed token with no compile or runtime error.
|
|
72
|
+
- Fixed a doc/implementation mismatch in `claude-stream.ts`: the module doc claimed `tool_result`
|
|
73
|
+
events carry `{ tool_use_id, content, is_error }`; the actual emitted shape is
|
|
74
|
+
`{ toolUseId, content, isError }`.
|
|
75
|
+
|
|
76
|
+
`@jini-ai/daemon`: `AgentExecutorRunInput.permissionMode` forwards the new
|
|
77
|
+
`RuntimeBuildOptions.permissionMode` through to `buildArgs`, so a host can actually reach the new
|
|
78
|
+
opt-out from the daemon's real run-input surface, not just from `@jini-ai/agent-runtime` in
|
|
79
|
+
isolation.
|
|
80
|
+
|
|
81
|
+
`@jini-ai/agentic`: `setAtPointer` no longer throws on a malformed (e.g. missing leading `/`)
|
|
82
|
+
`updateDataModel` path — degrades to a no-op like its sibling `getAtPointer`, matching this
|
|
83
|
+
package's own "a bad binding must not crash the renderer" contract. That path is agent-authored
|
|
84
|
+
wire data with no error boundary above it in any host, so the uncaught throw could unmount an
|
|
85
|
+
entire chat UI from ~40 bytes of malformed input.
|
|
86
|
+
|
|
87
|
+
`@jini-ai/chat-react`: a local (client-resolved) A2UI button action is no longer a silent no-op —
|
|
88
|
+
`A2uiSurfaceCard` now surfaces the resolved value. New `ExtEventErrorBoundary` confines a
|
|
89
|
+
`kind: 'ext'` event group's renderer to its own card instead of letting a render/effect-phase
|
|
90
|
+
throw from agent-controlled content unmount the whole chat root (there was no error boundary
|
|
91
|
+
anywhere in this package or its hosts before this).
|
|
92
|
+
|
|
93
|
+
- Updated dependencies
|
|
94
|
+
- @jini-ai/core@0.1.1
|
|
95
|
+
- @jini-ai/sidecar@0.1.1
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ local daemon discovery, and a pluggable command registry. This is deliberately *
|
|
|
5
5
|
slice, not the full `@jini-ai/cli` package** — it was extracted from Open Design's ~9,900-line
|
|
6
6
|
`cli.ts` by keeping only the transport/plumbing primitives that have nothing to do with any
|
|
7
7
|
product noun (project, plugin, brand, conversation, …); everything OD-domain-specific was left
|
|
8
|
-
behind (see
|
|
8
|
+
behind (see the archived provenance ledger's classification table). The library barrel (`index.ts`) intentionally
|
|
9
9
|
registers nothing — it ships the building blocks a host pack uses to register real commands, plus
|
|
10
10
|
a small set of concrete commands (`run`, `daemon`, `version`) that wrap `@jini-ai/http`'s actual
|
|
11
11
|
routes, and a bootable `jini` binary (`src/main.ts`, wired via `package.json`'s `bin` field) that
|
|
@@ -33,27 +33,27 @@ No peer dependencies. Depends on `@jini-ai/core` (for the `token()`-based transp
|
|
|
33
33
|
- **Structured errors** — `exitWithStructuredError`, `DEFAULT_CLI_EXIT_CODES`,
|
|
34
34
|
`structuredErrorData` — maps an error code to a stable exit code and a `{ error: { code,
|
|
35
35
|
message, data } }` stderr envelope.
|
|
36
|
-
- **Command registry** — `CommandRegistry` (`.add(name, handler, { usage?, override? })`,
|
|
37
|
-
`.dispatch(argv, { valueFlags? })`), `CommandDispatchResult`, `CommandHandler` — the generic
|
|
36
|
+
- **Command registry** — `CommandRegistry` (`.add({ name, handler }, { usage?, override? })`,
|
|
37
|
+
`.dispatch({ argv }, { valueFlags? })`), `CommandDispatchResult`, `CommandHandler` — the generic
|
|
38
38
|
"first-non-flag-token dispatch" shape a pack's `Pack['cli']` hook registers against.
|
|
39
39
|
- **Built-in commands** — `registerRunCommands` (`run start|list|get|cancel|watch`),
|
|
40
40
|
`registerDaemonCommands` (`daemon status|stop`), `registerVersionCommand` (`version`) — each a
|
|
41
41
|
thin transport over `@jini-ai/http`'s run/daemon-status routes.
|
|
42
42
|
- **Prompt/body input helpers** — `readPromptFromFlags`/`readBodyFromFlags` (`--prompt`/
|
|
43
|
-
`--prompt-file`/stdin conventions) and `
|
|
43
|
+
`--prompt-file`/stdin conventions) and `@jini-ai/core/text`'s `stripControlSequences`.
|
|
44
44
|
|
|
45
45
|
## Usage
|
|
46
46
|
|
|
47
47
|
```ts
|
|
48
48
|
import { CommandRegistry, resolveDaemonUrl, registerRunCommands, exitWithStructuredError } from '@jini-ai/cli';
|
|
49
49
|
|
|
50
|
-
const registry = new CommandRegistry();
|
|
50
|
+
const registry = new CommandRegistry({});
|
|
51
51
|
|
|
52
|
-
registerRunCommands(registry,
|
|
53
|
-
resolveBaseUrl: () => resolveDaemonUrl({ envVarName: 'JINI_DAEMON_URL', defaultUrl: 'http://127.0.0.1:4317' }),
|
|
52
|
+
registerRunCommands({ registry,
|
|
53
|
+
resolveBaseUrl: () => resolveDaemonUrl({}, { envVarName: 'JINI_DAEMON_URL', defaultUrl: 'http://127.0.0.1:4317' }),
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
-
const result = await registry.dispatch(process.argv.slice(2));
|
|
56
|
+
const result = await registry.dispatch({ argv: process.argv.slice(2) });
|
|
57
57
|
if (result.kind === 'not-found') {
|
|
58
58
|
exitWithStructuredError({ code: 'invalid-flag', message: `unknown command "${result.name}"` });
|
|
59
59
|
}
|
|
@@ -64,9 +64,11 @@ and run its `jini` binary directly (`npx jini run list --daemon-url http://127.0
|
|
|
64
64
|
|
|
65
65
|
## What's swappable
|
|
66
66
|
|
|
67
|
+
Command handlers and registrars receive `resolveBaseUrl` in the required argument object; optional I/O ports belong in the second object. Call a discovery probe with `discover({ env }, { timeoutMs })`.
|
|
68
|
+
|
|
67
69
|
`resolveDaemonUrl`'s `discover` callback is the intended injection point for daemon-discovery
|
|
68
70
|
strategy — `createLocalDaemonDiscovery` is the one concrete implementation shipped, but any
|
|
69
|
-
`(env, timeoutMs) => Promise<string | null>` works. `CommandRegistry` itself is the seam a pack
|
|
71
|
+
`({ env }, { timeoutMs }) => Promise<string | null>` works. `CommandRegistry` itself is the seam a pack
|
|
70
72
|
uses to add or `override` commands. The exit-code table (`DEFAULT_CLI_EXIT_CODES`) is a small
|
|
71
73
|
default a caller extends with its own `code → exitCode` entries rather than a fixed, closed set.
|
|
72
74
|
|
|
@@ -77,4 +79,19 @@ ESM only — ships `"type": "module"` with no CommonJS `require` build.
|
|
|
77
79
|
|
|
78
80
|
## Provenance
|
|
79
81
|
|
|
80
|
-
See
|
|
82
|
+
See the archived provenance ledger for per-file provenance and scope decisions. Apache-2.0.
|
|
83
|
+
|
|
84
|
+
## Introspection and I/O ports
|
|
85
|
+
|
|
86
|
+
The universal `./introspection` entry reads structural Commander trees and builds MCP schemas with caller-owned exclusions and tool-name prefixes. It is also exported from the Node library barrel. The binary stays separate. See [API.md](https://github.com/AINSEP/Jini/blob/main/packages/cli/API.md) for the object argument convention, including injected I/O ports.
|
|
87
|
+
|
|
88
|
+
## Design decisions
|
|
89
|
+
|
|
90
|
+
- [Introspection derives from the live command tree](docs/decisions/DR-001-live-command-introspection.md).
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
Text sanitization and terminal control stripping now live at `@jini-ai/core/text`, with their
|
|
94
|
+
bounded lowercase-marker output preserved. Former direct `redactSecretLike` callers use core
|
|
95
|
+
`redactSecrets({ input }, { policy: 'aggressive' })`; this explicit policy emits category markers
|
|
96
|
+
and also masks recognized PII. Core's default stays conservative for diagnostics. The CLI keeps
|
|
97
|
+
`sanitizeUnknownDeep` for bounded recursive JSON. Loopback warnings use `@jini-ai/platform/net`.
|
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
* @module command-registry
|
|
3
3
|
*
|
|
4
4
|
* The generic dispatcher shape behind OD's `SUBCOMMAND_MAP` (see
|
|
5
|
-
* `
|
|
5
|
+
* `archived provenance ledger`): look at the first non-flag `argv` token, dispatch to a
|
|
6
6
|
* registered handler, or report that nothing matched so the caller can fall
|
|
7
7
|
* back to root help. OD's actual map (33 product command names) was not
|
|
8
8
|
* ported — only the registrar pattern, which is what `@jini-ai/core`'s
|
|
9
9
|
* `Pack['cli']` hook (`cli?: (reg: unknown, services: Services) => void`)
|
|
10
10
|
* needs a concrete `reg` type for.
|
|
11
11
|
*/
|
|
12
|
-
export type CommandHandler = (
|
|
12
|
+
export type CommandHandler = (requiredArgs: {
|
|
13
|
+
args: readonly string[];
|
|
14
|
+
}) => void | Promise<void>;
|
|
13
15
|
export type CommandDispatchResult = {
|
|
14
16
|
kind: 'handled';
|
|
15
17
|
} | {
|
|
@@ -53,17 +55,25 @@ export interface CommandDispatchOptions {
|
|
|
53
55
|
* to skip over a value-flag's value while scanning for the command name.
|
|
54
56
|
*/
|
|
55
57
|
export declare class CommandRegistry {
|
|
58
|
+
constructor(_requiredArgs?: Record<string, never>);
|
|
56
59
|
private readonly commands;
|
|
57
60
|
/**
|
|
58
61
|
* Register a command. Throws if `name` is already registered — pass
|
|
59
62
|
* `{ override: true }` to intentionally replace an existing registration.
|
|
60
63
|
*/
|
|
61
|
-
add(name
|
|
62
|
-
|
|
64
|
+
add({ name, handler }: {
|
|
65
|
+
name: string;
|
|
66
|
+
handler: CommandHandler;
|
|
67
|
+
}, opts?: AddCommandOptions): this;
|
|
68
|
+
has({ name }: {
|
|
69
|
+
name: string;
|
|
70
|
+
}): boolean;
|
|
63
71
|
/** The usage text registered alongside `name`, if any. */
|
|
64
|
-
usageFor(name
|
|
72
|
+
usageFor({ name }: {
|
|
73
|
+
name: string;
|
|
74
|
+
}): string | undefined;
|
|
65
75
|
/** Every registered command name, insertion order. */
|
|
66
|
-
names(): string[];
|
|
76
|
+
names(_requiredArgs?: Record<string, never>): string[];
|
|
67
77
|
/**
|
|
68
78
|
* Find the first non-`-`-prefixed token in `argv`, look up its handler,
|
|
69
79
|
* and invoke it with every other token (flags before *and* after the
|
|
@@ -77,6 +87,8 @@ export declare class CommandRegistry {
|
|
|
77
87
|
* `options.valueFlags` — the set of global `--flag` names that consume the
|
|
78
88
|
* next token as a value — so the scan skips over it instead.
|
|
79
89
|
*/
|
|
80
|
-
dispatch(argv
|
|
90
|
+
dispatch({ argv }: {
|
|
91
|
+
argv: readonly string[];
|
|
92
|
+
}, options?: CommandDispatchOptions): Promise<CommandDispatchResult>;
|
|
81
93
|
}
|
|
82
94
|
//# sourceMappingURL=command-registry.d.ts.map
|
package/dist/command-registry.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @module command-registry
|
|
3
3
|
*
|
|
4
4
|
* The generic dispatcher shape behind OD's `SUBCOMMAND_MAP` (see
|
|
5
|
-
* `
|
|
5
|
+
* `archived provenance ledger`): look at the first non-flag `argv` token, dispatch to a
|
|
6
6
|
* registered handler, or report that nothing matched so the caller can fall
|
|
7
7
|
* back to root help. OD's actual map (33 product command names) was not
|
|
8
8
|
* ported — only the registrar pattern, which is what `@jini-ai/core`'s
|
|
@@ -18,27 +18,28 @@
|
|
|
18
18
|
* to skip over a value-flag's value while scanning for the command name.
|
|
19
19
|
*/
|
|
20
20
|
export class CommandRegistry {
|
|
21
|
+
constructor(_requiredArgs = {}) { }
|
|
21
22
|
commands = new Map();
|
|
22
23
|
/**
|
|
23
24
|
* Register a command. Throws if `name` is already registered — pass
|
|
24
25
|
* `{ override: true }` to intentionally replace an existing registration.
|
|
25
26
|
*/
|
|
26
|
-
add(name, handler, opts = {}) {
|
|
27
|
+
add({ name, handler }, opts = {}) {
|
|
27
28
|
if (this.commands.has(name) && opts.override !== true) {
|
|
28
29
|
throw new Error(`command "${name}" is already registered; pass { override: true } to replace it intentionally`);
|
|
29
30
|
}
|
|
30
31
|
this.commands.set(name, { name, handler, ...(opts.usage !== undefined ? { usage: opts.usage } : {}) });
|
|
31
32
|
return this;
|
|
32
33
|
}
|
|
33
|
-
has(name) {
|
|
34
|
+
has({ name }) {
|
|
34
35
|
return this.commands.has(name);
|
|
35
36
|
}
|
|
36
37
|
/** The usage text registered alongside `name`, if any. */
|
|
37
|
-
usageFor(name) {
|
|
38
|
+
usageFor({ name }) {
|
|
38
39
|
return this.commands.get(name)?.usage;
|
|
39
40
|
}
|
|
40
41
|
/** Every registered command name, insertion order. */
|
|
41
|
-
names() {
|
|
42
|
+
names(_requiredArgs = {}) {
|
|
42
43
|
return [...this.commands.keys()];
|
|
43
44
|
}
|
|
44
45
|
/**
|
|
@@ -54,7 +55,7 @@ export class CommandRegistry {
|
|
|
54
55
|
* `options.valueFlags` — the set of global `--flag` names that consume the
|
|
55
56
|
* next token as a value — so the scan skips over it instead.
|
|
56
57
|
*/
|
|
57
|
-
async dispatch(argv, options = {}) {
|
|
58
|
+
async dispatch({ argv }, options = {}) {
|
|
58
59
|
const valueFlags = options.valueFlags ?? new Set();
|
|
59
60
|
let index = -1;
|
|
60
61
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -81,7 +82,7 @@ export class CommandRegistry {
|
|
|
81
82
|
if (command === undefined)
|
|
82
83
|
return { kind: 'not-found', name };
|
|
83
84
|
const rest = [...argv.slice(0, index), ...argv.slice(index + 1)];
|
|
84
|
-
await command.handler(rest);
|
|
85
|
+
await command.handler({ args: rest });
|
|
85
86
|
return { kind: 'handled' };
|
|
86
87
|
}
|
|
87
88
|
}
|
package/dist/daemon-command.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* `daemon status` / `daemon stop` — concrete commands over `@jini-ai/http-kit`'s
|
|
5
5
|
* `daemonStatusRoute`/`daemonShutdownRoute` (`packages/http/src/daemon-status.ts`).
|
|
6
|
-
* `packages/cli/
|
|
6
|
+
* `packages/cli/archived provenance ledger`'s original UNCLEAR verdict on these two
|
|
7
7
|
* blocked on `@jini-ai/http-kit` being a stub with no `/status`/`/shutdown` route to
|
|
8
8
|
* call; that blocker no longer holds (both routes exist and are tested), and
|
|
9
9
|
* `run-command.ts` is now a real, established "first concrete command"
|
|
@@ -23,20 +23,35 @@ export interface DaemonCommandDeps {
|
|
|
23
23
|
/** Resolves the daemon HTTP base URL once per command invocation (e.g. wraps `resolveDaemonUrl`). */
|
|
24
24
|
resolveBaseUrl: () => Promise<string> | string;
|
|
25
25
|
/** Defaults to `process.stdout.write`; inject for tests. Used for successful command output. */
|
|
26
|
-
write?: (
|
|
26
|
+
write?: (requiredArgs: {
|
|
27
|
+
text: string;
|
|
28
|
+
}) => void;
|
|
27
29
|
/** Defaults to `process.stderr.write`; inject for tests. Used for usage/validation errors. */
|
|
28
|
-
writeErr?: (
|
|
30
|
+
writeErr?: (requiredArgs: {
|
|
31
|
+
text: string;
|
|
32
|
+
}) => void;
|
|
29
33
|
/** Defaults to the global `fetch`; inject for tests. */
|
|
30
34
|
fetchImpl?: typeof fetch;
|
|
31
35
|
/** Defaults to `process.exit`; inject for tests (must not return). */
|
|
32
|
-
exit?: (
|
|
36
|
+
exit?: (requiredArgs: {
|
|
37
|
+
code: number;
|
|
38
|
+
}) => never;
|
|
33
39
|
/** Extra/overriding `code -> exitCode` entries layered on the package default table. */
|
|
34
40
|
exitCodes?: ExitCodeTable;
|
|
35
41
|
}
|
|
36
42
|
/** `daemon status` */
|
|
37
|
-
export declare function daemonStatusCommand(args
|
|
43
|
+
export declare function daemonStatusCommand({ args, resolveBaseUrl }: {
|
|
44
|
+
args: readonly string[];
|
|
45
|
+
resolveBaseUrl: DaemonCommandDeps["resolveBaseUrl"];
|
|
46
|
+
}, optionalArgs?: Omit<DaemonCommandDeps, "resolveBaseUrl">): Promise<void>;
|
|
38
47
|
/** `daemon stop` */
|
|
39
|
-
export declare function daemonStopCommand(args
|
|
48
|
+
export declare function daemonStopCommand({ args, resolveBaseUrl }: {
|
|
49
|
+
args: readonly string[];
|
|
50
|
+
resolveBaseUrl: DaemonCommandDeps["resolveBaseUrl"];
|
|
51
|
+
}, optionalArgs?: Omit<DaemonCommandDeps, "resolveBaseUrl">): Promise<void>;
|
|
40
52
|
/** Registers `daemon` (dispatching `status`/`stop` on the first remaining token) against `registry`. */
|
|
41
|
-
export declare function registerDaemonCommands(registry
|
|
53
|
+
export declare function registerDaemonCommands({ registry, resolveBaseUrl }: {
|
|
54
|
+
registry: CommandRegistry;
|
|
55
|
+
resolveBaseUrl: DaemonCommandDeps["resolveBaseUrl"];
|
|
56
|
+
}, optionalArgs?: Omit<DaemonCommandDeps, "resolveBaseUrl">): void;
|
|
42
57
|
//# sourceMappingURL=daemon-command.d.ts.map
|
package/dist/daemon-command.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* `daemon status` / `daemon stop` — concrete commands over `@jini-ai/http-kit`'s
|
|
5
5
|
* `daemonStatusRoute`/`daemonShutdownRoute` (`packages/http/src/daemon-status.ts`).
|
|
6
|
-
* `packages/cli/
|
|
6
|
+
* `packages/cli/archived provenance ledger`'s original UNCLEAR verdict on these two
|
|
7
7
|
* blocked on `@jini-ai/http-kit` being a stub with no `/status`/`/shutdown` route to
|
|
8
8
|
* call; that blocker no longer holds (both routes exist and are tested), and
|
|
9
9
|
* `run-command.ts` is now a real, established "first concrete command"
|
|
@@ -20,29 +20,20 @@
|
|
|
20
20
|
import { exitWithStructuredError } from './errors.js';
|
|
21
21
|
import { getJsonFromDaemon, postJsonToDaemon } from './http.js';
|
|
22
22
|
import { renderUsage } from './usage.js';
|
|
23
|
-
function defaultWrite(text) {
|
|
23
|
+
function defaultWrite({ text }) {
|
|
24
24
|
process.stdout.write(text);
|
|
25
25
|
}
|
|
26
|
-
function defaultWriteErr(text) {
|
|
26
|
+
function defaultWriteErr({ text }) {
|
|
27
27
|
process.stderr.write(text);
|
|
28
28
|
}
|
|
29
|
-
const DAEMON_STATUS_USAGE = renderUsage({
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
});
|
|
33
|
-
const DAEMON_STOP_USAGE = renderUsage({
|
|
34
|
-
usage: ['daemon stop'],
|
|
35
|
-
description: 'Schedules a graceful daemon shutdown. Responds once the shutdown is scheduled, not once the process has actually exited.',
|
|
36
|
-
});
|
|
37
|
-
const DAEMON_USAGE = renderUsage({
|
|
38
|
-
usage: ['daemon <status|stop> ...'],
|
|
39
|
-
description: 'Inspect or control a running Jini daemon over HTTP.',
|
|
40
|
-
});
|
|
29
|
+
const DAEMON_STATUS_USAGE = renderUsage({ usage: ['daemon status'] }, { description: 'Reports the running daemon\'s version/host/port/dataDir/shutdown state.' });
|
|
30
|
+
const DAEMON_STOP_USAGE = renderUsage({ usage: ['daemon stop'] }, { description: 'Schedules a graceful daemon shutdown. Responds once the shutdown is scheduled, not once the process has actually exited.' });
|
|
31
|
+
const DAEMON_USAGE = renderUsage({ usage: ['daemon <status|stop> ...'] }, { description: 'Inspect or control a running Jini daemon over HTTP.' });
|
|
41
32
|
/** Builds `{write, exit, exitCodes?}` for `errors.ts`'s structured-error helpers, never assigning `exitCodes` when unset (required under `exactOptionalPropertyTypes`). */
|
|
42
33
|
function errorOptions(deps) {
|
|
43
34
|
return {
|
|
44
35
|
write: deps.writeErr ?? defaultWriteErr,
|
|
45
|
-
exit: deps.exit ?? ((code) => process.exit(code)),
|
|
36
|
+
exit: deps.exit ?? (({ code }) => process.exit(code)),
|
|
46
37
|
...(deps.exitCodes !== undefined ? { exitCodes: deps.exitCodes } : {}),
|
|
47
38
|
};
|
|
48
39
|
}
|
|
@@ -59,45 +50,48 @@ function invalidFlag(deps, message) {
|
|
|
59
50
|
return exitWithStructuredError({ code: 'invalid-flag', message }, errorOptions(deps));
|
|
60
51
|
}
|
|
61
52
|
function printJsonResult(deps, value) {
|
|
62
|
-
(deps.write ?? defaultWrite)(`${JSON.stringify(value)}\n`);
|
|
53
|
+
(deps.write ?? defaultWrite)({ text: `${JSON.stringify(value)}\n` });
|
|
63
54
|
}
|
|
64
55
|
/** `daemon status` */
|
|
65
|
-
export async function daemonStatusCommand(args,
|
|
56
|
+
export async function daemonStatusCommand({ args, resolveBaseUrl }, optionalArgs = {}) {
|
|
57
|
+
const deps = { resolveBaseUrl, ...optionalArgs };
|
|
66
58
|
if (args.includes('--help') || args.includes('-h')) {
|
|
67
|
-
(deps.write ?? defaultWrite)(`${DAEMON_STATUS_USAGE}\n`);
|
|
59
|
+
(deps.write ?? defaultWrite)({ text: `${DAEMON_STATUS_USAGE}\n` });
|
|
68
60
|
return;
|
|
69
61
|
}
|
|
70
62
|
const baseUrl = await deps.resolveBaseUrl();
|
|
71
|
-
const result = await getJsonFromDaemon(baseUrl, '/api/daemon/status', transportOptions(deps));
|
|
63
|
+
const result = await getJsonFromDaemon({ base: baseUrl, route: '/api/daemon/status' }, transportOptions(deps));
|
|
72
64
|
printJsonResult(deps, result);
|
|
73
65
|
}
|
|
74
66
|
/** `daemon stop` */
|
|
75
|
-
export async function daemonStopCommand(args,
|
|
67
|
+
export async function daemonStopCommand({ args, resolveBaseUrl }, optionalArgs = {}) {
|
|
68
|
+
const deps = { resolveBaseUrl, ...optionalArgs };
|
|
76
69
|
if (args.includes('--help') || args.includes('-h')) {
|
|
77
|
-
(deps.write ?? defaultWrite)(`${DAEMON_STOP_USAGE}\n`);
|
|
70
|
+
(deps.write ?? defaultWrite)({ text: `${DAEMON_STOP_USAGE}\n` });
|
|
78
71
|
return;
|
|
79
72
|
}
|
|
80
73
|
const baseUrl = await deps.resolveBaseUrl();
|
|
81
|
-
const result = await postJsonToDaemon(baseUrl, '/api/daemon/shutdown', {}, transportOptions(deps));
|
|
74
|
+
const result = await postJsonToDaemon({ base: baseUrl, route: '/api/daemon/shutdown', body: {} }, transportOptions(deps));
|
|
82
75
|
printJsonResult(deps, result);
|
|
83
76
|
}
|
|
84
77
|
/** Registers `daemon` (dispatching `status`/`stop` on the first remaining token) against `registry`. */
|
|
85
|
-
export function registerDaemonCommands(registry,
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
78
|
+
export function registerDaemonCommands({ registry, resolveBaseUrl }, optionalArgs = {}) {
|
|
79
|
+
const deps = { resolveBaseUrl, ...optionalArgs };
|
|
80
|
+
registry.add({ name: 'daemon', handler: async ({ args }) => {
|
|
81
|
+
const [sub, ...rest] = args;
|
|
82
|
+
switch (sub) {
|
|
83
|
+
case 'status':
|
|
84
|
+
return daemonStatusCommand({ args: rest, resolveBaseUrl: resolveBaseUrl }, optionalArgs);
|
|
85
|
+
case 'stop':
|
|
86
|
+
return daemonStopCommand({ args: rest, resolveBaseUrl: resolveBaseUrl }, optionalArgs);
|
|
87
|
+
case undefined:
|
|
88
|
+
case '--help':
|
|
89
|
+
case '-h':
|
|
90
|
+
(deps.write ?? defaultWrite)({ text: `${DAEMON_USAGE}\n` });
|
|
91
|
+
return;
|
|
92
|
+
default:
|
|
93
|
+
invalidFlag(deps, `unknown "daemon" subcommand: ${sub}`);
|
|
94
|
+
}
|
|
95
|
+
} }, { usage: DAEMON_USAGE });
|
|
102
96
|
}
|
|
103
97
|
//# sourceMappingURL=daemon-command.js.map
|
package/dist/daemon-url.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @module daemon-url
|
|
3
3
|
*
|
|
4
4
|
* Generic daemon-URL resolution, generalized from OD's
|
|
5
|
-
* `apps/daemon/src/daemon-url.ts` (see `
|
|
5
|
+
* `apps/daemon/src/daemon-url.ts` (see `archived provenance ledger`). The *order* —
|
|
6
6
|
* explicit flag, then an env var, then an injected discovery probe, then a
|
|
7
7
|
* caller-supplied default — is the reusable part. OD's own implementation
|
|
8
8
|
* hardcodes its sidecar IPC protocol and a `pnpm exec tools-dev status`
|
|
@@ -26,7 +26,11 @@ export interface ResolveDaemonUrlOptions {
|
|
|
26
26
|
* `null` when nothing was discovered so resolution falls through to
|
|
27
27
|
* `defaultUrl`.
|
|
28
28
|
*/
|
|
29
|
-
discover?: (
|
|
29
|
+
discover?: (requiredArgs: {
|
|
30
|
+
env: NodeJS.ProcessEnv;
|
|
31
|
+
}, optionalArgs: {
|
|
32
|
+
timeoutMs: number;
|
|
33
|
+
}) => Promise<string | null>;
|
|
30
34
|
/** Discovery timeout in ms. Defaults to 800ms so an absent daemon doesn't stall CLI startup. */
|
|
31
35
|
timeoutMs?: number;
|
|
32
36
|
/** Returned when the flag, env, and discovery steps all come up empty. */
|
|
@@ -36,7 +40,9 @@ export interface ResolveDaemonUrlOptions {
|
|
|
36
40
|
* succeeds. Defaults to a no-op. A user's own daemon may legitimately run
|
|
37
41
|
* on a remote, non-HTTPS host, so this only warns — it never rejects.
|
|
38
42
|
*/
|
|
39
|
-
warn?: (
|
|
43
|
+
warn?: (requiredArgs: {
|
|
44
|
+
message: string;
|
|
45
|
+
}) => void;
|
|
40
46
|
}
|
|
41
47
|
/**
|
|
42
48
|
* Resolve a daemon HTTP base URL for a CLI client command.
|
|
@@ -47,7 +53,7 @@ export interface ResolveDaemonUrlOptions {
|
|
|
47
53
|
* `http://127.0.0.1:0`-style fallback would be worse than a clear error,
|
|
48
54
|
* and this package has no locked default port to fall back to on its own.
|
|
49
55
|
*/
|
|
50
|
-
export declare function resolveDaemonUrl(options?: ResolveDaemonUrlOptions): Promise<string>;
|
|
56
|
+
export declare function resolveDaemonUrl(_requiredArgs: Record<string, never>, options?: ResolveDaemonUrlOptions): Promise<string>;
|
|
51
57
|
/**
|
|
52
58
|
* Return `url` with any embedded userinfo (`user:pass@host`) stripped, safe to include in a
|
|
53
59
|
* printed error message, log line, or structured-error envelope. Per SEC-RB-009
|
|
@@ -56,7 +62,9 @@ export declare function resolveDaemonUrl(options?: ResolveDaemonUrlOptions): Pro
|
|
|
56
62
|
* reach stderr/stdout verbatim. Returns `url` unchanged when it has no userinfo, so normal URLs
|
|
57
63
|
* are never reformatted/normalized as a side effect of display-sanitizing them.
|
|
58
64
|
*/
|
|
59
|
-
export declare function sanitizeDaemonUrlForDisplay(url
|
|
65
|
+
export declare function sanitizeDaemonUrlForDisplay({ url }: {
|
|
66
|
+
url: string;
|
|
67
|
+
}): string;
|
|
60
68
|
/**
|
|
61
69
|
* Non-fatal policy check for a resolved daemon URL: warn (never throw or reject) when it is
|
|
62
70
|
* neither loopback nor HTTPS, since that combination sends CLI traffic — including whatever
|
|
@@ -64,5 +72,7 @@ export declare function sanitizeDaemonUrlForDisplay(url: string): string;
|
|
|
64
72
|
* `null` when the URL looks safe or doesn't parse (a parse failure is reported elsewhere, by
|
|
65
73
|
* whatever code actually tries to use the URL).
|
|
66
74
|
*/
|
|
67
|
-
export declare function daemonUrlPolicyWarning(url
|
|
75
|
+
export declare function daemonUrlPolicyWarning({ url }: {
|
|
76
|
+
url: string;
|
|
77
|
+
}): string | null;
|
|
68
78
|
//# sourceMappingURL=daemon-url.d.ts.map
|
package/dist/daemon-url.js
CHANGED
|
@@ -1,15 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* @module daemon-url
|
|
3
|
-
*
|
|
4
|
-
* Generic daemon-URL resolution, generalized from OD's
|
|
5
|
-
* `apps/daemon/src/daemon-url.ts` (see `source-map.md`). The *order* —
|
|
6
|
-
* explicit flag, then an env var, then an injected discovery probe, then a
|
|
7
|
-
* caller-supplied default — is the reusable part. OD's own implementation
|
|
8
|
-
* hardcodes its sidecar IPC protocol and a `pnpm exec tools-dev status`
|
|
9
|
-
* fallback; neither belongs in a product-neutral package, so both are
|
|
10
|
-
* replaced with an injected `discover` callback the caller (e.g. a future
|
|
11
|
-
* `@jini-ai/sidecar`-backed adapter) supplies.
|
|
12
|
-
*/
|
|
1
|
+
import { isLoopbackHostname } from '@jini-ai/platform/net';
|
|
13
2
|
/**
|
|
14
3
|
* Resolve a daemon HTTP base URL for a CLI client command.
|
|
15
4
|
*
|
|
@@ -19,13 +8,13 @@
|
|
|
19
8
|
* `http://127.0.0.1:0`-style fallback would be worse than a clear error,
|
|
20
9
|
* and this package has no locked default port to fall back to on its own.
|
|
21
10
|
*/
|
|
22
|
-
export async function resolveDaemonUrl(options = {}) {
|
|
11
|
+
export async function resolveDaemonUrl(_requiredArgs, options = {}) {
|
|
23
12
|
const env = options.env ?? process.env;
|
|
24
13
|
const warn = options.warn ?? (() => { });
|
|
25
14
|
const report = (url) => {
|
|
26
|
-
const warning = daemonUrlPolicyWarning(url);
|
|
15
|
+
const warning = daemonUrlPolicyWarning({ url });
|
|
27
16
|
if (warning !== null)
|
|
28
|
-
warn(warning);
|
|
17
|
+
warn({ message: warning });
|
|
29
18
|
return url;
|
|
30
19
|
};
|
|
31
20
|
const flagUrl = options.flagUrl ?? null;
|
|
@@ -37,7 +26,7 @@ export async function resolveDaemonUrl(options = {}) {
|
|
|
37
26
|
return report(envUrl);
|
|
38
27
|
}
|
|
39
28
|
if (options.discover !== undefined) {
|
|
40
|
-
const discovered = await options.discover(env, options.timeoutMs ?? 800);
|
|
29
|
+
const discovered = await options.discover({ env }, { timeoutMs: options.timeoutMs ?? 800 });
|
|
41
30
|
if (discovered !== null && discovered.length > 0)
|
|
42
31
|
return report(discovered);
|
|
43
32
|
}
|
|
@@ -58,7 +47,7 @@ const USERINFO_PREFIX_RE = /^([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)[^/@]*@/;
|
|
|
58
47
|
* reach stderr/stdout verbatim. Returns `url` unchanged when it has no userinfo, so normal URLs
|
|
59
48
|
* are never reformatted/normalized as a side effect of display-sanitizing them.
|
|
60
49
|
*/
|
|
61
|
-
export function sanitizeDaemonUrlForDisplay(url) {
|
|
50
|
+
export function sanitizeDaemonUrlForDisplay({ url }) {
|
|
62
51
|
let parsed;
|
|
63
52
|
try {
|
|
64
53
|
parsed = new URL(url);
|
|
@@ -72,10 +61,6 @@ export function sanitizeDaemonUrlForDisplay(url) {
|
|
|
72
61
|
parsed.password = '';
|
|
73
62
|
return parsed.toString();
|
|
74
63
|
}
|
|
75
|
-
const LOOPBACK_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
|
|
76
|
-
function isLoopbackHostname(hostname) {
|
|
77
|
-
return LOOPBACK_HOSTNAMES.has(hostname.toLowerCase());
|
|
78
|
-
}
|
|
79
64
|
/**
|
|
80
65
|
* Non-fatal policy check for a resolved daemon URL: warn (never throw or reject) when it is
|
|
81
66
|
* neither loopback nor HTTPS, since that combination sends CLI traffic — including whatever
|
|
@@ -83,7 +68,7 @@ function isLoopbackHostname(hostname) {
|
|
|
83
68
|
* `null` when the URL looks safe or doesn't parse (a parse failure is reported elsewhere, by
|
|
84
69
|
* whatever code actually tries to use the URL).
|
|
85
70
|
*/
|
|
86
|
-
export function daemonUrlPolicyWarning(url) {
|
|
71
|
+
export function daemonUrlPolicyWarning({ url }) {
|
|
87
72
|
let parsed;
|
|
88
73
|
try {
|
|
89
74
|
parsed = new URL(url);
|
|
@@ -91,9 +76,9 @@ export function daemonUrlPolicyWarning(url) {
|
|
|
91
76
|
catch {
|
|
92
77
|
return null;
|
|
93
78
|
}
|
|
94
|
-
if (parsed.protocol === 'https:' || isLoopbackHostname(parsed.hostname))
|
|
79
|
+
if (parsed.protocol === 'https:' || isLoopbackHostname({ hostname: parsed.hostname }))
|
|
95
80
|
return null;
|
|
96
|
-
return (`daemon URL ${sanitizeDaemonUrlForDisplay(url)} is neither loopback nor HTTPS; ` +
|
|
81
|
+
return (`daemon URL ${sanitizeDaemonUrlForDisplay({ url: url })} is neither loopback nor HTTPS; ` +
|
|
97
82
|
'traffic to a remote, non-HTTPS daemon is not encrypted.');
|
|
98
83
|
}
|
|
99
84
|
//# sourceMappingURL=daemon-url.js.map
|