@tikoci/centrs 0.1.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 +207 -0
- package/GLOSSARY.txt +420 -0
- package/README.md +196 -0
- package/SECURITY.md +30 -0
- package/commands/AGENTS.md +50 -0
- package/commands/btest/README.md +257 -0
- package/commands/btest/examples.md +190 -0
- package/commands/check/README.md +59 -0
- package/commands/devices/README.md +405 -0
- package/commands/devices/examples.md +488 -0
- package/commands/discover/README.md +170 -0
- package/commands/discover/examples.md +98 -0
- package/commands/execute/README.md +208 -0
- package/commands/execute/examples.md +353 -0
- package/commands/mcp/README.md +260 -0
- package/commands/mcp/examples.md +158 -0
- package/commands/retrieve/README.md +209 -0
- package/commands/retrieve/examples.md +306 -0
- package/commands/settings/README.md +69 -0
- package/commands/stream/README.md +70 -0
- package/commands/terminal/README.md +177 -0
- package/commands/terminal/examples.md +144 -0
- package/commands/transfer/README.md +315 -0
- package/commands/transfer/examples.md +368 -0
- package/docs/CONSTITUTION.md +417 -0
- package/docs/MATRIX.md +224 -0
- package/docs/errors/README.md +74 -0
- package/docs/errors/auth/failed.md +9 -0
- package/docs/errors/cdb/already-exists.md +9 -0
- package/docs/errors/cdb/created.md +9 -0
- package/docs/errors/cdb/decrypt-failed.md +9 -0
- package/docs/errors/cdb/empty-group.md +9 -0
- package/docs/errors/cdb/invalid-option.md +9 -0
- package/docs/errors/cdb/invalid-record.md +9 -0
- package/docs/errors/cdb/not-found-target.md +9 -0
- package/docs/errors/cdb/not-found.md +9 -0
- package/docs/errors/cdb/override-applied.md +9 -0
- package/docs/errors/cdb/parse-failed.md +9 -0
- package/docs/errors/cdb/password-not-needed.md +9 -0
- package/docs/errors/cdb/password-required.md +9 -0
- package/docs/errors/cdb/reserved-key.md +9 -0
- package/docs/errors/cdb/reserved-option.md +9 -0
- package/docs/errors/cdb/target-not-registered.md +9 -0
- package/docs/errors/cdb/unknown-field.md +9 -0
- package/docs/errors/cdb/unknown-option.md +9 -0
- package/docs/errors/cdb/write-not-permitted.md +9 -0
- package/docs/errors/discover/broadcast-unavailable.md +9 -0
- package/docs/errors/discover/failed.md +9 -0
- package/docs/errors/identity/ambiguous.md +9 -0
- package/docs/errors/identity/no-match.md +9 -0
- package/docs/errors/input/invalid-command.md +9 -0
- package/docs/errors/input/invalid-match.md +9 -0
- package/docs/errors/input/invalid-path.md +9 -0
- package/docs/errors/input/invalid-routeros-path.md +9 -0
- package/docs/errors/input/invalid-target.md +9 -0
- package/docs/errors/input/local-file-not-found.md +9 -0
- package/docs/errors/input/mac-address.md +9 -0
- package/docs/errors/input/max-results-exceeded.md +9 -0
- package/docs/errors/internal/devices-failed.md +9 -0
- package/docs/errors/internal/mcp-start.md +9 -0
- package/docs/errors/internal/mcp-tool.md +9 -0
- package/docs/errors/internal/native-api-length.md +9 -0
- package/docs/errors/internal/unhandled.md +9 -0
- package/docs/errors/internal/unreachable.md +9 -0
- package/docs/errors/mndp/encode-failed.md +9 -0
- package/docs/errors/mndp/listen-failed.md +9 -0
- package/docs/errors/mndp/malformed.md +9 -0
- package/docs/errors/routeros/api-fatal.md +9 -0
- package/docs/errors/routeros/api-protocol.md +9 -0
- package/docs/errors/routeros/api-trap.md +9 -0
- package/docs/errors/routeros/btest-protocol.md +12 -0
- package/docs/errors/routeros/btest-too-many-sessions.md +9 -0
- package/docs/errors/routeros/command-failed.md +14 -0
- package/docs/errors/routeros/ec-srp5-protocol.md +11 -0
- package/docs/errors/routeros/error.md +9 -0
- package/docs/errors/routeros/invalid-value.md +14 -0
- package/docs/errors/routeros/mac-telnet-error.md +9 -0
- package/docs/errors/routeros/mac-telnet-not-ready.md +9 -0
- package/docs/errors/routeros/mac-telnet-protocol.md +9 -0
- package/docs/errors/routeros/mac-telnet-unsupported-auth.md +9 -0
- package/docs/errors/routeros/protocol-not-implemented.md +9 -0
- package/docs/errors/routeros/request-failed.md +9 -0
- package/docs/errors/routeros/session-closed.md +13 -0
- package/docs/errors/routeros/unknown-attribute.md +13 -0
- package/docs/errors/routeros/unknown-path.md +15 -0
- package/docs/errors/routeros/unsupported-capability.md +9 -0
- package/docs/errors/settings/invalid-boolean.md +9 -0
- package/docs/errors/settings/invalid-format.md +9 -0
- package/docs/errors/settings/invalid-integer.md +9 -0
- package/docs/errors/settings/invalid-timeout.md +9 -0
- package/docs/errors/settings/invalid-via.md +9 -0
- package/docs/errors/settings/unsafe-protocol-blocked.md +9 -0
- package/docs/errors/target/mac-not-in-arp.md +14 -0
- package/docs/errors/target/mac-required.md +23 -0
- package/docs/errors/target/mac-unresolved.md +17 -0
- package/docs/errors/target/unresolved.md +9 -0
- package/docs/errors/transport/auth-failed.md +9 -0
- package/docs/errors/transport/auto-method.md +9 -0
- package/docs/errors/transport/capability-unsupported.md +22 -0
- package/docs/errors/transport/checksum-unavailable.md +9 -0
- package/docs/errors/transport/connection-closed.md +9 -0
- package/docs/errors/transport/connection-refused.md +9 -0
- package/docs/errors/transport/dns.md +9 -0
- package/docs/errors/transport/host-key-mismatch.md +9 -0
- package/docs/errors/transport/incomplete-transfer.md +9 -0
- package/docs/errors/transport/insecure-trust.md +9 -0
- package/docs/errors/transport/local-tool-missing.md +11 -0
- package/docs/errors/transport/network.md +9 -0
- package/docs/errors/transport/timeout.md +9 -0
- package/docs/errors/transport/tls-certificate.md +9 -0
- package/docs/errors/transport/unreachable.md +9 -0
- package/docs/errors/transport/unsupported-operation.md +9 -0
- package/docs/errors/usage/confirmation-required.md +9 -0
- package/docs/errors/usage/conflicting-flags.md +9 -0
- package/docs/errors/usage/invalid-concurrency.md +9 -0
- package/docs/errors/usage/missing-group.md +9 -0
- package/docs/errors/usage/not-implemented.md +9 -0
- package/docs/errors/usage/target-exists.md +9 -0
- package/docs/errors/usage/timeout-out-of-range.md +9 -0
- package/docs/errors/validation/not-implemented.md +9 -0
- package/docs/errors/validation/option.md +9 -0
- package/docs/errors/validation/syntax.md +9 -0
- package/docs/errors/validation/unknown-attribute.md +9 -0
- package/docs/errors/validation/unknown-path.md +9 -0
- package/package.json +96 -0
- package/src/AGENTS.md +7 -0
- package/src/btest.ts +906 -0
- package/src/cli/btest.ts +455 -0
- package/src/cli/common.ts +53 -0
- package/src/cli/devices.ts +527 -0
- package/src/cli/discover.ts +186 -0
- package/src/cli/execute.ts +333 -0
- package/src/cli/mcp.ts +109 -0
- package/src/cli/retrieve.ts +383 -0
- package/src/cli/terminal.ts +303 -0
- package/src/cli/transfer.ts +471 -0
- package/src/cli.ts +115 -0
- package/src/core/envelope.ts +221 -0
- package/src/core/error-catalog.ts +469 -0
- package/src/core/routeros-errors.ts +268 -0
- package/src/data/AGENTS.md +11 -0
- package/src/data/dude-db.ts +0 -0
- package/src/data/mndp-cache.ts +132 -0
- package/src/data/mndp.ts +405 -0
- package/src/data/winbox-cdb-write.ts +200 -0
- package/src/data/winbox-cdb.ts +913 -0
- package/src/devices-schema.ts +107 -0
- package/src/devices.ts +1717 -0
- package/src/discover.ts +802 -0
- package/src/errors.ts +189 -0
- package/src/execute.ts +1350 -0
- package/src/index.ts +353 -0
- package/src/mcp/config.ts +66 -0
- package/src/mcp/resources.ts +98 -0
- package/src/mcp/safety.ts +119 -0
- package/src/mcp/server.ts +225 -0
- package/src/mcp/tools.ts +657 -0
- package/src/mcp.ts +33 -0
- package/src/protocols/AGENTS.md +14 -0
- package/src/protocols/adapter.ts +866 -0
- package/src/protocols/btest-session.ts +1493 -0
- package/src/protocols/btest.ts +447 -0
- package/src/protocols/ec-srp5.ts +466 -0
- package/src/protocols/index.ts +118 -0
- package/src/protocols/mac-telnet-console.ts +655 -0
- package/src/protocols/mac-telnet.ts +1299 -0
- package/src/protocols/mtwei.ts +47 -0
- package/src/protocols/native-api.ts +796 -0
- package/src/protocols/sftp.ts +427 -0
- package/src/protocols/ssh.ts +297 -0
- package/src/resolver/cdb.ts +580 -0
- package/src/resolver/comment-kv.ts +360 -0
- package/src/resolver/index.ts +74 -0
- package/src/resolver/mac.ts +227 -0
- package/src/resolver/settings.ts +228 -0
- package/src/resolver/target.ts +442 -0
- package/src/retrieve-fanout.ts +533 -0
- package/src/retrieve.ts +1215 -0
- package/src/terminal.ts +444 -0
- package/src/transfer.ts +1554 -0
- package/src/tui.ts +22 -0
- package/src/webproxy.ts +16 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `centrs transfer` CLI surface: command metadata, arg parsing, sub-verb
|
|
3
|
+
* dispatch, and error-to-envelope rendering. `upload` / `download` are also
|
|
4
|
+
* reachable as top-level command aliases (`centrs upload <router> …`) that
|
|
5
|
+
* forward here with a fixed verb — see `runTransferCli`'s `fixedVerb` option and
|
|
6
|
+
* `../cli.ts`. Mirrors the shape of `./retrieve.ts`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { asCentrsError, formatCentrsErrorText } from "../errors.ts";
|
|
10
|
+
import {
|
|
11
|
+
buildTransferErrorEnvelope,
|
|
12
|
+
CentrsError,
|
|
13
|
+
describeCentrs,
|
|
14
|
+
renderTransferEnvelope,
|
|
15
|
+
type TransferOutputFormat,
|
|
16
|
+
type TransferRequest,
|
|
17
|
+
type TransferVerb,
|
|
18
|
+
transfer,
|
|
19
|
+
transferOutputFormats,
|
|
20
|
+
transferVerbs,
|
|
21
|
+
} from "../index.ts";
|
|
22
|
+
import {
|
|
23
|
+
type CliCommandMetadata,
|
|
24
|
+
expectValue,
|
|
25
|
+
renderCommandHelp,
|
|
26
|
+
} from "./common.ts";
|
|
27
|
+
|
|
28
|
+
/** Sub-verb aliases, resolved to canonical verbs silently (commands/AGENTS.md). */
|
|
29
|
+
const VERB_ALIASES: Record<string, TransferVerb> = {
|
|
30
|
+
print: "list",
|
|
31
|
+
ls: "list",
|
|
32
|
+
put: "upload",
|
|
33
|
+
get: "download",
|
|
34
|
+
rm: "remove",
|
|
35
|
+
delete: "remove",
|
|
36
|
+
mkdir: "mkdir",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const transferCommand: CliCommandMetadata = {
|
|
40
|
+
name: "transfer",
|
|
41
|
+
usage:
|
|
42
|
+
"centrs transfer <router> upload <local> [remote] | download <remote> [local] | list [path] | remove <remote> | mkdir <remote> | copy <src> <dst> [flags]",
|
|
43
|
+
summary:
|
|
44
|
+
"Copy files to/from a RouterOS device and manage device files (rest/native/sftp).",
|
|
45
|
+
options: [
|
|
46
|
+
{
|
|
47
|
+
flag: "--via",
|
|
48
|
+
valueName: "<method>",
|
|
49
|
+
description:
|
|
50
|
+
"Pin the method: rest, native, or sftp (large transfers); scp/fetch/ftp are not built yet. Auto picks the cheapest by size/direction.",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
flag: "--ssh-key",
|
|
54
|
+
valueName: "<path>",
|
|
55
|
+
description:
|
|
56
|
+
"sftp only: explicit private-key path. Falls back to CENTRS_SSH_KEY / the ssh-agent.",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
flag: "--insecure",
|
|
60
|
+
description:
|
|
61
|
+
"Accept a self-signed TLS cert (https/api-ssl) or a new SSH host key. Default verifies.",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
flag: "--force",
|
|
65
|
+
valueName: "(--overwrite)",
|
|
66
|
+
description: "Replace an existing destination. Default refuses it.",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
flag: "--verify",
|
|
70
|
+
valueName: "<size|checksum|off>",
|
|
71
|
+
description:
|
|
72
|
+
"Post-transfer integrity check. Default size; --no-verify skips.",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
flag: "--type",
|
|
76
|
+
valueName: "<file|directory|disk|package>",
|
|
77
|
+
description: "list filter: RouterOS /file row type.",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
flag: "--name",
|
|
81
|
+
valueName: "<glob>",
|
|
82
|
+
description: "list filter: file-name glob.",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
flag: "--host",
|
|
86
|
+
valueName: "<host|url>",
|
|
87
|
+
description: "Override the resolved host or base URL.",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
flag: "--port",
|
|
91
|
+
valueName: "<port>",
|
|
92
|
+
description: "Override the resolved management port.",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
flag: "--username / --user / -u",
|
|
96
|
+
valueName: "<name>",
|
|
97
|
+
description:
|
|
98
|
+
"RouterOS username (aliases --user, -u). Falls back to CENTRS_USERNAME.",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
flag: "--password",
|
|
102
|
+
valueName: "<secret>",
|
|
103
|
+
description: "RouterOS password. Falls back to CENTRS_PASSWORD.",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
flag: "--timeout",
|
|
107
|
+
valueName: "<ms|5s>",
|
|
108
|
+
description: "Operation timeout. REST rejects values above 60s.",
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
flag: "--format",
|
|
112
|
+
valueName: `<${transferOutputFormats.join("|")}>`,
|
|
113
|
+
description:
|
|
114
|
+
"Output format. Defaults to text; --json / --yaml shortcuts.",
|
|
115
|
+
},
|
|
116
|
+
{ flag: "--json", description: "Shortcut for --format json." },
|
|
117
|
+
{
|
|
118
|
+
flag: "--cdb-file",
|
|
119
|
+
valueName: "<path>",
|
|
120
|
+
description: "Read target credentials from a WinBox CDB file.",
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
flag: "--cdb-password",
|
|
124
|
+
valueName: "<password>",
|
|
125
|
+
description: "Decrypt an encrypted WinBox CDB file.",
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
flag: "--validate / --no-validate",
|
|
129
|
+
description: "Enable or disable the existence-probe preflight.",
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
flag: "--verbose",
|
|
133
|
+
description: "Show the resolved transport in text output.",
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export interface RunTransferOptions {
|
|
139
|
+
/** Fixed verb for the top-level `upload` / `download` aliases. */
|
|
140
|
+
fixedVerb?: TransferVerb;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function runTransferCli(
|
|
144
|
+
args: readonly string[],
|
|
145
|
+
options: RunTransferOptions = {},
|
|
146
|
+
): Promise<number> {
|
|
147
|
+
let request: TransferRequest | undefined;
|
|
148
|
+
try {
|
|
149
|
+
const parsed = parseTransferCliArgs(args, options.fixedVerb);
|
|
150
|
+
if (parsed.help) {
|
|
151
|
+
console.log(renderCommandHelp(describeCentrs(), transferCommand));
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
request = parsed.request;
|
|
155
|
+
|
|
156
|
+
const envelope = await transfer(request);
|
|
157
|
+
const format = resolvedFormat(request);
|
|
158
|
+
const rendered = renderTransferEnvelope(envelope, format, {
|
|
159
|
+
verbose: request.verbose,
|
|
160
|
+
});
|
|
161
|
+
// A download to stdout (`local === "-"`) has already streamed the file bytes
|
|
162
|
+
// to stdout inside `transfer()`; keep the structured envelope on stderr so it
|
|
163
|
+
// cannot interleave with — and corrupt — the piped payload.
|
|
164
|
+
if (request.verb === "download" && request.local === "-") {
|
|
165
|
+
console.error(rendered);
|
|
166
|
+
} else {
|
|
167
|
+
console.log(rendered);
|
|
168
|
+
}
|
|
169
|
+
return 0;
|
|
170
|
+
} catch (error) {
|
|
171
|
+
// Drop credential fields before any renderer touches the request: error
|
|
172
|
+
// output reaches stderr/CI logs and must never carry the raw password
|
|
173
|
+
// (CodeQL js/clear-text-logging).
|
|
174
|
+
const safeRequest = redactTransferRequest(request);
|
|
175
|
+
const format = inferRequestedFormat(args, safeRequest);
|
|
176
|
+
if (format === "json" || format === "yaml") {
|
|
177
|
+
const envelope = buildTransferErrorEnvelope(
|
|
178
|
+
safeRequest ?? { verb: options.fixedVerb ?? "list" },
|
|
179
|
+
error,
|
|
180
|
+
);
|
|
181
|
+
console.error(
|
|
182
|
+
renderTransferEnvelope(envelope, format, {
|
|
183
|
+
verbose: safeRequest?.verbose ?? false,
|
|
184
|
+
}),
|
|
185
|
+
);
|
|
186
|
+
} else {
|
|
187
|
+
console.error(
|
|
188
|
+
formatCentrsErrorText(
|
|
189
|
+
asCentrsError(error, {
|
|
190
|
+
code: "input/invalid-command",
|
|
191
|
+
summary: error instanceof Error ? error.message : String(error),
|
|
192
|
+
remediation:
|
|
193
|
+
"Use `centrs transfer --help` to inspect the supported command shape and flags.",
|
|
194
|
+
}),
|
|
195
|
+
{ verbose: safeRequest?.verbose ?? args.includes("--verbose") },
|
|
196
|
+
),
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
return 1;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Strip credential fields before a failed request is handed to the error
|
|
205
|
+
* envelope/text renderers. Those outputs land on stderr (and CI logs), so the
|
|
206
|
+
* raw `password` / `cdbPassword` must never reach them.
|
|
207
|
+
*/
|
|
208
|
+
function redactTransferRequest(
|
|
209
|
+
request: TransferRequest | undefined,
|
|
210
|
+
): TransferRequest | undefined {
|
|
211
|
+
if (!request) {
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
// Always return a fresh object with the secrets stripped — never alias the
|
|
215
|
+
// original request, so the raw password cannot flow into a rendered error
|
|
216
|
+
// even by way of a sibling field.
|
|
217
|
+
return { ...request, password: undefined, cdbPassword: undefined };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
interface ParsedTransfer {
|
|
221
|
+
help?: boolean;
|
|
222
|
+
request: TransferRequest;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function parseTransferCliArgs(
|
|
226
|
+
args: readonly string[],
|
|
227
|
+
fixedVerb?: TransferVerb,
|
|
228
|
+
): ParsedTransfer {
|
|
229
|
+
const flags: Partial<TransferRequest> & { verbose?: boolean } = {};
|
|
230
|
+
const positional: string[] = [];
|
|
231
|
+
let help = false;
|
|
232
|
+
// `--verify <mode>` and `--no-verify` both map to the single `verify` field,
|
|
233
|
+
// so a contradictory pair is a CLI-surface conflict the request shape can't see.
|
|
234
|
+
let explicitVerify: string | undefined;
|
|
235
|
+
let sawNoVerify = false;
|
|
236
|
+
|
|
237
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
238
|
+
const arg = args[index];
|
|
239
|
+
if (arg === undefined) {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
switch (arg) {
|
|
243
|
+
case "--help":
|
|
244
|
+
case "-h":
|
|
245
|
+
help = true;
|
|
246
|
+
break;
|
|
247
|
+
case "--via":
|
|
248
|
+
flags.via = expectValue(args, ++index, arg);
|
|
249
|
+
break;
|
|
250
|
+
case "--host":
|
|
251
|
+
flags.host = expectValue(args, ++index, arg);
|
|
252
|
+
break;
|
|
253
|
+
case "--port":
|
|
254
|
+
flags.port = Number.parseInt(expectValue(args, ++index, arg), 10);
|
|
255
|
+
break;
|
|
256
|
+
case "--user":
|
|
257
|
+
case "-u":
|
|
258
|
+
case "--username":
|
|
259
|
+
flags.username = expectValue(args, ++index, arg);
|
|
260
|
+
break;
|
|
261
|
+
case "--password":
|
|
262
|
+
flags.password = expectValue(args, ++index, arg);
|
|
263
|
+
break;
|
|
264
|
+
case "--ssh-key":
|
|
265
|
+
flags.sshKey = expectValue(args, ++index, arg);
|
|
266
|
+
break;
|
|
267
|
+
case "--insecure":
|
|
268
|
+
flags.insecure = true;
|
|
269
|
+
break;
|
|
270
|
+
case "--timeout":
|
|
271
|
+
flags.timeout = expectValue(args, ++index, arg);
|
|
272
|
+
break;
|
|
273
|
+
case "--format":
|
|
274
|
+
flags.format = expectValue(args, ++index, arg);
|
|
275
|
+
break;
|
|
276
|
+
case "--json":
|
|
277
|
+
flags.format = "json";
|
|
278
|
+
break;
|
|
279
|
+
case "--yaml":
|
|
280
|
+
flags.format = "yaml";
|
|
281
|
+
break;
|
|
282
|
+
case "--force":
|
|
283
|
+
case "--overwrite":
|
|
284
|
+
flags.force = true;
|
|
285
|
+
break;
|
|
286
|
+
case "--verify":
|
|
287
|
+
explicitVerify = expectValue(args, ++index, arg);
|
|
288
|
+
flags.verify = explicitVerify;
|
|
289
|
+
break;
|
|
290
|
+
case "--no-verify":
|
|
291
|
+
sawNoVerify = true;
|
|
292
|
+
flags.verify = "off";
|
|
293
|
+
break;
|
|
294
|
+
case "--type":
|
|
295
|
+
flags.type = expectValue(args, ++index, arg);
|
|
296
|
+
break;
|
|
297
|
+
case "--name":
|
|
298
|
+
flags.name = expectValue(args, ++index, arg);
|
|
299
|
+
break;
|
|
300
|
+
case "--cdb-file":
|
|
301
|
+
flags.cdbFile = expectValue(args, ++index, arg);
|
|
302
|
+
break;
|
|
303
|
+
case "--cdb-password":
|
|
304
|
+
flags.cdbPassword = expectValue(args, ++index, arg);
|
|
305
|
+
break;
|
|
306
|
+
case "--validate":
|
|
307
|
+
flags.validate = true;
|
|
308
|
+
break;
|
|
309
|
+
case "--no-validate":
|
|
310
|
+
flags.validate = false;
|
|
311
|
+
break;
|
|
312
|
+
case "--verbose":
|
|
313
|
+
flags.verbose = true;
|
|
314
|
+
break;
|
|
315
|
+
default:
|
|
316
|
+
// A lone `-` is the stdin/stdout positional (upload from / download
|
|
317
|
+
// to a pipe), not a flag.
|
|
318
|
+
if (arg !== "-" && arg.startsWith("-")) {
|
|
319
|
+
throw new CentrsError({
|
|
320
|
+
code: "input/invalid-command",
|
|
321
|
+
summary: `Unknown transfer flag: ${arg}`,
|
|
322
|
+
remediation:
|
|
323
|
+
"Remove the flag or run `centrs transfer --help` for the supported options.",
|
|
324
|
+
context: { flag: arg },
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
positional.push(arg);
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (help) {
|
|
333
|
+
return { help: true, request: { verb: fixedVerb ?? "list" } };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (sawNoVerify && explicitVerify !== undefined && explicitVerify !== "off") {
|
|
337
|
+
throw new CentrsError({
|
|
338
|
+
code: "usage/conflicting-flags",
|
|
339
|
+
summary: "`--verify` and `--no-verify` cannot be combined.",
|
|
340
|
+
remediation:
|
|
341
|
+
"Pass either an explicit `--verify <size|checksum>` or `--no-verify`, not both.",
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const request = assemblePositionals(positional, flags, fixedVerb);
|
|
346
|
+
return { request };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Map positionals to the per-verb request shape. For the top-level alias the
|
|
351
|
+
* verb is fixed and the first positional is `<router>`; for `centrs transfer`
|
|
352
|
+
* the positionals are `<router> <verb> …`.
|
|
353
|
+
*/
|
|
354
|
+
function assemblePositionals(
|
|
355
|
+
positional: readonly string[],
|
|
356
|
+
flags: Partial<TransferRequest> & { verbose?: boolean },
|
|
357
|
+
fixedVerb?: TransferVerb,
|
|
358
|
+
): TransferRequest {
|
|
359
|
+
let targetInput: string | undefined;
|
|
360
|
+
let verb: TransferVerb;
|
|
361
|
+
let rest: readonly string[];
|
|
362
|
+
|
|
363
|
+
if (fixedVerb) {
|
|
364
|
+
targetInput = positional[0];
|
|
365
|
+
verb = fixedVerb;
|
|
366
|
+
rest = positional.slice(1);
|
|
367
|
+
} else {
|
|
368
|
+
targetInput = positional[0];
|
|
369
|
+
const rawVerb = positional[1];
|
|
370
|
+
if (!targetInput || !rawVerb) {
|
|
371
|
+
throw new CentrsError({
|
|
372
|
+
code: "input/invalid-command",
|
|
373
|
+
summary:
|
|
374
|
+
"`centrs transfer` requires <router> and a verb (upload|download|list|remove|mkdir|copy).",
|
|
375
|
+
remediation:
|
|
376
|
+
"Pass the target then a verb, e.g. `centrs transfer <router> list`; run `--help` for the full shape.",
|
|
377
|
+
context: { targetInput, verb: rawVerb },
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
verb = canonicalVerb(rawVerb);
|
|
381
|
+
rest = positional.slice(2);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (!targetInput) {
|
|
385
|
+
throw new CentrsError({
|
|
386
|
+
code: "input/invalid-command",
|
|
387
|
+
summary: "`centrs transfer` requires a <router> target.",
|
|
388
|
+
remediation:
|
|
389
|
+
"Pass the router host/identity as the first argument; run `centrs transfer --help` for the command shape.",
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const request: TransferRequest = { verb, targetInput, ...flags };
|
|
394
|
+
|
|
395
|
+
switch (verb) {
|
|
396
|
+
case "upload":
|
|
397
|
+
request.local = rest[0];
|
|
398
|
+
request.remote = rest[1];
|
|
399
|
+
break;
|
|
400
|
+
case "download":
|
|
401
|
+
request.remote = rest[0];
|
|
402
|
+
request.local = rest[1];
|
|
403
|
+
break;
|
|
404
|
+
case "list":
|
|
405
|
+
request.path = rest[0];
|
|
406
|
+
break;
|
|
407
|
+
case "remove":
|
|
408
|
+
case "mkdir":
|
|
409
|
+
request.remote = rest[0];
|
|
410
|
+
break;
|
|
411
|
+
case "copy":
|
|
412
|
+
request.remote = rest[0];
|
|
413
|
+
request.remoteDest = rest[1];
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return request;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function canonicalVerb(raw: string): TransferVerb {
|
|
421
|
+
if (transferVerbs.includes(raw as TransferVerb)) {
|
|
422
|
+
return raw as TransferVerb;
|
|
423
|
+
}
|
|
424
|
+
const alias = VERB_ALIASES[raw];
|
|
425
|
+
if (alias) {
|
|
426
|
+
return alias;
|
|
427
|
+
}
|
|
428
|
+
throw new CentrsError({
|
|
429
|
+
code: "input/invalid-command",
|
|
430
|
+
summary: `Unknown transfer verb: ${raw}.`,
|
|
431
|
+
remediation: `Use one of ${transferVerbs.join(", ")} (aliases: ${Object.keys(VERB_ALIASES).join(", ")}), or run \`centrs transfer --help\`.`,
|
|
432
|
+
context: { verb: raw },
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function resolvedFormat(request: TransferRequest): TransferOutputFormat {
|
|
437
|
+
const candidate = request.format;
|
|
438
|
+
if (candidate === "json" || candidate === "yaml" || candidate === "text") {
|
|
439
|
+
return candidate;
|
|
440
|
+
}
|
|
441
|
+
const envFormat = process.env["CENTRS_FORMAT"];
|
|
442
|
+
if (envFormat === "json" || envFormat === "yaml" || envFormat === "text") {
|
|
443
|
+
return envFormat;
|
|
444
|
+
}
|
|
445
|
+
return "text";
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function inferRequestedFormat(
|
|
449
|
+
args: readonly string[],
|
|
450
|
+
request?: TransferRequest,
|
|
451
|
+
): TransferOutputFormat {
|
|
452
|
+
if (request) {
|
|
453
|
+
return resolvedFormat(request);
|
|
454
|
+
}
|
|
455
|
+
if (args.includes("--json")) {
|
|
456
|
+
return "json";
|
|
457
|
+
}
|
|
458
|
+
if (args.includes("--yaml")) {
|
|
459
|
+
return "yaml";
|
|
460
|
+
}
|
|
461
|
+
const formatIndex = args.indexOf("--format");
|
|
462
|
+
const value = formatIndex !== -1 ? args[formatIndex + 1] : undefined;
|
|
463
|
+
if (value === "json" || value === "yaml" || value === "text") {
|
|
464
|
+
return value;
|
|
465
|
+
}
|
|
466
|
+
const envFormat = process.env["CENTRS_FORMAT"];
|
|
467
|
+
if (envFormat === "json" || envFormat === "yaml" || envFormat === "text") {
|
|
468
|
+
return envFormat;
|
|
469
|
+
}
|
|
470
|
+
return "text";
|
|
471
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { btestCommand, runBtestCli } from "./cli/btest.ts";
|
|
4
|
+
import { devicesCommand, runDevicesCli } from "./cli/devices.ts";
|
|
5
|
+
import { discoverCommand, runDiscoverCli } from "./cli/discover.ts";
|
|
6
|
+
import { executeCommand, runExecuteCli } from "./cli/execute.ts";
|
|
7
|
+
import { mcpCommand, runMcpCli } from "./cli/mcp.ts";
|
|
8
|
+
import { retrieveCommand, runRetrieveCli } from "./cli/retrieve.ts";
|
|
9
|
+
import { runTerminalCli, terminalCommand } from "./cli/terminal.ts";
|
|
10
|
+
import { runTransferCli, transferCommand } from "./cli/transfer.ts";
|
|
11
|
+
import { asCentrsError, formatCentrsErrorText } from "./errors.ts";
|
|
12
|
+
import { describeCentrs, plannedProtocols, plannedSurfaces } from "./index.ts";
|
|
13
|
+
|
|
14
|
+
const commandSummaries: ReadonlyArray<{ name: string; summary: string }> = [
|
|
15
|
+
retrieveCommand,
|
|
16
|
+
executeCommand,
|
|
17
|
+
transferCommand,
|
|
18
|
+
terminalCommand,
|
|
19
|
+
devicesCommand,
|
|
20
|
+
discoverCommand,
|
|
21
|
+
btestCommand,
|
|
22
|
+
mcpCommand,
|
|
23
|
+
].map((command) => ({ name: command.name, summary: command.summary }));
|
|
24
|
+
|
|
25
|
+
export function renderCliHelp(): string {
|
|
26
|
+
return [
|
|
27
|
+
describeCentrs(),
|
|
28
|
+
"",
|
|
29
|
+
`Surfaces: ${plannedSurfaces.join(", ")}`,
|
|
30
|
+
`Protocols: ${plannedProtocols.join(", ")}`,
|
|
31
|
+
"",
|
|
32
|
+
"Commands:",
|
|
33
|
+
...commandSummaries.map(
|
|
34
|
+
(command) => ` ${command.name.padEnd(12)} ${command.summary}`,
|
|
35
|
+
),
|
|
36
|
+
"",
|
|
37
|
+
"Use `centrs <command> --help` for command-specific options.",
|
|
38
|
+
].join("\n");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function runCli(
|
|
42
|
+
args: readonly string[] = Bun.argv.slice(2),
|
|
43
|
+
): Promise<number> {
|
|
44
|
+
try {
|
|
45
|
+
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
46
|
+
console.log(renderCliHelp());
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const [command, ...rest] = args;
|
|
51
|
+
if (command === "retrieve") {
|
|
52
|
+
return await runRetrieveCli(rest);
|
|
53
|
+
}
|
|
54
|
+
if (command === "execute") {
|
|
55
|
+
return await runExecuteCli(rest);
|
|
56
|
+
}
|
|
57
|
+
if (command === "transfer") {
|
|
58
|
+
return await runTransferCli(rest);
|
|
59
|
+
}
|
|
60
|
+
if (command === "terminal") {
|
|
61
|
+
return await runTerminalCli(rest);
|
|
62
|
+
}
|
|
63
|
+
// Top-level shortcuts for the two highest-frequency transfer verbs; they
|
|
64
|
+
// forward to the transfer runner with a fixed verb (commands/transfer/README.md).
|
|
65
|
+
if (command === "upload") {
|
|
66
|
+
return await runTransferCli(rest, { fixedVerb: "upload" });
|
|
67
|
+
}
|
|
68
|
+
if (command === "download") {
|
|
69
|
+
return await runTransferCli(rest, { fixedVerb: "download" });
|
|
70
|
+
}
|
|
71
|
+
if (command === "devices") {
|
|
72
|
+
return await runDevicesCli(rest);
|
|
73
|
+
}
|
|
74
|
+
if (command === "discover") {
|
|
75
|
+
return await runDiscoverCli(rest);
|
|
76
|
+
}
|
|
77
|
+
if (command === "btest") {
|
|
78
|
+
return await runBtestCli(rest);
|
|
79
|
+
}
|
|
80
|
+
if (command === "mcp") {
|
|
81
|
+
return await runMcpCli(rest);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.error(
|
|
85
|
+
formatCentrsErrorText(
|
|
86
|
+
asCentrsError(new Error(`Unknown centrs command: ${command}`), {
|
|
87
|
+
code: "input/invalid-command",
|
|
88
|
+
summary: `Unknown centrs command: ${command}`,
|
|
89
|
+
remediation: "Use `centrs --help` to see the available commands.",
|
|
90
|
+
}),
|
|
91
|
+
),
|
|
92
|
+
);
|
|
93
|
+
return 1;
|
|
94
|
+
} catch (error) {
|
|
95
|
+
// Last-resort safety net: a runner should already convert its own errors
|
|
96
|
+
// to a structured envelope, so reaching here means an unconverted throw
|
|
97
|
+
// escaped. Never let a raw stack trace reach the user.
|
|
98
|
+
console.error(
|
|
99
|
+
formatCentrsErrorText(
|
|
100
|
+
asCentrsError(error, {
|
|
101
|
+
code: "internal/unhandled",
|
|
102
|
+
summary: error instanceof Error ? error.message : String(error),
|
|
103
|
+
remediation:
|
|
104
|
+
"This is a centrs bug; re-run with --verbose and file an issue with the printed code.",
|
|
105
|
+
}),
|
|
106
|
+
{ verbose: args.includes("--verbose") },
|
|
107
|
+
),
|
|
108
|
+
);
|
|
109
|
+
return 1;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (import.meta.main) {
|
|
114
|
+
process.exitCode = await runCli();
|
|
115
|
+
}
|