@procedures/mark 0.0.0-stage → 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.
Files changed (91) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +21 -0
  3. package/README.md +799 -2
  4. package/dist/.tsbuildinfo +1 -0
  5. package/dist/args.d.ts +77 -0
  6. package/dist/args.d.ts.map +1 -0
  7. package/dist/args.js +351 -0
  8. package/dist/args.js.map +1 -0
  9. package/dist/ask.d.ts +48 -0
  10. package/dist/ask.d.ts.map +1 -0
  11. package/dist/ask.js +91 -0
  12. package/dist/ask.js.map +1 -0
  13. package/dist/bin.d.ts +9 -0
  14. package/dist/bin.d.ts.map +1 -0
  15. package/dist/bin.js +10 -0
  16. package/dist/bin.js.map +1 -0
  17. package/dist/cli.d.ts +61 -0
  18. package/dist/cli.d.ts.map +1 -0
  19. package/dist/cli.js +530 -0
  20. package/dist/cli.js.map +1 -0
  21. package/dist/client-mode.d.ts +21 -0
  22. package/dist/client-mode.d.ts.map +1 -0
  23. package/dist/client-mode.js +75 -0
  24. package/dist/client-mode.js.map +1 -0
  25. package/dist/ecosystem.d.ts +66 -0
  26. package/dist/ecosystem.d.ts.map +1 -0
  27. package/dist/ecosystem.js +211 -0
  28. package/dist/ecosystem.js.map +1 -0
  29. package/dist/failure.d.ts +16 -0
  30. package/dist/failure.d.ts.map +1 -0
  31. package/dist/failure.js +47 -0
  32. package/dist/failure.js.map +1 -0
  33. package/dist/format.d.ts +30 -0
  34. package/dist/format.d.ts.map +1 -0
  35. package/dist/format.js +125 -0
  36. package/dist/format.js.map +1 -0
  37. package/dist/index.d.ts +13 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +13 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/lockfile.d.ts +26 -0
  42. package/dist/lockfile.d.ts.map +1 -0
  43. package/dist/lockfile.js +68 -0
  44. package/dist/lockfile.js.map +1 -0
  45. package/dist/manifest-build.d.ts +7 -0
  46. package/dist/manifest-build.d.ts.map +1 -0
  47. package/dist/manifest-build.js +11 -0
  48. package/dist/manifest-build.js.map +1 -0
  49. package/dist/manifest.d.ts +57 -0
  50. package/dist/manifest.d.ts.map +1 -0
  51. package/dist/manifest.js +140 -0
  52. package/dist/manifest.js.map +1 -0
  53. package/dist/manifest.json +20885 -0
  54. package/dist/parse.d.ts +143 -0
  55. package/dist/parse.d.ts.map +1 -0
  56. package/dist/parse.js +509 -0
  57. package/dist/parse.js.map +1 -0
  58. package/dist/print.d.ts +42 -0
  59. package/dist/print.d.ts.map +1 -0
  60. package/dist/print.js +80 -0
  61. package/dist/print.js.map +1 -0
  62. package/dist/prompts.d.ts +58 -0
  63. package/dist/prompts.d.ts.map +1 -0
  64. package/dist/prompts.js +124 -0
  65. package/dist/prompts.js.map +1 -0
  66. package/dist/repl.d.ts +34 -0
  67. package/dist/repl.d.ts.map +1 -0
  68. package/dist/repl.js +101 -0
  69. package/dist/repl.js.map +1 -0
  70. package/dist/server-mode.d.ts +37 -0
  71. package/dist/server-mode.d.ts.map +1 -0
  72. package/dist/server-mode.js +167 -0
  73. package/dist/server-mode.js.map +1 -0
  74. package/package.json +97 -3
  75. package/src/args.ts +440 -0
  76. package/src/ask.ts +119 -0
  77. package/src/bin.ts +11 -0
  78. package/src/cli.ts +596 -0
  79. package/src/client-mode.ts +90 -0
  80. package/src/ecosystem.ts +258 -0
  81. package/src/failure.ts +47 -0
  82. package/src/format.ts +177 -0
  83. package/src/index.ts +31 -0
  84. package/src/lockfile.ts +85 -0
  85. package/src/manifest-build.ts +12 -0
  86. package/src/manifest.ts +162 -0
  87. package/src/parse.ts +670 -0
  88. package/src/print.ts +108 -0
  89. package/src/prompts.ts +181 -0
  90. package/src/repl.ts +121 -0
  91. package/src/server-mode.ts +205 -0
