@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.
- package/CHANGELOG.md +5 -0
- package/LICENSE +21 -0
- package/README.md +799 -2
- package/dist/.tsbuildinfo +1 -0
- package/dist/args.d.ts +77 -0
- package/dist/args.d.ts.map +1 -0
- package/dist/args.js +351 -0
- package/dist/args.js.map +1 -0
- package/dist/ask.d.ts +48 -0
- package/dist/ask.d.ts.map +1 -0
- package/dist/ask.js +91 -0
- package/dist/ask.js.map +1 -0
- package/dist/bin.d.ts +9 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +10 -0
- package/dist/bin.js.map +1 -0
- package/dist/cli.d.ts +61 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +530 -0
- package/dist/cli.js.map +1 -0
- package/dist/client-mode.d.ts +21 -0
- package/dist/client-mode.d.ts.map +1 -0
- package/dist/client-mode.js +75 -0
- package/dist/client-mode.js.map +1 -0
- package/dist/ecosystem.d.ts +66 -0
- package/dist/ecosystem.d.ts.map +1 -0
- package/dist/ecosystem.js +211 -0
- package/dist/ecosystem.js.map +1 -0
- package/dist/failure.d.ts +16 -0
- package/dist/failure.d.ts.map +1 -0
- package/dist/failure.js +47 -0
- package/dist/failure.js.map +1 -0
- package/dist/format.d.ts +30 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +125 -0
- package/dist/format.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/lockfile.d.ts +26 -0
- package/dist/lockfile.d.ts.map +1 -0
- package/dist/lockfile.js +68 -0
- package/dist/lockfile.js.map +1 -0
- package/dist/manifest-build.d.ts +7 -0
- package/dist/manifest-build.d.ts.map +1 -0
- package/dist/manifest-build.js +11 -0
- package/dist/manifest-build.js.map +1 -0
- package/dist/manifest.d.ts +57 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +140 -0
- package/dist/manifest.js.map +1 -0
- package/dist/manifest.json +20885 -0
- package/dist/parse.d.ts +143 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +509 -0
- package/dist/parse.js.map +1 -0
- package/dist/print.d.ts +42 -0
- package/dist/print.d.ts.map +1 -0
- package/dist/print.js +80 -0
- package/dist/print.js.map +1 -0
- package/dist/prompts.d.ts +58 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +124 -0
- package/dist/prompts.js.map +1 -0
- package/dist/repl.d.ts +34 -0
- package/dist/repl.d.ts.map +1 -0
- package/dist/repl.js +101 -0
- package/dist/repl.js.map +1 -0
- package/dist/server-mode.d.ts +37 -0
- package/dist/server-mode.d.ts.map +1 -0
- package/dist/server-mode.js +167 -0
- package/dist/server-mode.js.map +1 -0
- package/package.json +97 -3
- package/src/args.ts +440 -0
- package/src/ask.ts +119 -0
- package/src/bin.ts +11 -0
- package/src/cli.ts +596 -0
- package/src/client-mode.ts +90 -0
- package/src/ecosystem.ts +258 -0
- package/src/failure.ts +47 -0
- package/src/format.ts +177 -0
- package/src/index.ts +31 -0
- package/src/lockfile.ts +85 -0
- package/src/manifest-build.ts +12 -0
- package/src/manifest.ts +162 -0
- package/src/parse.ts +670 -0
- package/src/print.ts +108 -0
- package/src/prompts.ts +181 -0
- package/src/repl.ts +121 -0
- 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
|
+
}
|