appduct 0.10.0 → 0.11.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/README.md +121 -51
- package/dist/artifact-inspect.d.ts +0 -1
- package/dist/bin.d.ts +0 -1
- package/dist/bin.js +1250 -2
- package/dist/cli/command-options.d.ts +8 -1
- package/dist/cli/create-cli.d.ts +0 -1
- package/dist/cli/dispatch.d.ts +10 -1
- package/dist/cli/envelope.d.ts +19 -0
- package/dist/cli/global-flags.d.ts +30 -0
- package/dist/cli/open-target.d.ts +25 -8
- package/dist/cli/result-types.d.ts +30 -5
- package/dist/cli/router.d.ts +78 -0
- package/dist/cli/routes/daemon/index.d.ts +9 -0
- package/dist/cli/routes/daemon/index.js +848 -0
- package/dist/cli/routes/daemon/run.d.ts +3 -0
- package/dist/cli/routes/daemon/run.js +3246 -0
- package/dist/cli/routes/daemon/start.d.ts +3 -0
- package/dist/cli/routes/daemon/start.js +1668 -0
- package/dist/cli/routes/daemon/status.d.ts +4 -0
- package/dist/cli/routes/daemon/status.js +1676 -0
- package/dist/cli/routes/daemon/stop.d.ts +4 -0
- package/dist/cli/routes/daemon/stop.js +1646 -0
- package/dist/cli/routes/doctor.d.ts +3 -0
- package/dist/cli/routes/doctor.js +1075 -0
- package/dist/cli/routes/events.d.ts +3 -0
- package/dist/cli/routes/events.js +1906 -0
- package/dist/cli/routes/init.d.ts +3 -0
- package/dist/cli/routes/init.js +1486 -0
- package/dist/cli/routes/invoke.d.ts +3 -0
- package/dist/cli/routes/invoke.js +1884 -0
- package/dist/cli/routes/keygen.d.ts +3 -0
- package/dist/cli/routes/keygen.js +928 -0
- package/dist/cli/routes/link.d.ts +3 -0
- package/dist/cli/routes/link.js +2705 -0
- package/dist/cli/routes/ls.d.ts +3 -0
- package/dist/cli/routes/ls.js +1659 -0
- package/dist/cli/routes/mcp.d.ts +5 -0
- package/dist/cli/routes/mcp.js +3917 -0
- package/dist/cli/routes/revoke.d.ts +3 -0
- package/dist/cli/routes/revoke.js +1675 -0
- package/dist/cli/routes/tools.d.ts +3 -0
- package/dist/cli/routes/tools.js +1851 -0
- package/dist/cli/runner.d.ts +3 -11
- package/dist/cli/types.d.ts +9 -6
- package/dist/cli/version-guard.d.ts +20 -0
- package/dist/cli.d.ts +0 -1
- package/dist/client/app-client.d.ts +0 -1
- package/dist/client/bootstrap.d.ts +4 -4
- package/dist/client/connect.d.ts +0 -1
- package/dist/client/errors.d.ts +0 -1
- package/dist/client/index.d.ts +0 -1
- package/dist/client/index.js +2379 -16
- package/dist/commands/daemon/context.d.ts +14 -0
- package/dist/commands/daemon/run.d.ts +11 -0
- package/dist/commands/daemon/start.d.ts +5 -0
- package/dist/commands/daemon/status.d.ts +5 -0
- package/dist/commands/daemon/stop.d.ts +5 -0
- package/dist/commands/doctor.d.ts +0 -1
- package/dist/commands/events.d.ts +0 -1
- package/dist/commands/init.d.ts +17 -4
- package/dist/commands/invoke.d.ts +0 -1
- package/dist/commands/keygen.d.ts +0 -1
- package/dist/commands/link.d.ts +1 -2
- package/dist/commands/ls.d.ts +0 -1
- package/dist/commands/mcp.d.ts +2 -4
- package/dist/commands/revoke.d.ts +0 -1
- package/dist/commands/tools.d.ts +19 -3
- package/dist/daemon/address.d.ts +0 -1
- package/dist/daemon/audit.d.ts +6 -11
- package/dist/daemon/calls.d.ts +0 -1
- package/dist/daemon/config.d.ts +13 -16
- package/dist/daemon/daemon.d.ts +0 -1
- package/dist/daemon/event-bus.d.ts +0 -1
- package/dist/daemon/links.d.ts +0 -1
- package/dist/daemon/listener.d.ts +0 -1
- package/dist/daemon/log-rotation.d.ts +0 -1
- package/dist/daemon/pidfile.d.ts +14 -8
- package/dist/daemon/policy.d.ts +0 -1
- package/dist/daemon/registry.d.ts +0 -1
- package/dist/daemon/rpc-errors.d.ts +16 -0
- package/dist/daemon/rpc-server.d.ts +0 -11
- package/dist/daemon/sessions.d.ts +0 -1
- package/dist/daemon/socket-probe.d.ts +0 -1
- package/dist/daemon/state-dir.d.ts +0 -1
- package/dist/daemon/timers.d.ts +0 -1
- package/dist/daemon/tls.d.ts +0 -1
- package/dist/errors.d.ts +0 -1
- package/dist/host-certificate.d.ts +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +1307 -3
- package/dist/key-material.d.ts +0 -1
- package/dist/link.d.ts +8 -7
- package/dist/mcp/app-tools.d.ts +153 -0
- package/dist/mcp/connect-tool.d.ts +14 -8
- package/dist/mcp/daemon-tools.d.ts +2 -11
- package/dist/mcp/events-tool.d.ts +0 -1
- package/dist/mcp/server.d.ts +33 -14
- package/dist/native-scheme.d.ts +0 -1
- package/dist/output.d.ts +8 -11
- package/dist/package-root.d.ts +11 -0
- package/dist/package-version.d.ts +0 -1
- package/dist/qr-terminal.d.ts +0 -1
- package/dist/rpc/client.d.ts +0 -1
- package/dist/scheme.d.ts +60 -2
- package/dist/spki-pin.d.ts +0 -1
- package/package.json +4 -3
- package/dist/artifact-inspect.d.ts.map +0 -1
- package/dist/artifact-inspect.js +0 -334
- package/dist/bin.d.ts.map +0 -1
- package/dist/cli/command-options.d.ts.map +0 -1
- package/dist/cli/command-options.js +0 -95
- package/dist/cli/create-cli.d.ts.map +0 -1
- package/dist/cli/create-cli.js +0 -56
- package/dist/cli/dispatch.d.ts.map +0 -1
- package/dist/cli/dispatch.js +0 -275
- package/dist/cli/open-target.d.ts.map +0 -1
- package/dist/cli/open-target.js +0 -438
- package/dist/cli/result-types.d.ts.map +0 -1
- package/dist/cli/result-types.js +0 -6
- package/dist/cli/runner.d.ts.map +0 -1
- package/dist/cli/runner.js +0 -111
- package/dist/cli/types.d.ts.map +0 -1
- package/dist/cli/types.js +0 -10
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -2
- package/dist/client/app-client.d.ts.map +0 -1
- package/dist/client/app-client.js +0 -223
- package/dist/client/bootstrap.d.ts.map +0 -1
- package/dist/client/bootstrap.js +0 -108
- package/dist/client/connect.d.ts.map +0 -1
- package/dist/client/connect.js +0 -38
- package/dist/client/errors.d.ts.map +0 -1
- package/dist/client/errors.js +0 -40
- package/dist/client/index.d.ts.map +0 -1
- package/dist/commands/daemon.d.ts +0 -22
- package/dist/commands/daemon.d.ts.map +0 -1
- package/dist/commands/daemon.js +0 -128
- package/dist/commands/doctor.d.ts.map +0 -1
- package/dist/commands/doctor.js +0 -42
- package/dist/commands/events.d.ts.map +0 -1
- package/dist/commands/events.js +0 -76
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js +0 -231
- package/dist/commands/invoke.d.ts.map +0 -1
- package/dist/commands/invoke.js +0 -54
- package/dist/commands/keygen.d.ts.map +0 -1
- package/dist/commands/keygen.js +0 -46
- package/dist/commands/link.d.ts.map +0 -1
- package/dist/commands/link.js +0 -43
- package/dist/commands/ls.d.ts.map +0 -1
- package/dist/commands/ls.js +0 -7
- package/dist/commands/mcp.d.ts.map +0 -1
- package/dist/commands/mcp.js +0 -98
- package/dist/commands/revoke.d.ts.map +0 -1
- package/dist/commands/revoke.js +0 -7
- package/dist/commands/tools.d.ts.map +0 -1
- package/dist/commands/tools.js +0 -52
- package/dist/daemon/address.d.ts.map +0 -1
- package/dist/daemon/address.js +0 -71
- package/dist/daemon/audit.d.ts.map +0 -1
- package/dist/daemon/audit.js +0 -237
- package/dist/daemon/calls.d.ts.map +0 -1
- package/dist/daemon/calls.js +0 -158
- package/dist/daemon/config.d.ts.map +0 -1
- package/dist/daemon/config.js +0 -170
- package/dist/daemon/daemon.d.ts.map +0 -1
- package/dist/daemon/daemon.js +0 -560
- package/dist/daemon/event-bus.d.ts.map +0 -1
- package/dist/daemon/event-bus.js +0 -111
- package/dist/daemon/links.d.ts.map +0 -1
- package/dist/daemon/links.js +0 -118
- package/dist/daemon/listener.d.ts.map +0 -1
- package/dist/daemon/listener.js +0 -132
- package/dist/daemon/log-rotation.d.ts.map +0 -1
- package/dist/daemon/log-rotation.js +0 -136
- package/dist/daemon/pidfile.d.ts.map +0 -1
- package/dist/daemon/pidfile.js +0 -90
- package/dist/daemon/policy.d.ts.map +0 -1
- package/dist/daemon/policy.js +0 -24
- package/dist/daemon/registry.d.ts.map +0 -1
- package/dist/daemon/registry.js +0 -31
- package/dist/daemon/rpc-server.d.ts.map +0 -1
- package/dist/daemon/rpc-server.js +0 -203
- package/dist/daemon/sessions.d.ts.map +0 -1
- package/dist/daemon/sessions.js +0 -429
- package/dist/daemon/socket-probe.d.ts.map +0 -1
- package/dist/daemon/socket-probe.js +0 -28
- package/dist/daemon/state-dir.d.ts.map +0 -1
- package/dist/daemon/state-dir.js +0 -43
- package/dist/daemon/timers.d.ts.map +0 -1
- package/dist/daemon/timers.js +0 -12
- package/dist/daemon/tls.d.ts.map +0 -1
- package/dist/daemon/tls.js +0 -79
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -162
- package/dist/host-certificate.d.ts.map +0 -1
- package/dist/host-certificate.js +0 -111
- package/dist/index.d.ts.map +0 -1
- package/dist/key-material.d.ts.map +0 -1
- package/dist/key-material.js +0 -42
- package/dist/link.d.ts.map +0 -1
- package/dist/link.js +0 -117
- package/dist/mcp/connect-tool.d.ts.map +0 -1
- package/dist/mcp/connect-tool.js +0 -396
- package/dist/mcp/daemon-tools.d.ts.map +0 -1
- package/dist/mcp/daemon-tools.js +0 -29
- package/dist/mcp/events-tool.d.ts.map +0 -1
- package/dist/mcp/events-tool.js +0 -322
- package/dist/mcp/server.d.ts.map +0 -1
- package/dist/mcp/server.js +0 -538
- package/dist/mcp/tool-mapping.d.ts +0 -58
- package/dist/mcp/tool-mapping.d.ts.map +0 -1
- package/dist/mcp/tool-mapping.js +0 -143
- package/dist/mcp/tool-namespace.d.ts +0 -40
- package/dist/mcp/tool-namespace.d.ts.map +0 -1
- package/dist/mcp/tool-namespace.js +0 -65
- package/dist/native-scheme.d.ts.map +0 -1
- package/dist/native-scheme.js +0 -465
- package/dist/output.d.ts.map +0 -1
- package/dist/output.js +0 -342
- package/dist/package-version.d.ts.map +0 -1
- package/dist/package-version.js +0 -47
- package/dist/qr-terminal.d.ts.map +0 -1
- package/dist/qr-terminal.js +0 -45
- package/dist/rpc/client.d.ts.map +0 -1
- package/dist/rpc/client.js +0 -819
- package/dist/scheme.d.ts.map +0 -1
- package/dist/scheme.js +0 -321
- package/dist/spki-pin.d.ts.map +0 -1
- package/dist/spki-pin.js +0 -14
package/dist/index.js
CHANGED
|
@@ -1,3 +1,1307 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// src/cli/create-cli.ts
|
|
2
|
+
import { cac } from "cac";
|
|
3
|
+
|
|
4
|
+
// src/package-version.ts
|
|
5
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
6
|
+
import { join as join2 } from "node:path";
|
|
7
|
+
|
|
8
|
+
// src/package-root.ts
|
|
9
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
var PACKAGE_NAME = "appduct";
|
|
13
|
+
var cached;
|
|
14
|
+
var getPackageRoot = () => {
|
|
15
|
+
if (cached !== void 0) {
|
|
16
|
+
return cached;
|
|
17
|
+
}
|
|
18
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
for (; ; ) {
|
|
20
|
+
const candidate = join(dir, "package.json");
|
|
21
|
+
if (existsSync(candidate)) {
|
|
22
|
+
let name;
|
|
23
|
+
try {
|
|
24
|
+
name = JSON.parse(readFileSync(candidate, "utf8")).name;
|
|
25
|
+
} catch {
|
|
26
|
+
}
|
|
27
|
+
if (name === PACKAGE_NAME) {
|
|
28
|
+
cached = dir;
|
|
29
|
+
return dir;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const parent = dirname(dir);
|
|
33
|
+
if (parent === dir) {
|
|
34
|
+
throw new Error(`Could not locate the "${PACKAGE_NAME}" package root above ${import.meta.url}.`);
|
|
35
|
+
}
|
|
36
|
+
dir = parent;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// src/package-version.ts
|
|
41
|
+
var cached2;
|
|
42
|
+
var getPackageVersion = () => {
|
|
43
|
+
cached2 ??= JSON.parse(readFileSync2(join2(getPackageRoot(), "package.json"), "utf8")).version;
|
|
44
|
+
return cached2;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// src/cli/global-flags.ts
|
|
48
|
+
var globalFlagDefinitions = [
|
|
49
|
+
{
|
|
50
|
+
key: "json",
|
|
51
|
+
spec: "--json",
|
|
52
|
+
description: "Print machine-readable JSON (NDJSON for streaming commands).",
|
|
53
|
+
fromOptions: (options) => Boolean(options.json),
|
|
54
|
+
fromArgv: (argv) => argv.includes("--json")
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
key: "pretty",
|
|
58
|
+
spec: "--pretty",
|
|
59
|
+
description: "Indent JSON output. Applies to --json results and to JSON values embedded in human output; never to NDJSON event lines.",
|
|
60
|
+
fromOptions: (options) => Boolean(options.pretty),
|
|
61
|
+
fromArgv: (argv) => argv.includes("--pretty")
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
key: "verbose",
|
|
65
|
+
spec: "--verbose",
|
|
66
|
+
description: "Include command metadata (command, timestamp, duration) in the output.",
|
|
67
|
+
fromOptions: (options) => Boolean(options.verbose),
|
|
68
|
+
fromArgv: (argv) => argv.includes("--verbose")
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
key: "color",
|
|
72
|
+
spec: "--no-color",
|
|
73
|
+
description: "Disable terminal color in human-readable output.",
|
|
74
|
+
// `cac` reports a `--no-<flag>` option as `<flag>: false` when passed, and omits it (leaving
|
|
75
|
+
// the parsed value `undefined`) otherwise — so "not explicitly disabled" is the default `true`.
|
|
76
|
+
fromOptions: (options) => options.color !== false,
|
|
77
|
+
fromArgv: (argv) => !argv.includes("--no-color")
|
|
78
|
+
}
|
|
79
|
+
];
|
|
80
|
+
var registerGlobalFlags = (cli) => {
|
|
81
|
+
for (const definition of globalFlagDefinitions) {
|
|
82
|
+
cli.option(definition.spec, definition.description);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
var resolveGlobalFlags = (options) => {
|
|
86
|
+
const flags = {};
|
|
87
|
+
for (const definition of globalFlagDefinitions) {
|
|
88
|
+
flags[definition.key] = definition.fromOptions(options);
|
|
89
|
+
}
|
|
90
|
+
return flags;
|
|
91
|
+
};
|
|
92
|
+
var resolveGlobalFlagsFromArgv = (argv) => {
|
|
93
|
+
const flags = {};
|
|
94
|
+
for (const definition of globalFlagDefinitions) {
|
|
95
|
+
flags[definition.key] = definition.fromArgv(argv);
|
|
96
|
+
}
|
|
97
|
+
return flags;
|
|
98
|
+
};
|
|
99
|
+
var formatJson = (value, flags) => {
|
|
100
|
+
return flags.pretty ? JSON.stringify(value, null, 2) : JSON.stringify(value);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// src/cli/create-cli.ts
|
|
104
|
+
var createCli = () => {
|
|
105
|
+
const version = getPackageVersion();
|
|
106
|
+
const cli = cac("appduct");
|
|
107
|
+
registerGlobalFlags(cli);
|
|
108
|
+
cli.option("--state-dir <path>", "Override the Appduct state directory (default: ~/.appduct).");
|
|
109
|
+
cli.option(
|
|
110
|
+
"--daemon-restart",
|
|
111
|
+
"On a daemon/CLI version mismatch, restart the daemon even though that drops live sessions and unclaimed links."
|
|
112
|
+
);
|
|
113
|
+
cli.command("init", "Set up the current app directory: write .appduct/config.json and print the MCP snippet.").option(
|
|
114
|
+
"--scheme <scheme>",
|
|
115
|
+
`Deep-link URI scheme to write. Only this flag and static project files in <cwd> are consulted (app.json's "expo.scheme", then Android app/build.gradle(.kts) and app/src/main/AndroidManifest.xml, then iOS Info.plist and project.yml) \u2014 not APPDUCT_SCHEME, and no walk-up: init decides what to write here, so it never bakes an ambient value into a committed file.`
|
|
116
|
+
).option("--force", "Replace the scheme already recorded in the project config.").option(
|
|
117
|
+
"--ios-app-id <id>",
|
|
118
|
+
'iOS bundle id to write as "appId.ios" (needed to deliver a link with --open ios-device).'
|
|
119
|
+
).option(
|
|
120
|
+
"--android-app-id <id>",
|
|
121
|
+
'Android package name to write as "appId.android" (needed to deliver a link with --open android).'
|
|
122
|
+
);
|
|
123
|
+
cli.command("keygen", "Generate an Appduct host private key and print its app fingerprint.").option("--out <path>", "Destination path (default: <state-dir>/key.pem).").option("--force", "Overwrite an existing key at the destination path.");
|
|
124
|
+
cli.command("link", "Mint a pending session and print its deep link.").option("--ttl <seconds>", "Link time-to-live in seconds (default: from config.json).").option("--qr", "Also render the deep link as a terminal QR code.").option(
|
|
125
|
+
"--scheme <scheme>",
|
|
126
|
+
`Deep-link URI scheme (also: APPDUCT_SCHEME; default: .appduct/config.json, then app.json's "expo.scheme", then the Android/iOS project files in <cwd>).`
|
|
127
|
+
).option(
|
|
128
|
+
"--open <target>",
|
|
129
|
+
"Deliver the link automatically via adb/simctl/devicectl (android|ios-sim|ios-device; ios-device is experimental)."
|
|
130
|
+
).option("--device <id>", "adb serial, simulator udid or paired-device udid to target when --open is ambiguous.").option(
|
|
131
|
+
"--app-id <id>",
|
|
132
|
+
`Installed app id for --open android/ios-device (default: .appduct/config.json's "appId.<platform>").`
|
|
133
|
+
).option(
|
|
134
|
+
"--relaunch",
|
|
135
|
+
"With --open ios-device, terminate a running instance first (try this if delivery to an already-running app does nothing)."
|
|
136
|
+
);
|
|
137
|
+
cli.command("ls", "List Appduct sessions.");
|
|
138
|
+
cli.command("tools [selector] [name]", "List a session's tools, or show one tool's full schema.").option("--full", "Render full schemas/annotations for every listed tool.").option("--group <name>", 'Only tools in this group ("checkout" includes "checkout/payment").').option("--groups", "List the session's groups with tool counts instead of its tools.").option("--filter <text>", "Only tools whose name or description contains this text (case-insensitive).").option("--limit <n>", "Show at most n tools.").option("--offset <n>", "Skip the first n tools of the sorted list.");
|
|
139
|
+
cli.command("invoke [selector] [tool]", "Call a tool on a session.").option("--input <json>", "Tool input arguments as a JSON object.").option(
|
|
140
|
+
"--timeout <ms>",
|
|
141
|
+
"Call timeout in milliseconds. Shortens the deadline; it cannot extend one past the app's own timer, which is the tool's declared timeoutMs (else 10000)."
|
|
142
|
+
);
|
|
143
|
+
cli.command("events [selector]", "Stream session/tool events until interrupted.").option("--follow", "Accepted for script readability; the default behavior already follows.").option("--since <cursor>", "One-shot: print events retained since this cursor instead of streaming live.");
|
|
144
|
+
cli.command("revoke [selector]", "Revoke a session.");
|
|
145
|
+
cli.command("mcp", "Start a stdio MCP server that gives MCP clients access to connected apps' tools.").option(
|
|
146
|
+
"--scheme <scheme>",
|
|
147
|
+
"Deep-link URI scheme for appduct_connect (also: APPDUCT_SCHEME)."
|
|
148
|
+
);
|
|
149
|
+
cli.command("doctor <artifact>", "Report (or assert) whether a built .app/.ipa/.apk/.aab contains Appduct.").option("--assert-present", "Exit non-zero (and report) if Appduct is not present in the artifact.").option("--assert-absent", "Exit non-zero (and report) if Appduct is present in the artifact.");
|
|
150
|
+
cli.command("daemon [action]", "Manage the Appduct daemon: run, start, stop, or status.");
|
|
151
|
+
cli.help();
|
|
152
|
+
cli.version(version);
|
|
153
|
+
return cli;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// src/daemon/config.ts
|
|
157
|
+
import { readFile } from "node:fs/promises";
|
|
158
|
+
var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
159
|
+
"wssPort",
|
|
160
|
+
"keyPath",
|
|
161
|
+
"graceSeconds",
|
|
162
|
+
"linkTtlSeconds",
|
|
163
|
+
"keepaliveIntervalSeconds",
|
|
164
|
+
"eventBufferSize",
|
|
165
|
+
"auditRetentionDays",
|
|
166
|
+
"daemonLogMaxBytes",
|
|
167
|
+
"policy",
|
|
168
|
+
"advertisedIp",
|
|
169
|
+
"scheme",
|
|
170
|
+
"restartDaemonOnVersionMismatch"
|
|
171
|
+
]);
|
|
172
|
+
var KNOWN_POLICY_KEYS = /* @__PURE__ */ new Set(["default", "destructive", "tools"]);
|
|
173
|
+
var POLICY_DECISIONS = /* @__PURE__ */ new Set(["allow", "deny", "prompt"]);
|
|
174
|
+
var DEFAULT_AUDIT_RETENTION_DAYS = 30;
|
|
175
|
+
var DEFAULT_DAEMON_LOG_MAX_BYTES = 10 * 1024 * 1024;
|
|
176
|
+
var AppductConfigError = class extends Error {
|
|
177
|
+
constructor(key, message) {
|
|
178
|
+
super(message);
|
|
179
|
+
this.key = key;
|
|
180
|
+
this.name = "AppductConfigError";
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
var configError = (key, message) => {
|
|
184
|
+
return new AppductConfigError(key, `Invalid Appduct config value for "${key}": ${message}`);
|
|
185
|
+
};
|
|
186
|
+
var requireWssPort = (value, key) => {
|
|
187
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > 65535) {
|
|
188
|
+
throw configError(key, "must be a port number between 0 and 65535 (0 binds an OS-assigned port).");
|
|
189
|
+
}
|
|
190
|
+
return value;
|
|
191
|
+
};
|
|
192
|
+
var requirePositiveInteger = (value, key) => {
|
|
193
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
194
|
+
throw configError(key, "must be a positive integer.");
|
|
195
|
+
}
|
|
196
|
+
return value;
|
|
197
|
+
};
|
|
198
|
+
var requireNonEmptyString = (value, key) => {
|
|
199
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
200
|
+
throw configError(key, "must be a non-empty string.");
|
|
201
|
+
}
|
|
202
|
+
return value;
|
|
203
|
+
};
|
|
204
|
+
var requireBoolean = (value, key) => {
|
|
205
|
+
if (typeof value !== "boolean") {
|
|
206
|
+
throw configError(key, "must be a boolean.");
|
|
207
|
+
}
|
|
208
|
+
return value;
|
|
209
|
+
};
|
|
210
|
+
var requirePolicyDecision = (value, key) => {
|
|
211
|
+
if (typeof value !== "string" || !POLICY_DECISIONS.has(value)) {
|
|
212
|
+
throw configError(key, 'must be "allow", "deny", or "prompt".');
|
|
213
|
+
}
|
|
214
|
+
return value;
|
|
215
|
+
};
|
|
216
|
+
var defaultConfig = (paths) => {
|
|
217
|
+
return {
|
|
218
|
+
wssPort: 8443,
|
|
219
|
+
keyPath: paths.keyPath,
|
|
220
|
+
graceSeconds: 600,
|
|
221
|
+
linkTtlSeconds: 300,
|
|
222
|
+
keepaliveIntervalSeconds: 15,
|
|
223
|
+
eventBufferSize: 256,
|
|
224
|
+
auditRetentionDays: DEFAULT_AUDIT_RETENTION_DAYS,
|
|
225
|
+
daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
|
|
226
|
+
restartDaemonOnVersionMismatch: false,
|
|
227
|
+
policy: {
|
|
228
|
+
default: "allow",
|
|
229
|
+
destructive: "allow"
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
};
|
|
233
|
+
var loadConfig = async (paths, options = {}) => {
|
|
234
|
+
const warn = options.warn ?? (() => {
|
|
235
|
+
});
|
|
236
|
+
const config = defaultConfig(paths);
|
|
237
|
+
let raw;
|
|
238
|
+
try {
|
|
239
|
+
raw = JSON.parse(await readFile(paths.configPath, "utf8"));
|
|
240
|
+
} catch (error) {
|
|
241
|
+
if (error.code === "ENOENT") {
|
|
242
|
+
return config;
|
|
243
|
+
}
|
|
244
|
+
throw configError("config.json", `could not be read/parsed (${error.message}).`);
|
|
245
|
+
}
|
|
246
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
247
|
+
throw configError("config.json", "must be a JSON object.");
|
|
248
|
+
}
|
|
249
|
+
const parsed = raw;
|
|
250
|
+
for (const key of Object.keys(parsed)) {
|
|
251
|
+
if (!KNOWN_TOP_LEVEL_KEYS.has(key)) {
|
|
252
|
+
warn(`Unknown Appduct config key "${key}" is ignored.`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (parsed.wssPort !== void 0) {
|
|
256
|
+
config.wssPort = requireWssPort(parsed.wssPort, "wssPort");
|
|
257
|
+
}
|
|
258
|
+
if (parsed.keyPath !== void 0) {
|
|
259
|
+
config.keyPath = requireNonEmptyString(parsed.keyPath, "keyPath");
|
|
260
|
+
}
|
|
261
|
+
if (parsed.advertisedIp !== void 0) {
|
|
262
|
+
config.advertisedIp = requireNonEmptyString(parsed.advertisedIp, "advertisedIp");
|
|
263
|
+
}
|
|
264
|
+
if (parsed.scheme !== void 0) {
|
|
265
|
+
config.scheme = requireNonEmptyString(parsed.scheme, "scheme");
|
|
266
|
+
}
|
|
267
|
+
if (parsed.graceSeconds !== void 0) {
|
|
268
|
+
config.graceSeconds = requirePositiveInteger(parsed.graceSeconds, "graceSeconds");
|
|
269
|
+
}
|
|
270
|
+
if (parsed.linkTtlSeconds !== void 0) {
|
|
271
|
+
config.linkTtlSeconds = requirePositiveInteger(parsed.linkTtlSeconds, "linkTtlSeconds");
|
|
272
|
+
}
|
|
273
|
+
if (parsed.keepaliveIntervalSeconds !== void 0) {
|
|
274
|
+
config.keepaliveIntervalSeconds = requirePositiveInteger(
|
|
275
|
+
parsed.keepaliveIntervalSeconds,
|
|
276
|
+
"keepaliveIntervalSeconds"
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
if (parsed.eventBufferSize !== void 0) {
|
|
280
|
+
config.eventBufferSize = requirePositiveInteger(parsed.eventBufferSize, "eventBufferSize");
|
|
281
|
+
}
|
|
282
|
+
if (parsed.auditRetentionDays !== void 0) {
|
|
283
|
+
config.auditRetentionDays = requirePositiveInteger(parsed.auditRetentionDays, "auditRetentionDays");
|
|
284
|
+
}
|
|
285
|
+
if (parsed.daemonLogMaxBytes !== void 0) {
|
|
286
|
+
config.daemonLogMaxBytes = requirePositiveInteger(parsed.daemonLogMaxBytes, "daemonLogMaxBytes");
|
|
287
|
+
}
|
|
288
|
+
if (parsed.restartDaemonOnVersionMismatch !== void 0) {
|
|
289
|
+
config.restartDaemonOnVersionMismatch = requireBoolean(
|
|
290
|
+
parsed.restartDaemonOnVersionMismatch,
|
|
291
|
+
"restartDaemonOnVersionMismatch"
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
if (parsed.policy !== void 0) {
|
|
295
|
+
if (typeof parsed.policy !== "object" || parsed.policy === null || Array.isArray(parsed.policy)) {
|
|
296
|
+
throw configError("policy", "must be an object.");
|
|
297
|
+
}
|
|
298
|
+
const policy = parsed.policy;
|
|
299
|
+
for (const key of Object.keys(policy)) {
|
|
300
|
+
if (!KNOWN_POLICY_KEYS.has(key)) {
|
|
301
|
+
warn(`Unknown Appduct config key "policy.${key}" is ignored.`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (policy.default !== void 0) {
|
|
305
|
+
config.policy.default = requirePolicyDecision(policy.default, "policy.default");
|
|
306
|
+
}
|
|
307
|
+
if (policy.destructive !== void 0) {
|
|
308
|
+
config.policy.destructive = requirePolicyDecision(policy.destructive, "policy.destructive");
|
|
309
|
+
}
|
|
310
|
+
if (policy.tools !== void 0) {
|
|
311
|
+
if (typeof policy.tools !== "object" || policy.tools === null || Array.isArray(policy.tools)) {
|
|
312
|
+
throw configError("policy.tools", "must be an object.");
|
|
313
|
+
}
|
|
314
|
+
const tools = {};
|
|
315
|
+
for (const [toolKey, toolValue] of Object.entries(policy.tools)) {
|
|
316
|
+
tools[toolKey] = requirePolicyDecision(toolValue, `policy.tools["${toolKey}"]`);
|
|
317
|
+
}
|
|
318
|
+
config.policy.tools = tools;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return config;
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
// src/daemon/state-dir.ts
|
|
325
|
+
import { homedir } from "node:os";
|
|
326
|
+
import { join as join3 } from "node:path";
|
|
327
|
+
var resolveStateDir = (override) => {
|
|
328
|
+
return override ?? process.env.APPDUCT_STATE_DIR ?? join3(homedir(), ".appduct");
|
|
329
|
+
};
|
|
330
|
+
var getStateDirPaths = (stateDir) => {
|
|
331
|
+
return {
|
|
332
|
+
root: stateDir,
|
|
333
|
+
socketPath: join3(stateDir, "daemon.sock"),
|
|
334
|
+
pidFilePath: join3(stateDir, "daemon.pid"),
|
|
335
|
+
logFilePath: join3(stateDir, "daemon.log"),
|
|
336
|
+
keyPath: join3(stateDir, "key.pem"),
|
|
337
|
+
configPath: join3(stateDir, "config.json"),
|
|
338
|
+
auditDir: join3(stateDir, "audit"),
|
|
339
|
+
spawnLockPath: join3(stateDir, "daemon.spawn.lock")
|
|
340
|
+
};
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
// src/rpc/client.ts
|
|
344
|
+
import { join as join4 } from "node:path";
|
|
345
|
+
import { RPC_METHODS } from "@appduct/shared";
|
|
346
|
+
var defaultBinPath = join4(getPackageRoot(), "bin.js");
|
|
347
|
+
var DaemonRpcError = class extends Error {
|
|
348
|
+
constructor(code, message, data) {
|
|
349
|
+
super(message);
|
|
350
|
+
this.code = code;
|
|
351
|
+
this.data = data;
|
|
352
|
+
this.name = "DaemonRpcError";
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
var DaemonUnavailableError = class extends Error {
|
|
356
|
+
constructor(message, reason = "unreachable") {
|
|
357
|
+
super(message);
|
|
358
|
+
this.reason = reason;
|
|
359
|
+
this.name = "DaemonUnavailableError";
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
var describeLiveState = (state) => {
|
|
363
|
+
const parts = [];
|
|
364
|
+
if (state.sessions > 0) {
|
|
365
|
+
parts.push(`${state.sessions} connected session(s)`);
|
|
366
|
+
}
|
|
367
|
+
if (state.pendingLinks > 0) {
|
|
368
|
+
parts.push(`${state.pendingLinks} unclaimed link(s)`);
|
|
369
|
+
}
|
|
370
|
+
return parts.join(" and ");
|
|
371
|
+
};
|
|
372
|
+
var DaemonVersionMismatchError = class extends Error {
|
|
373
|
+
constructor(daemonVersion, clientVersion, live, message) {
|
|
374
|
+
super(
|
|
375
|
+
message ?? `The running Appduct daemon is version ${daemonVersion}, but this client is version ${clientVersion}. It was not restarted automatically because ${describeLiveState(live)} would be lost. Run "appduct daemon stop" once that state is expendable, or force it with "--daemon-restart".`
|
|
376
|
+
);
|
|
377
|
+
this.daemonVersion = daemonVersion;
|
|
378
|
+
this.clientVersion = clientVersion;
|
|
379
|
+
this.name = "DaemonVersionMismatchError";
|
|
380
|
+
this.sessionCount = live.sessions;
|
|
381
|
+
this.pendingLinkCount = live.pendingLinks;
|
|
382
|
+
}
|
|
383
|
+
sessionCount;
|
|
384
|
+
pendingLinkCount;
|
|
385
|
+
};
|
|
386
|
+
var isConnectionMissingError = (error) => {
|
|
387
|
+
const code = error.code;
|
|
388
|
+
return code === "ENOENT" || code === "ECONNREFUSED";
|
|
389
|
+
};
|
|
390
|
+
var isDaemonUnreachableError = (error) => {
|
|
391
|
+
return error instanceof DaemonUnavailableError || isConnectionMissingError(error);
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
// src/errors.ts
|
|
395
|
+
var EXIT_CODE_BY_ERROR_TYPE = {
|
|
396
|
+
usage_error: 64,
|
|
397
|
+
validation_error: 65,
|
|
398
|
+
// EX_NOINPUT (66): "an input file did not exist or was not readable" — `doctor` throws this for a
|
|
399
|
+
// missing external tool or an unreadable/corrupt artifact, deliberately distinct from every other
|
|
400
|
+
// code here so a release pipeline can't mistake "we couldn't tell" for either a clean pass or an
|
|
401
|
+
// assertion failure (docs/tasks/08-appduct-doctor.md).
|
|
402
|
+
inspection_error: 66,
|
|
403
|
+
connection_error: 70,
|
|
404
|
+
session_error: 71,
|
|
405
|
+
tool_error: 72,
|
|
406
|
+
permission_error: 77,
|
|
407
|
+
internal_error: 1,
|
|
408
|
+
// `doctor --assert-present`/`--assert-absent`: the artifact was inspected successfully and the
|
|
409
|
+
// assertion did not hold. Distinct from `inspection_error` (66) — this means the answer is known
|
|
410
|
+
// and it's "no", not "couldn't tell" — and distinct from `internal_error` (1) so a caller can't
|
|
411
|
+
// conflate a normal assertion failure with an unexpected crash.
|
|
412
|
+
assertion_error: 3
|
|
413
|
+
};
|
|
414
|
+
var RPC_ERROR_EXIT_CLASS = {
|
|
415
|
+
no_session: "session_error",
|
|
416
|
+
ambiguous_session: "session_error",
|
|
417
|
+
unknown_session: "session_error",
|
|
418
|
+
session_not_active: "session_error",
|
|
419
|
+
session_suspended: "session_error",
|
|
420
|
+
tool_not_found: "tool_error",
|
|
421
|
+
tool_input_validation_error: "tool_error",
|
|
422
|
+
tool_output_validation_error: "tool_error",
|
|
423
|
+
tool_execution_error: "tool_error",
|
|
424
|
+
tool_serialization_error: "tool_error",
|
|
425
|
+
tool_timeout: "tool_error",
|
|
426
|
+
tool_cancelled: "tool_error",
|
|
427
|
+
policy_denied: "permission_error",
|
|
428
|
+
invalid_request: "validation_error"
|
|
429
|
+
};
|
|
430
|
+
var AppductCliError = class extends Error {
|
|
431
|
+
type;
|
|
432
|
+
details;
|
|
433
|
+
constructor(type, message, details) {
|
|
434
|
+
super(message);
|
|
435
|
+
this.name = "AppductCliError";
|
|
436
|
+
this.type = type;
|
|
437
|
+
this.details = details;
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
var usageError = (message, details) => {
|
|
441
|
+
return new AppductCliError("usage_error", message, details);
|
|
442
|
+
};
|
|
443
|
+
var validationError = (message, details) => {
|
|
444
|
+
return new AppductCliError("validation_error", message, details);
|
|
445
|
+
};
|
|
446
|
+
var connectionError = (message, details) => {
|
|
447
|
+
return new AppductCliError("connection_error", message, details);
|
|
448
|
+
};
|
|
449
|
+
var sessionError = (message, details) => {
|
|
450
|
+
return new AppductCliError("session_error", message, details);
|
|
451
|
+
};
|
|
452
|
+
var toolError = (message, details) => {
|
|
453
|
+
return new AppductCliError("tool_error", message, details);
|
|
454
|
+
};
|
|
455
|
+
var permissionError = (message, details) => {
|
|
456
|
+
return new AppductCliError("permission_error", message, details);
|
|
457
|
+
};
|
|
458
|
+
var internalError = (message, details) => {
|
|
459
|
+
return new AppductCliError("internal_error", message, details);
|
|
460
|
+
};
|
|
461
|
+
var inspectionError = (message, details) => {
|
|
462
|
+
return new AppductCliError("inspection_error", message, details);
|
|
463
|
+
};
|
|
464
|
+
var assertionError = (message, details) => {
|
|
465
|
+
return new AppductCliError("assertion_error", message, details);
|
|
466
|
+
};
|
|
467
|
+
var isAppductCliError = (value) => {
|
|
468
|
+
return value instanceof AppductCliError;
|
|
469
|
+
};
|
|
470
|
+
var rpcErrorClass = (error) => {
|
|
471
|
+
const type = error.data?.type;
|
|
472
|
+
return type ? RPC_ERROR_EXIT_CLASS[type] : void 0;
|
|
473
|
+
};
|
|
474
|
+
var toCliError = (error) => {
|
|
475
|
+
if (isAppductCliError(error)) {
|
|
476
|
+
return {
|
|
477
|
+
type: error.type,
|
|
478
|
+
message: error.message,
|
|
479
|
+
details: error.details
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
if (error instanceof DaemonVersionMismatchError) {
|
|
483
|
+
return {
|
|
484
|
+
type: "connection_error",
|
|
485
|
+
message: error.message,
|
|
486
|
+
details: {
|
|
487
|
+
daemon_version: error.daemonVersion,
|
|
488
|
+
client_version: error.clientVersion,
|
|
489
|
+
session_count: error.sessionCount,
|
|
490
|
+
pending_link_count: error.pendingLinkCount
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
if (error instanceof DaemonRpcError && error.data?.type) {
|
|
495
|
+
return {
|
|
496
|
+
type: error.data.type,
|
|
497
|
+
message: error.message,
|
|
498
|
+
details: error.data.details
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
if (isDaemonUnreachableError(error)) {
|
|
502
|
+
return {
|
|
503
|
+
type: "connection_error",
|
|
504
|
+
message: error instanceof Error ? error.message : "The Appduct daemon is unreachable."
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
if (error instanceof Error) {
|
|
508
|
+
return {
|
|
509
|
+
type: "internal_error",
|
|
510
|
+
message: error.message
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
return {
|
|
514
|
+
type: "internal_error",
|
|
515
|
+
message: "An unexpected error occurred.",
|
|
516
|
+
details: error
|
|
517
|
+
};
|
|
518
|
+
};
|
|
519
|
+
var getExitCodeForError = (error) => {
|
|
520
|
+
if (isAppductCliError(error)) {
|
|
521
|
+
return EXIT_CODE_BY_ERROR_TYPE[error.type];
|
|
522
|
+
}
|
|
523
|
+
if (error instanceof DaemonVersionMismatchError) {
|
|
524
|
+
return EXIT_CODE_BY_ERROR_TYPE.connection_error;
|
|
525
|
+
}
|
|
526
|
+
if (error instanceof DaemonRpcError) {
|
|
527
|
+
const cliErrorClass = rpcErrorClass(error);
|
|
528
|
+
return EXIT_CODE_BY_ERROR_TYPE[cliErrorClass ?? "internal_error"];
|
|
529
|
+
}
|
|
530
|
+
if (isDaemonUnreachableError(error)) {
|
|
531
|
+
return EXIT_CODE_BY_ERROR_TYPE.connection_error;
|
|
532
|
+
}
|
|
533
|
+
return EXIT_CODE_BY_ERROR_TYPE.internal_error;
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
// src/output.ts
|
|
537
|
+
import pc from "picocolors";
|
|
538
|
+
import {
|
|
539
|
+
formatAgentWebSocketUrl,
|
|
540
|
+
renderToolSignature,
|
|
541
|
+
summarizeToolDescription
|
|
542
|
+
} from "@appduct/shared";
|
|
543
|
+
|
|
544
|
+
// src/qr-terminal.ts
|
|
545
|
+
import { toQR } from "toqr";
|
|
546
|
+
var HALF_UPPER = "\u2580";
|
|
547
|
+
var HALF_LOWER = "\u2584";
|
|
548
|
+
var FULL = "\u2588";
|
|
549
|
+
var renderQrToTerminal = (content, options) => {
|
|
550
|
+
const margin = options?.margin ?? 0;
|
|
551
|
+
const ecLevel = options?.ecLevel ?? 1;
|
|
552
|
+
const modules = toQR(content, ecLevel);
|
|
553
|
+
const side = Math.sqrt(modules.length);
|
|
554
|
+
if (!Number.isInteger(side)) {
|
|
555
|
+
throw new Error("Invalid QR matrix size");
|
|
556
|
+
}
|
|
557
|
+
const dim = side + 2 * margin;
|
|
558
|
+
const isDark = (row, col) => {
|
|
559
|
+
if (row < margin || col < margin || row >= margin + side || col >= margin + side) {
|
|
560
|
+
return false;
|
|
561
|
+
}
|
|
562
|
+
const mr = row - margin;
|
|
563
|
+
const mc = col - margin;
|
|
564
|
+
return modules[mr * side + mc] === 1;
|
|
565
|
+
};
|
|
566
|
+
const lines = [];
|
|
567
|
+
for (let row = 0; row < dim; row += 2) {
|
|
568
|
+
let line = "";
|
|
569
|
+
for (let col = 0; col < dim; col++) {
|
|
570
|
+
const top = isDark(row, col);
|
|
571
|
+
const bottom = row + 1 < dim ? isDark(row + 1, col) : false;
|
|
572
|
+
line += top ? bottom ? FULL : HALF_UPPER : bottom ? HALF_LOWER : " ";
|
|
573
|
+
}
|
|
574
|
+
lines.push(line);
|
|
575
|
+
}
|
|
576
|
+
return lines.join("\n");
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
// src/output.ts
|
|
580
|
+
var renderMetaLines = (meta) => {
|
|
581
|
+
if (!meta) {
|
|
582
|
+
return [];
|
|
583
|
+
}
|
|
584
|
+
return [
|
|
585
|
+
"Meta",
|
|
586
|
+
` Command: ${meta.command}`,
|
|
587
|
+
` Timestamp: ${meta.timestamp}`,
|
|
588
|
+
...typeof meta.duration_ms === "number" ? [` Duration: ${meta.duration_ms} ms`] : []
|
|
589
|
+
];
|
|
590
|
+
};
|
|
591
|
+
var formatScalar = (value, flags) => {
|
|
592
|
+
if (typeof value === "string") {
|
|
593
|
+
return value;
|
|
594
|
+
}
|
|
595
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
596
|
+
return String(value);
|
|
597
|
+
}
|
|
598
|
+
if (value === null || value === void 0) {
|
|
599
|
+
return "null";
|
|
600
|
+
}
|
|
601
|
+
return formatJson(value, flags);
|
|
602
|
+
};
|
|
603
|
+
var renderFields = (title, fields, flags) => {
|
|
604
|
+
const visibleFields = fields.filter(([, value]) => value !== void 0);
|
|
605
|
+
if (visibleFields.length === 0) {
|
|
606
|
+
return [];
|
|
607
|
+
}
|
|
608
|
+
const width = Math.max(...visibleFields.map(([label]) => label.length));
|
|
609
|
+
return [
|
|
610
|
+
title,
|
|
611
|
+
...visibleFields.map(([label, value]) => ` ${label.padEnd(width)} ${formatScalar(value, flags)}`)
|
|
612
|
+
];
|
|
613
|
+
};
|
|
614
|
+
var formatAge = (fromIso, toIso) => {
|
|
615
|
+
const elapsedMs = Math.max(0, new Date(toIso).getTime() - new Date(fromIso).getTime());
|
|
616
|
+
const elapsedSeconds = Math.floor(elapsedMs / 1e3);
|
|
617
|
+
if (elapsedSeconds < 60) {
|
|
618
|
+
return `${elapsedSeconds}s`;
|
|
619
|
+
}
|
|
620
|
+
const elapsedMinutes = Math.floor(elapsedSeconds / 60);
|
|
621
|
+
if (elapsedMinutes < 60) {
|
|
622
|
+
return `${elapsedMinutes}m`;
|
|
623
|
+
}
|
|
624
|
+
const elapsedHours = Math.floor(elapsedMinutes / 60);
|
|
625
|
+
if (elapsedHours < 24) {
|
|
626
|
+
return `${elapsedHours}h`;
|
|
627
|
+
}
|
|
628
|
+
return `${Math.floor(elapsedHours / 24)}d`;
|
|
629
|
+
};
|
|
630
|
+
var formatDevice = (device) => {
|
|
631
|
+
const parts = [device.manufacturer, device.model].filter((part) => Boolean(part));
|
|
632
|
+
const label = parts.length > 0 ? parts.join(" ") : "unknown device";
|
|
633
|
+
return device.os ? `${label} (${device.os})` : label;
|
|
634
|
+
};
|
|
635
|
+
var renderLsData = (colors, data, now) => {
|
|
636
|
+
if (data.length === 0) {
|
|
637
|
+
return [colors.dim("Sessions"), " No Appduct sessions are registered."];
|
|
638
|
+
}
|
|
639
|
+
const nowIso = now.toISOString();
|
|
640
|
+
const headers = ["Alias", "State", "Device", "Tools", "Age"];
|
|
641
|
+
const rows = data.map((session) => [
|
|
642
|
+
session.alias,
|
|
643
|
+
session.state,
|
|
644
|
+
formatDevice(session.device),
|
|
645
|
+
String(session.toolCount),
|
|
646
|
+
formatAge(session.claimedAt ?? session.createdAt, nowIso)
|
|
647
|
+
]);
|
|
648
|
+
const widths = headers.map(
|
|
649
|
+
(header, index) => Math.max(header.length, ...rows.map((row) => row[index].length))
|
|
650
|
+
);
|
|
651
|
+
return [
|
|
652
|
+
colors.green("Sessions"),
|
|
653
|
+
` ${headers.map((header, index) => header.padEnd(widths[index])).join(" ")}`,
|
|
654
|
+
...rows.map((row) => ` ${row.map((cell, index) => cell.padEnd(widths[index])).join(" ")}`)
|
|
655
|
+
];
|
|
656
|
+
};
|
|
657
|
+
var isToolsListing = (data) => {
|
|
658
|
+
return typeof data === "object" && data !== null && Array.isArray(data.tools);
|
|
659
|
+
};
|
|
660
|
+
var isToolGroupsListing = (data) => {
|
|
661
|
+
return typeof data === "object" && data !== null && Array.isArray(data.groups) && !Array.isArray(data.tools);
|
|
662
|
+
};
|
|
663
|
+
var UNGROUPED_LABEL = "(ungrouped)";
|
|
664
|
+
var MAX_FOOTER_GROUPS = 10;
|
|
665
|
+
var hasAnyGroup = (groups) => {
|
|
666
|
+
return (groups ?? []).some((entry) => entry.group !== null);
|
|
667
|
+
};
|
|
668
|
+
var topLevelGroups = (groups) => {
|
|
669
|
+
return (groups ?? []).filter((entry) => entry.group !== null && !entry.group.includes("/"));
|
|
670
|
+
};
|
|
671
|
+
var renderEmptyToolsLine = (data) => {
|
|
672
|
+
if (data.total > 0) {
|
|
673
|
+
return ` No tools at offset ${data.offset ?? 0}; ${data.total} matching tool${data.total === 1 ? "" : "s"} in total.`;
|
|
674
|
+
}
|
|
675
|
+
if (data.group !== void 0) {
|
|
676
|
+
const match = data.filter === void 0 ? "" : ` match ${JSON.stringify(data.filter)}`;
|
|
677
|
+
return ` No tools in group ${JSON.stringify(data.group)}${match}. Run \`appduct tools --groups\` to see the session's groups.`;
|
|
678
|
+
}
|
|
679
|
+
return data.filter === void 0 ? " No tools registered." : ` No tools match ${JSON.stringify(data.filter)}.`;
|
|
680
|
+
};
|
|
681
|
+
var renderGroupHint = (data) => {
|
|
682
|
+
if (data.group !== void 0 || !hasAnyGroup(data.groups)) {
|
|
683
|
+
return void 0;
|
|
684
|
+
}
|
|
685
|
+
const top = topLevelGroups(data.groups);
|
|
686
|
+
const named = top.slice(0, MAX_FOOTER_GROUPS).map((entry) => `${entry.group} ${entry.total}`);
|
|
687
|
+
const more = top.length > MAX_FOOTER_GROUPS ? `, ... ${top.length - MAX_FOOTER_GROUPS} more; see --groups` : "";
|
|
688
|
+
return `--group <name> (groups: ${named.join(", ")}${more})`;
|
|
689
|
+
};
|
|
690
|
+
var renderTruncationLine = (data) => {
|
|
691
|
+
if (data.tools.length >= data.total) {
|
|
692
|
+
return [];
|
|
693
|
+
}
|
|
694
|
+
const groupHint = renderGroupHint(data);
|
|
695
|
+
const narrow = groupHint === void 0 ? "--filter <text> or" : `${groupHint} or --filter <text>, or`;
|
|
696
|
+
return [
|
|
697
|
+
"",
|
|
698
|
+
`Showing ${data.tools.length} of ${data.total} tools (offset ${data.offset ?? 0}). Narrow with ${narrow} page with --offset <n>.`
|
|
699
|
+
];
|
|
700
|
+
};
|
|
701
|
+
var renderToolSummaryLines = (tool, indent) => {
|
|
702
|
+
const tag = tool.policy === "allow" ? "" : ` [${tool.policy}]`;
|
|
703
|
+
return [`${indent}${renderToolSignature(tool)}${tag}`, `${indent} ${summarizeToolDescription(tool.description)}`];
|
|
704
|
+
};
|
|
705
|
+
var renderGroupedToolLines = (colors, tools) => {
|
|
706
|
+
const byGroup = /* @__PURE__ */ new Map();
|
|
707
|
+
for (const tool of tools) {
|
|
708
|
+
const key = tool.group ?? null;
|
|
709
|
+
const bucket = byGroup.get(key);
|
|
710
|
+
if (bucket) {
|
|
711
|
+
bucket.push(tool);
|
|
712
|
+
} else {
|
|
713
|
+
byGroup.set(key, [tool]);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
const tops = /* @__PURE__ */ new Map();
|
|
717
|
+
for (const key of byGroup.keys()) {
|
|
718
|
+
if (key === null) {
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
const slash = key.indexOf("/");
|
|
722
|
+
const top = slash === -1 ? key : key.slice(0, slash);
|
|
723
|
+
const subs = tops.get(top) ?? [];
|
|
724
|
+
if (slash !== -1) {
|
|
725
|
+
subs.push(key);
|
|
726
|
+
}
|
|
727
|
+
tops.set(top, subs);
|
|
728
|
+
}
|
|
729
|
+
const byCodePoint = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
730
|
+
const lines = [];
|
|
731
|
+
for (const top of [...tops.keys()].sort(byCodePoint)) {
|
|
732
|
+
lines.push(` ${colors.cyan(top)}`);
|
|
733
|
+
lines.push(...(byGroup.get(top) ?? []).flatMap((tool) => renderToolSummaryLines(tool, " ")));
|
|
734
|
+
for (const sub of tops.get(top).sort(byCodePoint)) {
|
|
735
|
+
lines.push(` ${colors.cyan(sub)}`);
|
|
736
|
+
lines.push(...byGroup.get(sub).flatMap((tool) => renderToolSummaryLines(tool, " ")));
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
const ungrouped = byGroup.get(null);
|
|
740
|
+
if (ungrouped) {
|
|
741
|
+
lines.push(` ${colors.cyan(UNGROUPED_LABEL)}`);
|
|
742
|
+
lines.push(...ungrouped.flatMap((tool) => renderToolSummaryLines(tool, " ")));
|
|
743
|
+
}
|
|
744
|
+
return lines;
|
|
745
|
+
};
|
|
746
|
+
var renderToolsTitle = (colors, data) => {
|
|
747
|
+
return colors.green(data.group === void 0 ? "Tools" : `Tools in group ${data.group}`);
|
|
748
|
+
};
|
|
749
|
+
var renderToolSummaryTable = (colors, data) => {
|
|
750
|
+
if (data.tools.length === 0) {
|
|
751
|
+
return [renderToolsTitle(colors, data), renderEmptyToolsLine(data)];
|
|
752
|
+
}
|
|
753
|
+
const grouped = data.group === void 0 && hasAnyGroup(data.groups);
|
|
754
|
+
return [
|
|
755
|
+
renderToolsTitle(colors, data),
|
|
756
|
+
...grouped ? renderGroupedToolLines(colors, data.tools) : data.tools.flatMap((tool) => renderToolSummaryLines(tool, " ")),
|
|
757
|
+
...renderTruncationLine(data),
|
|
758
|
+
"",
|
|
759
|
+
"Run `appduct tools <name>` for a tool's full schema."
|
|
760
|
+
];
|
|
761
|
+
};
|
|
762
|
+
var renderToolDetail = (colors, tool, flags) => {
|
|
763
|
+
return renderFields(
|
|
764
|
+
colors.green(`Tool: ${tool.name}`),
|
|
765
|
+
[
|
|
766
|
+
["Signature", renderToolSignature(tool)],
|
|
767
|
+
["Description", tool.description],
|
|
768
|
+
["Input schema", tool.input_schema],
|
|
769
|
+
["Output schema", tool.output_schema],
|
|
770
|
+
// `?? undefined` keeps an ungrouped tool's row hidden rather than printing a bare "null":
|
|
771
|
+
// `renderFields` drops undefined rows, and the machine form's `null` is for consumers, not
|
|
772
|
+
// for a human scanning one tool's schema.
|
|
773
|
+
["Group", tool.group ?? void 0],
|
|
774
|
+
["Annotations", tool.annotations],
|
|
775
|
+
// Only rendered for a tool that declares one; `renderFields` drops undefined rows, so a
|
|
776
|
+
// tool on the daemon's default deadline shows no line at all rather than a misleading
|
|
777
|
+
// "10000".
|
|
778
|
+
["Timeout (ms)", tool.timeout_ms]
|
|
779
|
+
],
|
|
780
|
+
flags
|
|
781
|
+
);
|
|
782
|
+
};
|
|
783
|
+
var renderToolsFullListing = (colors, data, flags) => {
|
|
784
|
+
if (data.tools.length === 0) {
|
|
785
|
+
return [renderToolsTitle(colors, data), renderEmptyToolsLine(data)];
|
|
786
|
+
}
|
|
787
|
+
return [
|
|
788
|
+
...data.tools.flatMap((tool, index) => [
|
|
789
|
+
...index > 0 ? [""] : [],
|
|
790
|
+
...renderToolDetail(colors, tool, flags)
|
|
791
|
+
]),
|
|
792
|
+
...renderTruncationLine(data)
|
|
793
|
+
];
|
|
794
|
+
};
|
|
795
|
+
var renderToolGroups = (colors, data) => {
|
|
796
|
+
if (data.groups.length === 0) {
|
|
797
|
+
return [colors.green("Groups"), " No tools registered."];
|
|
798
|
+
}
|
|
799
|
+
const label = (entry) => {
|
|
800
|
+
if (entry.group === null) {
|
|
801
|
+
return UNGROUPED_LABEL;
|
|
802
|
+
}
|
|
803
|
+
return entry.group.includes("/") ? ` ${entry.group}` : entry.group;
|
|
804
|
+
};
|
|
805
|
+
const width = Math.max(...data.groups.map((entry) => label(entry).length));
|
|
806
|
+
const countWidth = Math.max(...data.groups.map((entry) => String(entry.total).length));
|
|
807
|
+
return [
|
|
808
|
+
colors.green("Groups"),
|
|
809
|
+
...data.groups.map((entry) => ` ${label(entry).padEnd(width)} ${String(entry.total).padStart(countWidth)}`),
|
|
810
|
+
"",
|
|
811
|
+
`${data.total} tool${data.total === 1 ? "" : "s"} in total. ` + (hasAnyGroup(data.groups) ? "Run `appduct tools --group <name>` to list one group's tools." : "No tool declares a group.")
|
|
812
|
+
];
|
|
813
|
+
};
|
|
814
|
+
var renderToolsData = (colors, data, flags, full) => {
|
|
815
|
+
if (isToolGroupsListing(data)) {
|
|
816
|
+
return renderToolGroups(colors, data);
|
|
817
|
+
}
|
|
818
|
+
if (!isToolsListing(data)) {
|
|
819
|
+
return renderToolDetail(colors, data, flags);
|
|
820
|
+
}
|
|
821
|
+
return full ? renderToolsFullListing(colors, data, flags) : renderToolSummaryTable(colors, data);
|
|
822
|
+
};
|
|
823
|
+
var renderInvokeData = (colors, data, flags) => {
|
|
824
|
+
return [colors.green("Result"), formatScalar(data, flags)];
|
|
825
|
+
};
|
|
826
|
+
var renderLinkData = (colors, data, flags, qr) => {
|
|
827
|
+
const lines = [
|
|
828
|
+
colors.green("Link Created"),
|
|
829
|
+
...renderFields(
|
|
830
|
+
"Link",
|
|
831
|
+
[
|
|
832
|
+
["Session", data.sessionId],
|
|
833
|
+
["Deep link", data.deepLink],
|
|
834
|
+
["Endpoint", formatAgentWebSocketUrl(data.endpoint)],
|
|
835
|
+
["Pin", data.pin],
|
|
836
|
+
["Expires", new Date(data.expiresAt * 1e3).toISOString()],
|
|
837
|
+
["Delivered", data.delivered ? `yes (${data.target})` : void 0]
|
|
838
|
+
],
|
|
839
|
+
flags
|
|
840
|
+
)
|
|
841
|
+
];
|
|
842
|
+
if (qr) {
|
|
843
|
+
lines.push("", "QR", renderQrToTerminal(data.deepLink));
|
|
844
|
+
}
|
|
845
|
+
return lines;
|
|
846
|
+
};
|
|
847
|
+
var renderRevokeData = (colors, _data) => {
|
|
848
|
+
return [colors.green("Session Revoked")];
|
|
849
|
+
};
|
|
850
|
+
var renderKeygenData = (colors, data, flags) => {
|
|
851
|
+
return [
|
|
852
|
+
colors.green("Key Ready"),
|
|
853
|
+
...renderFields(
|
|
854
|
+
"Key",
|
|
855
|
+
[
|
|
856
|
+
["Path", data.path],
|
|
857
|
+
["Fingerprint", data.pin]
|
|
858
|
+
],
|
|
859
|
+
flags
|
|
860
|
+
)
|
|
861
|
+
];
|
|
862
|
+
};
|
|
863
|
+
var renderInitData = (colors, data, flags) => {
|
|
864
|
+
const snippet = JSON.stringify(
|
|
865
|
+
{ mcpServers: { appduct: data.mcpServerEntry } },
|
|
866
|
+
null,
|
|
867
|
+
2
|
|
868
|
+
);
|
|
869
|
+
return [
|
|
870
|
+
colors.green(data.changed ? "Project Initialized" : "Project Already Initialized"),
|
|
871
|
+
...renderFields(
|
|
872
|
+
"Config",
|
|
873
|
+
[
|
|
874
|
+
["Path", data.path],
|
|
875
|
+
["Scheme", data.scheme],
|
|
876
|
+
["Source", data.source],
|
|
877
|
+
// Only present for a scheme discovery read off a static project file (app.json, or one of
|
|
878
|
+
// the native Android/iOS probes) — names the exact file/key, not just which platform.
|
|
879
|
+
["Read from", data.origin],
|
|
880
|
+
["Written", data.changed ? data.created ? "created" : "updated" : "unchanged"]
|
|
881
|
+
],
|
|
882
|
+
flags
|
|
883
|
+
),
|
|
884
|
+
...data.note === void 0 ? [] : ["", colors.yellow(`Note: ${data.note}`)],
|
|
885
|
+
"",
|
|
886
|
+
"MCP server entry",
|
|
887
|
+
...snippet.split("\n").map((line) => ` ${line}`),
|
|
888
|
+
"",
|
|
889
|
+
"Next",
|
|
890
|
+
...data.nextSteps.map((step, index) => ` ${index + 1}. ${step}`)
|
|
891
|
+
];
|
|
892
|
+
};
|
|
893
|
+
var renderDaemonRunData = (colors, data, flags) => {
|
|
894
|
+
return [
|
|
895
|
+
colors.green("Daemon Running"),
|
|
896
|
+
...renderFields(
|
|
897
|
+
"Daemon",
|
|
898
|
+
[
|
|
899
|
+
["PID", data.daemon.pid],
|
|
900
|
+
["State dir", data.daemon.state_dir],
|
|
901
|
+
["Socket", data.daemon.socket_path]
|
|
902
|
+
],
|
|
903
|
+
flags
|
|
904
|
+
)
|
|
905
|
+
];
|
|
906
|
+
};
|
|
907
|
+
var renderDaemonStartData = (colors, data, flags) => {
|
|
908
|
+
return [
|
|
909
|
+
colors.green("Daemon Started"),
|
|
910
|
+
...renderFields(
|
|
911
|
+
"Daemon",
|
|
912
|
+
[
|
|
913
|
+
["PID", data.daemon.pid],
|
|
914
|
+
["WSS port", data.daemon.wss_port],
|
|
915
|
+
["Started at", data.daemon.started_at]
|
|
916
|
+
],
|
|
917
|
+
flags
|
|
918
|
+
)
|
|
919
|
+
];
|
|
920
|
+
};
|
|
921
|
+
var renderDaemonStopData = (colors, data, flags) => {
|
|
922
|
+
return [
|
|
923
|
+
colors.green("Daemon Stopped"),
|
|
924
|
+
...renderFields("Daemon", [["Method", data.daemon.method]], flags)
|
|
925
|
+
];
|
|
926
|
+
};
|
|
927
|
+
var formatByteSize = (bytes) => {
|
|
928
|
+
const units = ["B", "KiB", "MiB", "GiB", "TiB"];
|
|
929
|
+
let value = bytes;
|
|
930
|
+
let unit = 0;
|
|
931
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
932
|
+
value /= 1024;
|
|
933
|
+
unit += 1;
|
|
934
|
+
}
|
|
935
|
+
return `${unit === 0 ? value : value.toFixed(1)} ${units[unit]}`;
|
|
936
|
+
};
|
|
937
|
+
var renderDaemonStatusData = (colors, data, flags) => {
|
|
938
|
+
return [
|
|
939
|
+
colors.green("Daemon Status"),
|
|
940
|
+
...renderFields(
|
|
941
|
+
"Daemon",
|
|
942
|
+
[
|
|
943
|
+
["Version", data.daemon.version],
|
|
944
|
+
["PID", data.daemon.pid],
|
|
945
|
+
["Started at", data.daemon.started_at],
|
|
946
|
+
["WSS port", data.daemon.wss_port],
|
|
947
|
+
["Pinned keys", data.daemon.pinned_keys.length],
|
|
948
|
+
["Sessions", data.daemon.session_count]
|
|
949
|
+
],
|
|
950
|
+
flags
|
|
951
|
+
),
|
|
952
|
+
"",
|
|
953
|
+
...renderFields(
|
|
954
|
+
"Policy",
|
|
955
|
+
[
|
|
956
|
+
["Default", data.policy.default],
|
|
957
|
+
["Destructive", data.policy.destructive],
|
|
958
|
+
["Overrides", data.policy.tools ? Object.keys(data.policy.tools).length : 0]
|
|
959
|
+
],
|
|
960
|
+
flags
|
|
961
|
+
),
|
|
962
|
+
"",
|
|
963
|
+
// The retention rows drop out entirely against a daemon that predates them (`renderFields`
|
|
964
|
+
// skips `undefined`), rather than printing "undefined" or an invented zero.
|
|
965
|
+
...renderFields(
|
|
966
|
+
"Audit",
|
|
967
|
+
[
|
|
968
|
+
["Path", data.audit.path],
|
|
969
|
+
["Retention", data.audit.retention_days === void 0 ? void 0 : `${data.audit.retention_days} days`],
|
|
970
|
+
["Files", data.audit.files],
|
|
971
|
+
["Size", data.audit.bytes === void 0 ? void 0 : formatByteSize(data.audit.bytes)],
|
|
972
|
+
["Failed writes", data.audit.failed_writes],
|
|
973
|
+
["Failed prunes", data.audit.failed_prunes]
|
|
974
|
+
],
|
|
975
|
+
flags
|
|
976
|
+
),
|
|
977
|
+
// Version drift (issue #30) is the one thing here an operator has to act on, so it goes last
|
|
978
|
+
// — the line still on screen after the block scrolls — and in yellow, not a quiet field row.
|
|
979
|
+
...data.warning ? ["", colors.yellow(`Warning: ${data.warning}`)] : []
|
|
980
|
+
];
|
|
981
|
+
};
|
|
982
|
+
var renderDoctorData = (colors, data, flags) => {
|
|
983
|
+
return [
|
|
984
|
+
data.present ? colors.green("Appduct Present") : colors.yellow("Appduct Absent"),
|
|
985
|
+
...renderFields(
|
|
986
|
+
"Artifact",
|
|
987
|
+
[
|
|
988
|
+
["Path", data.artifact],
|
|
989
|
+
["Platform", data.platform],
|
|
990
|
+
["Format", data.format],
|
|
991
|
+
["Present", data.present],
|
|
992
|
+
["Signals", data.signals.length > 0 ? data.signals.join(", ") : "none"],
|
|
993
|
+
["Assertion", data.assertion ? `${data.assertion.expected} (holds)` : void 0]
|
|
994
|
+
],
|
|
995
|
+
flags
|
|
996
|
+
)
|
|
997
|
+
];
|
|
998
|
+
};
|
|
999
|
+
var renderSuccessData = (colors, command, data, options) => {
|
|
1000
|
+
const flags = options.flags;
|
|
1001
|
+
switch (command) {
|
|
1002
|
+
case "init":
|
|
1003
|
+
return renderInitData(colors, data, flags);
|
|
1004
|
+
case "keygen":
|
|
1005
|
+
return renderKeygenData(colors, data, flags);
|
|
1006
|
+
case "link":
|
|
1007
|
+
return renderLinkData(colors, data, flags, options.qr);
|
|
1008
|
+
case "ls":
|
|
1009
|
+
return renderLsData(colors, data, options.now ?? /* @__PURE__ */ new Date());
|
|
1010
|
+
case "tools":
|
|
1011
|
+
return renderToolsData(colors, data, flags, options.full);
|
|
1012
|
+
case "invoke":
|
|
1013
|
+
return renderInvokeData(colors, data, flags);
|
|
1014
|
+
case "revoke":
|
|
1015
|
+
return renderRevokeData(colors, data);
|
|
1016
|
+
case "daemon run":
|
|
1017
|
+
return renderDaemonRunData(colors, data, flags);
|
|
1018
|
+
case "daemon start":
|
|
1019
|
+
return renderDaemonStartData(colors, data, flags);
|
|
1020
|
+
case "daemon stop":
|
|
1021
|
+
return renderDaemonStopData(colors, data, flags);
|
|
1022
|
+
case "daemon status":
|
|
1023
|
+
return renderDaemonStatusData(colors, data, flags);
|
|
1024
|
+
case "doctor":
|
|
1025
|
+
return renderDoctorData(colors, data, flags);
|
|
1026
|
+
default:
|
|
1027
|
+
return [colors.green("Command Complete"), formatJson(data, flags)];
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
var renderHumanError = (colors, error, flags, meta) => {
|
|
1031
|
+
return [
|
|
1032
|
+
colors.red("Command Failed"),
|
|
1033
|
+
...renderFields(
|
|
1034
|
+
"Error",
|
|
1035
|
+
[
|
|
1036
|
+
["Type", error.type],
|
|
1037
|
+
["Message", error.message],
|
|
1038
|
+
["Details", error.details]
|
|
1039
|
+
],
|
|
1040
|
+
flags
|
|
1041
|
+
),
|
|
1042
|
+
...meta ? ["", ...renderMetaLines(meta)] : []
|
|
1043
|
+
].join("\n");
|
|
1044
|
+
};
|
|
1045
|
+
var renderResult = (result, options) => {
|
|
1046
|
+
if (options.flags.json) {
|
|
1047
|
+
return {
|
|
1048
|
+
stdout: `${formatJson(result, options.flags)}
|
|
1049
|
+
`
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
const colors = pc.createColors(options.flags.color);
|
|
1053
|
+
if (!result.ok) {
|
|
1054
|
+
return {
|
|
1055
|
+
stderr: `${renderHumanError(colors, result.error, options.flags, result.meta)}
|
|
1056
|
+
`
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
const lines = [
|
|
1060
|
+
...renderSuccessData(colors, options.command, result.data, options),
|
|
1061
|
+
"",
|
|
1062
|
+
...renderMetaLines(result.meta)
|
|
1063
|
+
].filter((line, index, collection) => {
|
|
1064
|
+
if (line !== "") {
|
|
1065
|
+
return true;
|
|
1066
|
+
}
|
|
1067
|
+
return index > 0 && collection[index - 1] !== "" && index < collection.length - 1;
|
|
1068
|
+
});
|
|
1069
|
+
return {
|
|
1070
|
+
stdout: `${lines.join("\n")}
|
|
1071
|
+
`
|
|
1072
|
+
};
|
|
1073
|
+
};
|
|
1074
|
+
var renderEventLine = (event, flags) => {
|
|
1075
|
+
if (flags.json) {
|
|
1076
|
+
return JSON.stringify(event);
|
|
1077
|
+
}
|
|
1078
|
+
const colors = pc.createColors(flags.color);
|
|
1079
|
+
const timestamp = new Date(event.ts).toISOString();
|
|
1080
|
+
const target = event.alias ?? event.sessionId;
|
|
1081
|
+
const dataSuffix = event.data === void 0 ? "" : ` ${formatJson(event.data, flags)}`;
|
|
1082
|
+
return `${colors.dim(timestamp)} ${colors.green(event.kind)}${target ? ` ${target}` : ""}${dataSuffix}`;
|
|
1083
|
+
};
|
|
1084
|
+
var renderEventsCursorLine = (cursor, flags) => {
|
|
1085
|
+
if (flags.json) {
|
|
1086
|
+
return JSON.stringify({ cursor });
|
|
1087
|
+
}
|
|
1088
|
+
const colors = pc.createColors(flags.color);
|
|
1089
|
+
return colors.dim(`cursor: ${cursor} (pass --since ${cursor} to resume from here)`);
|
|
1090
|
+
};
|
|
1091
|
+
|
|
1092
|
+
// src/cli/envelope.ts
|
|
1093
|
+
var createCommandMeta = (command, startedAt, finishedAt) => {
|
|
1094
|
+
return {
|
|
1095
|
+
command,
|
|
1096
|
+
timestamp: finishedAt.toISOString(),
|
|
1097
|
+
duration_ms: finishedAt.getTime() - startedAt.getTime()
|
|
1098
|
+
};
|
|
1099
|
+
};
|
|
1100
|
+
var finalizeResult = (result, timing, flags) => {
|
|
1101
|
+
if (!flags.verbose) {
|
|
1102
|
+
return result;
|
|
1103
|
+
}
|
|
1104
|
+
return {
|
|
1105
|
+
...result,
|
|
1106
|
+
meta: createCommandMeta(timing.command, timing.startedAt, timing.finishedAt)
|
|
1107
|
+
};
|
|
1108
|
+
};
|
|
1109
|
+
|
|
1110
|
+
// src/cli/runner.ts
|
|
1111
|
+
var writeRenderedOutput = (rendered, writers) => {
|
|
1112
|
+
if (rendered.stdout) {
|
|
1113
|
+
writers.stdout.write(rendered.stdout);
|
|
1114
|
+
}
|
|
1115
|
+
if (rendered.stderr) {
|
|
1116
|
+
writers.stderr.write(rendered.stderr);
|
|
1117
|
+
}
|
|
1118
|
+
};
|
|
1119
|
+
var executeCommand = async (command, handler, env, render = {}) => {
|
|
1120
|
+
const startedAt = env.clock.now();
|
|
1121
|
+
try {
|
|
1122
|
+
const result = await handler();
|
|
1123
|
+
const finishedAt = env.clock.now();
|
|
1124
|
+
const finalized = finalizeResult(result, { command, startedAt, finishedAt }, env.flags);
|
|
1125
|
+
const renderOptions = {
|
|
1126
|
+
command,
|
|
1127
|
+
flags: env.flags,
|
|
1128
|
+
now: finishedAt,
|
|
1129
|
+
qr: render.qr,
|
|
1130
|
+
full: render.full
|
|
1131
|
+
};
|
|
1132
|
+
writeRenderedOutput(renderResult(finalized, renderOptions), env);
|
|
1133
|
+
return 0;
|
|
1134
|
+
} catch (error) {
|
|
1135
|
+
const finishedAt = env.clock.now();
|
|
1136
|
+
const cliError = toCliError(error);
|
|
1137
|
+
const result = { ok: false, error: cliError };
|
|
1138
|
+
const finalized = finalizeResult(result, { command, startedAt, finishedAt }, env.flags);
|
|
1139
|
+
const renderOptions = {
|
|
1140
|
+
command,
|
|
1141
|
+
flags: env.flags,
|
|
1142
|
+
now: finishedAt,
|
|
1143
|
+
qr: render.qr,
|
|
1144
|
+
full: render.full
|
|
1145
|
+
};
|
|
1146
|
+
writeRenderedOutput(renderResult(finalized, renderOptions), env);
|
|
1147
|
+
return getExitCodeForError(error);
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
|
|
1151
|
+
// src/cli/router.ts
|
|
1152
|
+
var commandName = (context) => {
|
|
1153
|
+
return context.path.join(" ");
|
|
1154
|
+
};
|
|
1155
|
+
var createRouter = (table, options) => {
|
|
1156
|
+
return async (context) => {
|
|
1157
|
+
const [word, ...rest] = context.args;
|
|
1158
|
+
const load = word !== void 0 && Object.hasOwn(table, word) ? table[word] : void 0;
|
|
1159
|
+
if (load === void 0) {
|
|
1160
|
+
return executeCommand(
|
|
1161
|
+
commandName(context),
|
|
1162
|
+
() => {
|
|
1163
|
+
throw options.unknown(word, context);
|
|
1164
|
+
},
|
|
1165
|
+
context.env
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
const { route } = await load();
|
|
1169
|
+
return route({ ...context, path: [...context.path, word], args: rest });
|
|
1170
|
+
};
|
|
1171
|
+
};
|
|
1172
|
+
var unknownCommandError = (word) => {
|
|
1173
|
+
return usageError(word === void 0 ? "A command is required." : `Unknown command "${word}".`);
|
|
1174
|
+
};
|
|
1175
|
+
|
|
1176
|
+
// src/cli/types.ts
|
|
1177
|
+
var systemClock = {
|
|
1178
|
+
now: () => /* @__PURE__ */ new Date()
|
|
1179
|
+
};
|
|
1180
|
+
|
|
1181
|
+
// src/cli/dispatch.ts
|
|
1182
|
+
var DAEMON_RESTART_ENV = "APPDUCT_DAEMON_RESTART";
|
|
1183
|
+
var isEnvTruthy = (value) => {
|
|
1184
|
+
return value === "1" || value?.toLowerCase() === "true";
|
|
1185
|
+
};
|
|
1186
|
+
var rootRouter = createRouter(
|
|
1187
|
+
{
|
|
1188
|
+
init: () => import("./cli/routes/init.js"),
|
|
1189
|
+
keygen: () => import("./cli/routes/keygen.js"),
|
|
1190
|
+
link: () => import("./cli/routes/link.js"),
|
|
1191
|
+
ls: () => import("./cli/routes/ls.js"),
|
|
1192
|
+
tools: () => import("./cli/routes/tools.js"),
|
|
1193
|
+
invoke: () => import("./cli/routes/invoke.js"),
|
|
1194
|
+
revoke: () => import("./cli/routes/revoke.js"),
|
|
1195
|
+
events: () => import("./cli/routes/events.js"),
|
|
1196
|
+
mcp: () => import("./cli/routes/mcp.js"),
|
|
1197
|
+
doctor: () => import("./cli/routes/doctor.js"),
|
|
1198
|
+
daemon: () => import("./cli/routes/daemon/index.js")
|
|
1199
|
+
},
|
|
1200
|
+
{ unknown: unknownCommandError }
|
|
1201
|
+
);
|
|
1202
|
+
var runCli = async (argv, options = {}) => {
|
|
1203
|
+
const writers = {
|
|
1204
|
+
stdout: options.stdout ?? process.stdout,
|
|
1205
|
+
stderr: options.stderr ?? process.stderr
|
|
1206
|
+
};
|
|
1207
|
+
const clock = options.clock ?? systemClock;
|
|
1208
|
+
const cli = createCli();
|
|
1209
|
+
try {
|
|
1210
|
+
cli.parse(["node", "appduct", ...argv], {
|
|
1211
|
+
run: false
|
|
1212
|
+
});
|
|
1213
|
+
} catch (error) {
|
|
1214
|
+
return executeCommand(
|
|
1215
|
+
"cli",
|
|
1216
|
+
() => {
|
|
1217
|
+
throw error;
|
|
1218
|
+
},
|
|
1219
|
+
{
|
|
1220
|
+
flags: resolveGlobalFlagsFromArgv(argv),
|
|
1221
|
+
stdout: writers.stdout,
|
|
1222
|
+
stderr: writers.stderr,
|
|
1223
|
+
clock
|
|
1224
|
+
}
|
|
1225
|
+
);
|
|
1226
|
+
}
|
|
1227
|
+
const matchedCommand = cli.matchedCommandName;
|
|
1228
|
+
const parsedOptions = cli.options;
|
|
1229
|
+
const parsedArgs = cli.args;
|
|
1230
|
+
if (parsedOptions.help || parsedOptions.version) {
|
|
1231
|
+
return 0;
|
|
1232
|
+
}
|
|
1233
|
+
const flags = resolveGlobalFlags(parsedOptions);
|
|
1234
|
+
if (!matchedCommand) {
|
|
1235
|
+
if (parsedArgs[0]) {
|
|
1236
|
+
return executeCommand(
|
|
1237
|
+
"cli",
|
|
1238
|
+
() => {
|
|
1239
|
+
throw usageError(`Unknown command "${parsedArgs[0]}".`);
|
|
1240
|
+
},
|
|
1241
|
+
{
|
|
1242
|
+
flags,
|
|
1243
|
+
stdout: writers.stdout,
|
|
1244
|
+
stderr: writers.stderr,
|
|
1245
|
+
clock
|
|
1246
|
+
}
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
cli.outputHelp();
|
|
1250
|
+
return 0;
|
|
1251
|
+
}
|
|
1252
|
+
const env = { flags, stdout: writers.stdout, stderr: writers.stderr, clock };
|
|
1253
|
+
const stateDir = resolveStateDir(
|
|
1254
|
+
typeof parsedOptions.stateDir === "string" ? parsedOptions.stateDir : void 0
|
|
1255
|
+
);
|
|
1256
|
+
let forceRestart;
|
|
1257
|
+
const resolveForceRestart = () => {
|
|
1258
|
+
forceRestart ??= (async () => {
|
|
1259
|
+
let configuredForce = false;
|
|
1260
|
+
try {
|
|
1261
|
+
configuredForce = (await loadConfig(getStateDirPaths(stateDir))).restartDaemonOnVersionMismatch;
|
|
1262
|
+
} catch {
|
|
1263
|
+
}
|
|
1264
|
+
const flag = typeof parsedOptions.daemonRestart === "boolean" ? parsedOptions.daemonRestart : void 0;
|
|
1265
|
+
return flag ?? (isEnvTruthy(process.env[DAEMON_RESTART_ENV]) || configuredForce);
|
|
1266
|
+
})();
|
|
1267
|
+
return forceRestart;
|
|
1268
|
+
};
|
|
1269
|
+
const cliWarning = flags.json ? () => {
|
|
1270
|
+
} : (message) => void writers.stderr.write(message);
|
|
1271
|
+
const context = {
|
|
1272
|
+
path: [],
|
|
1273
|
+
// cac has already split the command word off `cli.args`; put it back so the root router
|
|
1274
|
+
// consumes it the same way the nested routers consume theirs.
|
|
1275
|
+
args: [matchedCommand, ...parsedArgs],
|
|
1276
|
+
options: parsedOptions,
|
|
1277
|
+
argv,
|
|
1278
|
+
env,
|
|
1279
|
+
stateDir,
|
|
1280
|
+
versionCheck: {
|
|
1281
|
+
clientVersion: getPackageVersion(),
|
|
1282
|
+
forceRestart: resolveForceRestart,
|
|
1283
|
+
warn: cliWarning
|
|
1284
|
+
}
|
|
1285
|
+
};
|
|
1286
|
+
return rootRouter(context);
|
|
1287
|
+
};
|
|
1288
|
+
export {
|
|
1289
|
+
AppductCliError,
|
|
1290
|
+
assertionError,
|
|
1291
|
+
connectionError,
|
|
1292
|
+
createCli,
|
|
1293
|
+
getExitCodeForError,
|
|
1294
|
+
inspectionError,
|
|
1295
|
+
internalError,
|
|
1296
|
+
isAppductCliError,
|
|
1297
|
+
permissionError,
|
|
1298
|
+
renderEventLine,
|
|
1299
|
+
renderEventsCursorLine,
|
|
1300
|
+
renderResult,
|
|
1301
|
+
runCli,
|
|
1302
|
+
sessionError,
|
|
1303
|
+
toCliError,
|
|
1304
|
+
toolError,
|
|
1305
|
+
usageError,
|
|
1306
|
+
validationError
|
|
1307
|
+
};
|