package/src/print.ts ADDED
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Terminal Output Utilities
3
+ *
4
+ * Replaces Gluegun's print with Chalk + Ora + cli-table3
5
+ * Provides a consistent interface for CLI output.
6
+ */
7
+
8
+ import chalk from "chalk";
9
+ import ora from "ora";
10
+ import Table from "cli-table3";
11
+
12
+ /**
13
+ * Spinner interface (compatible with existing code)
14
+ */
15
+ export interface Spinner {
16
+ stop: () => void;
17
+ succeed: (message?: string) => void;
18
+ fail: (message?: string) => void;
19
+ text: string;
20
+ }
21
+
22
+ /**
23
+ * Print utilities
24
+ */
25
+ export const print = {
26
+ // Basic output
27
+ info: (msg: string): void => {
28
+ console.log(chalk.cyan(msg));
29
+ },
30
+
31
+ success: (msg: string): void => {
32
+ console.log(chalk.green("✓ " + msg));
33
+ },
34
+
35
+ error: (msg: string): void => {
36
+ console.error(chalk.red("✗ " + msg));
37
+ },
38
+
39
+ warning: (msg: string): void => {
40
+ console.log(chalk.yellow("⚠ " + msg));
41
+ },
42
+
43
+ muted: (msg: string): void => {
44
+ console.log(chalk.gray(msg));
45
+ },
46
+
47
+ highlight: (msg: string): void => {
48
+ console.log(chalk.bold.white(msg));
49
+ },
50
+
51
+ // Spinner - returns Ora instance wrapped to match our Spinner interface
52
+ spin: (msg: string): Spinner => {
53
+ const spinner = ora(msg).start();
54
+ return {
55
+ stop: () => spinner.stop(),
56
+ succeed: (message?: string) => spinner.succeed(message),
57
+ fail: (message?: string) => spinner.fail(message),
58
+ get text() {
59
+ return spinner.text;
60
+ },
61
+ set text(value: string) {
62
+ spinner.text = value;
63
+ },
64
+ };
65
+ },
66
+
67
+ // Table - accepts array of arrays (first row is headers)
68
+ table: (data: string[][], _options?: { format?: string }): void => {
69
+ if (data.length === 0) return;
70
+
71
+ const [headers, ...rows] = data;
72
+ if (!headers) return;
73
+
74
+ const table = new Table({
75
+ head: headers.map((h) => chalk.bold(h)),
76
+ style: {
77
+ head: [],
78
+ border: [],
79
+ },
80
+ });
81
+
82
+ for (const row of rows) {
83
+ table.push(row);
84
+ }
85
+
86
+ console.log(table.toString());
87
+ },
88
+
89
+ // Divider line
90
+ divider: (): void => {
91
+ console.log(chalk.gray("─".repeat(50)));
92
+ },
93
+
94
+ // Newline
95
+ newline: (): void => {
96
+ console.log();
97
+ },
98
+ };
99
+
100
+ /**
101
+ * Print interface type for external use
102
+ */
103
+ export type Print = typeof print;
104
+
105
+ /**
106
+ * Re-export chalk for direct color access
107
+ */
108
+ export { chalk };
package/src/prompts.ts ADDED
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Interactive Prompts
3
+ *
4
+ * Wraps @clack/prompts for beautiful, modern CLI prompts.
5
+ * Used by Vite, Astro, and other modern tools.
6
+ */
7
+
8
+ import * as p from "@clack/prompts";
9
+ import chalk from "chalk";
10
+
11
+ // Types
12
+ type TextResult = Promise<string | symbol>;
13
+ type ConfirmResult = Promise<boolean | symbol>;
14
+ type SelectResult<T> = Promise<T | symbol>;
15
+ type MultiselectResult<T> = Promise<T[] | symbol>;
16
+ type PasswordResult = Promise<string | symbol>;
17
+ type SpinnerInstance = ReturnType<typeof p.spinner>;
18
+
19
+ /** Prompts interface for type annotation */
20
+ export interface Prompts {
21
+ text: (message: string, options?: { placeholder?: string; defaultValue?: string }) => TextResult;
22
+ confirm: (message: string, options?: { initialValue?: boolean }) => ConfirmResult;
23
+ select: <T extends string>(
24
+ message: string,
25
+ options: Array<{ value: T; label: string; hint?: string }>
26
+ ) => SelectResult<T>;
27
+ multiselect: <T extends string>(
28
+ message: string,
29
+ options: Array<{ value: T; label: string; hint?: string }>
30
+ ) => MultiselectResult<T>;
31
+ password: (message: string) => PasswordResult;
32
+ group: typeof p.group;
33
+ intro: (msg: string) => void;
34
+ outro: (msg: string) => void;
35
+ spinner: () => SpinnerInstance;
36
+ log: {
37
+ info: (msg: string) => void;
38
+ success: (msg: string) => void;
39
+ warn: (msg: string) => void;
40
+ error: (msg: string) => void;
41
+ message: (msg: string) => void;
42
+ step: (msg: string) => void;
43
+ };
44
+ isCancel: typeof p.isCancel;
45
+ cancel: (msg?: string) => void;
46
+ note: (message: string, title?: string) => void;
47
+ }
48
+
49
+ /**
50
+ * Text input prompt
51
+ */
52
+ function text(
53
+ message: string,
54
+ options?: { placeholder?: string; defaultValue?: string }
55
+ ): TextResult {
56
+ const opts: Parameters<typeof p.text>[0] = { message };
57
+ if (options?.placeholder !== undefined) opts.placeholder = options.placeholder;
58
+ if (options?.defaultValue !== undefined) opts.defaultValue = options.defaultValue;
59
+ return p.text(opts);
60
+ }
61
+
62
+ /**
63
+ * Yes/No confirmation prompt
64
+ */
65
+ function confirm(message: string, options?: { initialValue?: boolean }): ConfirmResult {
66
+ const opts: Parameters<typeof p.confirm>[0] = { message };
67
+ if (options?.initialValue !== undefined) opts.initialValue = options.initialValue;
68
+ return p.confirm(opts);
69
+ }
70
+
71
+ /**
72
+ * Single select prompt
73
+ */
74
+ function select<T extends string>(
75
+ message: string,
76
+ options: Array<{ value: T; label: string; hint?: string }>
77
+ ): SelectResult<T> {
78
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
79
+ return p.select({ message, options } as any) as SelectResult<T>;
80
+ }
81
+
82
+ /**
83
+ * Multi-select prompt
84
+ */
85
+ function multiselect<T extends string>(
86
+ message: string,
87
+ options: Array<{ value: T; label: string; hint?: string }>
88
+ ): MultiselectResult<T> {
89
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
90
+ return p.multiselect({ message, options } as any) as MultiselectResult<T>;
91
+ }
92
+
93
+ /**
94
+ * Password/hidden input prompt
95
+ */
96
+ function password(message: string): PasswordResult {
97
+ return p.password({ message });
98
+ }
99
+
100
+ /**
101
+ * Intro banner
102
+ */
103
+ function intro(msg: string): void {
104
+ p.intro(chalk.bgCyan.black(` ${msg} `));
105
+ }
106
+
107
+ /**
108
+ * Outro banner
109
+ */
110
+ function outro(msg: string): void {
111
+ p.outro(chalk.green(msg));
112
+ }
113
+
114
+ /**
115
+ * Spinner for async operations
116
+ */
117
+ function spinner(): SpinnerInstance {
118
+ return p.spinner();
119
+ }
120
+
121
+ /**
122
+ * Cancel operation
123
+ */
124
+ function cancel(msg?: string): void {
125
+ p.cancel(msg ?? "Operation cancelled");
126
+ }
127
+
128
+ /**
129
+ * Note/info box
130
+ */
131
+ function note(message: string, title?: string): void {
132
+ p.note(message, title);
133
+ }
134
+
135
+ /**
136
+ * Log message within prompt flow
137
+ */
138
+ const log = {
139
+ info: (msg: string): void => {
140
+ p.log.info(msg);
141
+ },
142
+ success: (msg: string): void => {
143
+ p.log.success(msg);
144
+ },
145
+ warn: (msg: string): void => {
146
+ p.log.warn(msg);
147
+ },
148
+ error: (msg: string): void => {
149
+ p.log.error(msg);
150
+ },
151
+ message: (msg: string): void => {
152
+ p.log.message(msg);
153
+ },
154
+ step: (msg: string): void => {
155
+ p.log.step(msg);
156
+ },
157
+ };
158
+
159
+ /**
160
+ * Prompt utilities
161
+ */
162
+ export const prompts: Prompts = {
163
+ text: text,
164
+ confirm: confirm,
165
+ select: select,
166
+ multiselect: multiselect,
167
+ password: password,
168
+ group: p.group,
169
+ intro: intro,
170
+ outro: outro,
171
+ spinner: spinner,
172
+ log: log,
173
+ isCancel: p.isCancel,
174
+ cancel: cancel,
175
+ note: note,
176
+ };
177
+
178
+ /**
179
+ * Re-export for direct access
180
+ */
181
+ export { p as clack };
package/src/repl.ts ADDED
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Interactive REPL Mode
3
+ *
4
+ * Thin wrapper around the CLI. Loads procedures once,
5
+ * then executes each line through the same code path as `mark <command>`.
6
+ */
7
+
8
+ import * as readline from "node:readline";
9
+ import { print } from "./print.js";
10
+ import { initCli, executeArgs, type CliContext } from "./cli.js";
11
+
12
+ export interface ReplOptions {
13
+ /** Verbose output */
14
+ verbose?: boolean | undefined;
15
+ }
16
+
17
+ /**
18
+ * Start the interactive REPL
19
+ */
20
+ export async function startRepl(options: ReplOptions): Promise<void> {
21
+ const { verbose = false } = options;
22
+
23
+ // One-time initialization (same as CLI startup)
24
+ const spinner = print.spin("Loading procedures...");
25
+ const ctx: CliContext = await initCli(verbose);
26
+ spinner.succeed(`Ready (${ctx.procedures.length} procedures)`);
27
+
28
+ print.info("Type any command (without 'mark' prefix). Ctrl+C to exit.\n");
29
+
30
+ const reader = lineReader(process.stdin, process.stdout);
31
+ await runLines(reader.lines, (argv) => executeArgs(argv, ctx), reader.prompt);
32
+
33
+ print.info("\nGoodbye!");
34
+ // A piped script keeps the exit code of its failed commands. An interactive session exits with 0.
35
+ process.exit(process.stdin.isTTY ? 0 : (process.exitCode ?? 0));
36
+ }
37
+
38
+ /**
39
+ * The lines of an input, and a prompt that does nothing after the input ends. (A piped script
40
+ * ends its input while a command runs: a prompt after that threw "readline was closed".)
41
+ */
42
+ export function lineReader(
43
+ input: NodeJS.ReadableStream,
44
+ output: NodeJS.WritableStream
45
+ ): { lines: AsyncIterable<string>; prompt: () => void } {
46
+ const rl = readline.createInterface({ input, output, prompt: "mark> " });
47
+ let closed = false;
48
+ rl.on("close", () => {
49
+ closed = true;
50
+ });
51
+ return {
52
+ lines: rl,
53
+ prompt: () => {
54
+ if (!closed) {
55
+ rl.prompt();
56
+ }
57
+ },
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Run each line as a command, one at a time and in order: a command ends before the next line
63
+ * starts. The function returns after the last line, when the input ends. (Before, each line
64
+ * started at once, so the commands ran together, and a piped script stopped at its end before
65
+ * its commands were done: deep dive CLI-15.)
66
+ */
67
+ export async function runLines(
68
+ lines: AsyncIterable<string>,
69
+ execute: (argv: string[]) => Promise<void>,
70
+ prompt: () => void = () => {}
71
+ ): Promise<void> {
72
+ prompt();
73
+ for await (const line of lines) {
74
+ const input = line.trim();
75
+ if (input !== "") {
76
+ // Tokenize the same way the shell would, then run it as `mark <command>` does
77
+ await execute(tokenize(input));
78
+ }
79
+ prompt();
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Simple tokenizer that handles quoted strings. A quoted empty string ("") is a token.
85
+ */
86
+ export function tokenize(input: string): string[] {
87
+ const tokens: string[] = [];
88
+ let current = "";
89
+ let started = false;
90
+ let inQuote: string | null = null;
91
+
92
+ for (let i = 0; i < input.length; i++) {
93
+ const ch = input[i]!;
94
+
95
+ if (inQuote) {
96
+ if (ch === inQuote) {
97
+ inQuote = null;
98
+ } else {
99
+ current += ch;
100
+ }
101
+ } else if (ch === '"' || ch === "'") {
102
+ inQuote = ch;
103
+ started = true;
104
+ } else if (ch === " " || ch === "\t") {
105
+ if (started) {
106
+ tokens.push(current);
107
+ current = "";
108
+ started = false;
109
+ }
110
+ } else {
111
+ current += ch;
112
+ started = true;
113
+ }
114
+ }
115
+
116
+ if (started) {
117
+ tokens.push(current);
118
+ }
119
+
120
+ return tokens;
121
+ }
@@ -0,0 +1,205 @@
1
+ /**
2
+ * CLI Server Mode
3
+ *
4
+ * Runs the CLI as a persistent server, exposing all ecosystem procedures
5
+ * via HTTP. The server has the registry of mark (the module of each package, roadmap 3.1) and
6
+ * the host policy of the core (roadmap 2.3): the token in the lockfile.
7
+ */
8
+
9
+ import { print } from "./print.js";
10
+ import { loadEcosystemRegistry } from "./ecosystem.js";
11
+ import { randomBytes } from "node:crypto";
12
+ import { writeLockfile, removeLockfileForPort, getLockfilePath, currentBuild } from "./lockfile.js";
13
+
14
+ export interface ServerModeOptions {
15
+ port: number;
16
+ host?: string;
17
+ transport?: "http" | "websocket" | "both";
18
+ verbose?: boolean;
19
+ }
20
+
21
+ interface ServerCreateResult {
22
+ serverId: string;
23
+ endpoints: Array<{ type: string; address: string }>;
24
+ procedureCount: number;
25
+ }
26
+
27
+ /**
28
+ * Build transport configuration array
29
+ */
30
+ function buildTransports(options: ServerModeOptions) {
31
+ const transports: Array<{
32
+ type: "http" | "websocket";
33
+ port: number;
34
+ host: string;
35
+ basePath?: string;
36
+ path?: string;
37
+ }> = [];
38
+
39
+ // Loopback and no CORS by default: the server holds the whole registry, shell.* included.
40
+ // Before, it listened on 0.0.0.0 with Access-Control-Allow-Origin: * (deep dive CLI-1, TRN-1).
41
+ const { port, host = "127.0.0.1", transport = "http" } = options;
42
+
43
+ if (transport === "http" || transport === "both") {
44
+ transports.push({
45
+ type: "http",
46
+ port,
47
+ host,
48
+ basePath: "/api",
49
+ });
50
+ }
51
+
52
+ if (transport === "websocket" || transport === "both") {
53
+ transports.push({
54
+ type: "websocket",
55
+ port: transport === "both" ? port + 1 : port,
56
+ host,
57
+ path: "/ws",
58
+ });
59
+ }
60
+
61
+ return transports;
62
+ }
63
+
64
+ /**
65
+ * Start CLI in server mode
66
+ */
67
+ export async function startServerMode(options: ServerModeOptions): Promise<void> {
68
+ const { port = 3000, verbose = false } = options;
69
+
70
+ print.info("Starting CLI server mode...");
71
+
72
+ // The registry of mark (roadmap 3.1): the module of each workspace package, with the server
73
+ // procedures (client-server is in the workspace)
74
+ if (verbose) {
75
+ print.info("Loading ecosystem procedures...");
76
+ }
77
+ const { registry } = await loadEcosystemRegistry(verbose);
78
+
79
+ // Build transport config
80
+ const transports = buildTransports(options);
81
+
82
+ // Every request must send this token. Only the lockfile (readable by this user) holds it.
83
+ const token = randomBytes(32).toString("hex");
84
+ const host = options.host ?? "127.0.0.1";
85
+ if (!["127.0.0.1", "localhost", "::1"].includes(host)) {
86
+ print.warning(`The server listens on ${host}: other computers can reach it. Every request needs its token.`);
87
+ }
88
+ if (options.transport === "websocket") {
89
+ print.warning("A WebSocket-only server takes no `mark` commands: `mark` sends its commands over HTTP.");
90
+ }
91
+
92
+ // The host policy of the core (roadmap 2.3): the token of each transport. The server of mark
93
+ // serves every procedure of the workspace to its one user, so it has no expose list.
94
+ const { hostPolicy } = await import("@procedures/client");
95
+ const { createPeer } = await import("@procedures/peer");
96
+ const peer = await createPeer({
97
+ id: `peer-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
98
+ transports,
99
+ registry,
100
+ policy: hostPolicy({ token }),
101
+ });
102
+ await peer.start();
103
+ const result: ServerCreateResult = {
104
+ serverId: peer.id,
105
+ endpoints: peer.getEndpoints(),
106
+ procedureCount: peer.getServer().procedureCount,
107
+ };
108
+
109
+ // Write lockfile for client discovery. A client uses this server only from the same folder,
110
+ // with the same build of mark, and when the health endpoint answers with this peer id.
111
+ await writeLockfile({
112
+ pid: process.pid,
113
+ port,
114
+ transport: options.transport ?? "http",
115
+ endpoint: result.endpoints[0]?.address ?? `http://127.0.0.1:${port}/api`,
116
+ startedAt: new Date().toISOString(),
117
+ token,
118
+ cwd: process.cwd(),
119
+ build: currentBuild(),
120
+ peerId: result.serverId,
121
+ });
122
+
123
+ print.success("\nCLI Server running!");
124
+ print.info(` PID: ${process.pid}`);
125
+ print.info(` Server ID: ${result.serverId}`);
126
+ print.info(` Procedures: ${result.procedureCount}`);
127
+ print.info(` Lockfile: ${getLockfilePath()}`);
128
+ print.info("\nEndpoints:");
129
+ for (const endpoint of result.endpoints) {
130
+ print.info(` [${endpoint.type}] ${endpoint.address}`);
131
+ }
132
+ print.info("\nPress Ctrl+C to stop.");
133
+
134
+ // Handle shutdown
135
+ const cleanup = async () => {
136
+ print.info("\nStopping server...");
137
+ removeLockfileForPort(port);
138
+ process.exit(0);
139
+ };
140
+
141
+ process.on("SIGINT", cleanup);
142
+ process.on("SIGTERM", cleanup);
143
+
144
+ // Keep the process alive
145
+ await new Promise(() => {
146
+ // Never resolves - keeps process running
147
+ });
148
+ }
149
+
150
+ /**
151
+ * The port number of a `--port` value. An invalid port is an error (before, it became 3000:
152
+ * deep dive CLI-16).
153
+ */
154
+ export function parsePort(value: string): number {
155
+ const port = /^\d+$/.test(value) ? Number(value) : NaN;
156
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
157
+ throw new Error(`Invalid port: "${value}". Give a number from 1 to 65535.`);
158
+ }
159
+ return port;
160
+ }
161
+
162
+ /** The transports `--transport` accepts */
163
+ export const SERVER_TRANSPORTS = ["http", "websocket", "both"] as const;
164
+
165
+ /**
166
+ * The transport of a `--transport` value
167
+ */
168
+ export function parseTransport(value: string): "http" | "websocket" | "both" {
169
+ const transport = SERVER_TRANSPORTS.find((name) => name === value);
170
+ if (!transport) {
171
+ throw new Error(`Invalid transport: "${value}". Give one of: ${SERVER_TRANSPORTS.join(", ")}.`);
172
+ }
173
+ return transport;
174
+ }
175
+
176
+ /**
177
+ * Extract port from argv
178
+ */
179
+ export function extractPort(argv: string[]): number | null {
180
+ // --port 4000 and --port=4000 (before, the second form was ignored: deep dive CLI-1)
181
+ const inline = argv.find((arg) => arg.startsWith("--port="));
182
+ const portIdx = argv.indexOf("--port");
183
+ if (inline === undefined && portIdx === -1) return null;
184
+ const portValue = inline !== undefined ? inline.slice("--port=".length) : argv[portIdx + 1];
185
+ if (portValue === undefined) {
186
+ throw new Error("--port needs a value");
187
+ }
188
+ return parsePort(portValue);
189
+ }
190
+
191
+ /**
192
+ * Extract host from argv
193
+ */
194
+ export function extractHost(argv: string[]): string | null {
195
+ const inline = argv.find((arg) => arg.startsWith("--host="));
196
+ if (inline !== undefined) return inline.slice("--host=".length);
197
+ const hostIdx = argv.indexOf("--host");
198
+ if (hostIdx !== -1) {
199
+ const hostValue = argv[hostIdx + 1];
200
+ if (hostValue !== undefined) {
201
+ return hostValue;
202
+ }
203
+ }
204
+ return null;
205
+ }