@nonbot/cli 0.5.13
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 +35 -0
- package/LICENSE +43 -0
- package/README.md +93 -0
- package/dist/commands/daemon.js +286 -0
- package/dist/commands/doctor.js +262 -0
- package/dist/commands/login.js +112 -0
- package/dist/commands/logs.js +113 -0
- package/dist/commands/profiles.js +24 -0
- package/dist/commands/run.js +63 -0
- package/dist/commands/status.js +103 -0
- package/dist/commands/test.js +4 -0
- package/dist/index.js +135 -0
- package/dist/lib/activations.js +480 -0
- package/dist/lib/activity-log.js +48 -0
- package/dist/lib/auth.js +106 -0
- package/dist/lib/banner.js +94 -0
- package/dist/lib/command-builders.js +279 -0
- package/dist/lib/completion.js +67 -0
- package/dist/lib/output.js +382 -0
- package/dist/lib/payload-validator.js +174 -0
- package/dist/lib/service.js +145 -0
- package/dist/lib/terminal.js +313 -0
- package/dist/version.js +1 -0
- package/package.json +33 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import { ANSI, renderBannerHeader } from './banner.js';
|
|
2
|
+
export { ANSI };
|
|
3
|
+
export const BRAND = {
|
|
4
|
+
white: '\x1b[38;2;255;255;255m',
|
|
5
|
+
cyan: '\x1b[38;2;79;195;247m',
|
|
6
|
+
green: '\x1b[38;2;40;199;111m',
|
|
7
|
+
amber: '\x1b[38;2;255;159;67m',
|
|
8
|
+
red: '\x1b[38;2;255;76;81m',
|
|
9
|
+
muted: '\x1b[38;2;139;148;158m',
|
|
10
|
+
bold: '\x1b[1m',
|
|
11
|
+
reset: '\x1b[0m',
|
|
12
|
+
underline: '\x1b[4m',
|
|
13
|
+
};
|
|
14
|
+
export function isTTY(stream = process.stdout) {
|
|
15
|
+
const noColor = process.env.NO_COLOR;
|
|
16
|
+
if (noColor && noColor.length > 0)
|
|
17
|
+
return false;
|
|
18
|
+
const force = process.env.FORCE_COLOR;
|
|
19
|
+
if (force && force.length > 0)
|
|
20
|
+
return true;
|
|
21
|
+
const tty = stream.isTTY;
|
|
22
|
+
return tty === true;
|
|
23
|
+
}
|
|
24
|
+
export function paintRaw(s, code, stream = process.stdout) {
|
|
25
|
+
if (!isTTY(stream))
|
|
26
|
+
return s;
|
|
27
|
+
return code + s + BRAND.reset;
|
|
28
|
+
}
|
|
29
|
+
function paint(s, code, stream = process.stdout) {
|
|
30
|
+
return paintRaw(s, code, stream);
|
|
31
|
+
}
|
|
32
|
+
export const c = {
|
|
33
|
+
white: (s, stream) => paintRaw(s, BRAND.white, stream),
|
|
34
|
+
cyan: (s, stream) => paintRaw(s, BRAND.cyan, stream),
|
|
35
|
+
green: (s, stream) => paintRaw(s, BRAND.green, stream),
|
|
36
|
+
amber: (s, stream) => paintRaw(s, BRAND.amber, stream),
|
|
37
|
+
red: (s, stream) => paintRaw(s, BRAND.red, stream),
|
|
38
|
+
muted: (s, stream) => paintRaw(s, BRAND.muted, stream),
|
|
39
|
+
bold: (s, stream) => paintRaw(s, BRAND.bold, stream),
|
|
40
|
+
underline: (s, stream) => paintRaw(s, BRAND.underline, stream),
|
|
41
|
+
reset: () => BRAND.reset,
|
|
42
|
+
};
|
|
43
|
+
const STATUS_COLOR = {
|
|
44
|
+
'✓': ANSI.green,
|
|
45
|
+
'⚠': ANSI.yellow,
|
|
46
|
+
'✗': ANSI.red,
|
|
47
|
+
'▶': ANSI.primary,
|
|
48
|
+
'●': ANSI.green,
|
|
49
|
+
'ℹ': ANSI.cyan,
|
|
50
|
+
};
|
|
51
|
+
const SIGIL_ASCII = {
|
|
52
|
+
'✓': 'OK',
|
|
53
|
+
'⚠': '!!',
|
|
54
|
+
'✗': 'XX',
|
|
55
|
+
'▶': '>',
|
|
56
|
+
'●': '*',
|
|
57
|
+
'ℹ': 'i',
|
|
58
|
+
};
|
|
59
|
+
function asciiOnly(env = process.env) {
|
|
60
|
+
return env.NONBOT_ASCII_ONLY === '1' || env.NONBOT_ASCII_ONLY === 'true';
|
|
61
|
+
}
|
|
62
|
+
function sigil(status, stream = process.stdout) {
|
|
63
|
+
const char = asciiOnly() ? SIGIL_ASCII[status] : status;
|
|
64
|
+
return paint(char, STATUS_COLOR[status], stream);
|
|
65
|
+
}
|
|
66
|
+
export function header(title, subtitle, opts = {}) {
|
|
67
|
+
const stream = opts.stream ?? process.stdout;
|
|
68
|
+
const status = opts.status ?? '▶';
|
|
69
|
+
const sig = sigil(status, stream);
|
|
70
|
+
const titlePainted = paint(title, ANSI.bold, stream);
|
|
71
|
+
if (!subtitle)
|
|
72
|
+
return `${sig} ${titlePainted}`;
|
|
73
|
+
const sep = paint('·', ANSI.dim, stream);
|
|
74
|
+
const sub = paint(subtitle, ANSI.dim, stream);
|
|
75
|
+
return `${sig} ${titlePainted} ${sep} ${sub}`;
|
|
76
|
+
}
|
|
77
|
+
export function boxHeader(title, opts) {
|
|
78
|
+
return renderBannerHeader(title, opts);
|
|
79
|
+
}
|
|
80
|
+
export function kvRow(key, value, opts = {}) {
|
|
81
|
+
const stream = opts.stream ?? process.stdout;
|
|
82
|
+
const width = opts.keyWidth ?? 12;
|
|
83
|
+
const paddedKey = key.padEnd(width, ' ');
|
|
84
|
+
const keyPainted = paint(paddedKey, ANSI.cyan, stream);
|
|
85
|
+
const valuePainted = paint(value, ANSI.dim, stream);
|
|
86
|
+
return ` ${keyPainted} ${valuePainted}`;
|
|
87
|
+
}
|
|
88
|
+
export function statusRow(status, label, detail = '', opts = {}) {
|
|
89
|
+
const stream = opts.stream ?? process.stdout;
|
|
90
|
+
const width = opts.labelWidth ?? 18;
|
|
91
|
+
const sig = sigil(status, stream);
|
|
92
|
+
const paddedLabel = label.padEnd(width, ' ');
|
|
93
|
+
const labelPainted = paint(paddedLabel, ANSI.cyan, stream);
|
|
94
|
+
if (!detail)
|
|
95
|
+
return ` ${sig} ${labelPainted}`;
|
|
96
|
+
const detailPainted = paint(detail, ANSI.dim, stream);
|
|
97
|
+
return ` ${sig} ${labelPainted} ${detailPainted}`;
|
|
98
|
+
}
|
|
99
|
+
export function formatKV(rows, opts = {}) {
|
|
100
|
+
const maxKey = opts.maxKeyWidth ?? 18;
|
|
101
|
+
const longest = rows.reduce((m, [k]) => Math.max(m, k.length), 0);
|
|
102
|
+
const width = Math.min(longest, maxKey);
|
|
103
|
+
return rows.map(([k, v]) => kvRow(k, v, { keyWidth: width, stream: opts.stream }));
|
|
104
|
+
}
|
|
105
|
+
export function errorBlock(what, fix, opts = {}) {
|
|
106
|
+
const stream = opts.stream ?? process.stderr;
|
|
107
|
+
const head = statusRow('✗', what, '', { labelWidth: what.length, stream });
|
|
108
|
+
if (!fix)
|
|
109
|
+
return head + '\n';
|
|
110
|
+
const fixLabel = paint('Fix:', ANSI.cyan, stream);
|
|
111
|
+
const fixText = paint(fix, ANSI.dim, stream);
|
|
112
|
+
return head + '\n ' + fixLabel + ' ' + fixText + '\n';
|
|
113
|
+
}
|
|
114
|
+
export function eventRow(parts, opts = {}) {
|
|
115
|
+
const stream = opts.stream ?? process.stdout;
|
|
116
|
+
const status = opts.status ?? '▶';
|
|
117
|
+
const sig = sigil(status, stream);
|
|
118
|
+
const sep = paint('·', ANSI.dim, stream);
|
|
119
|
+
const painted = parts.map((p, i) => {
|
|
120
|
+
if (i === 0)
|
|
121
|
+
return p;
|
|
122
|
+
return paint(p, ANSI.dim, stream);
|
|
123
|
+
});
|
|
124
|
+
return `${sig} ` + painted.join(` ${sep} `);
|
|
125
|
+
}
|
|
126
|
+
export function clockTime(d = new Date()) {
|
|
127
|
+
const pad = (n) => (n < 10 ? '0' + n : String(n));
|
|
128
|
+
return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
129
|
+
}
|
|
130
|
+
const WORDMARK_LINES = [
|
|
131
|
+
'███╗ ██╗ ██████╗ ███╗ ██╗ ██████╗ ██████╗ ████████╗',
|
|
132
|
+
'████╗ ██║ ██╔═══██╗ ████╗ ██║ ██╔══██╗ ██╔═══██╗ ╚══██╔══╝',
|
|
133
|
+
'██╔██╗ ██║ ██║ ██║ ██╔██╗ ██║ ██████╔╝ ██║ ██║ ██║ ',
|
|
134
|
+
'██║╚██╗██║ ██║ ██║ ██║╚██╗██║ ██╔══██╗ ██║ ██║ ██║ ',
|
|
135
|
+
'██║ ╚████║ ╚██████╔╝ ██║ ╚████║██╗██████╔╝ ╚██████╔╝ ██║ ',
|
|
136
|
+
'╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝╚═════╝ ╚═════╝ ╚═╝ ',
|
|
137
|
+
];
|
|
138
|
+
export const WORDMARK_WIDTH = 63;
|
|
139
|
+
export const WORDMARK_HEIGHT = 6;
|
|
140
|
+
export function terminalWidth(width) {
|
|
141
|
+
const cols = width ?? (process.stdout && process.stdout.columns) ?? 80;
|
|
142
|
+
const min = WORDMARK_WIDTH + 6;
|
|
143
|
+
const max = 200;
|
|
144
|
+
if (cols < min)
|
|
145
|
+
return min;
|
|
146
|
+
if (cols > max)
|
|
147
|
+
return max;
|
|
148
|
+
return cols;
|
|
149
|
+
}
|
|
150
|
+
function trackedCaps(s) {
|
|
151
|
+
return s.split('').join(' ');
|
|
152
|
+
}
|
|
153
|
+
export function wordmark(version, opts = {}) {
|
|
154
|
+
const stream = opts.stream ?? process.stdout;
|
|
155
|
+
const state = opts.state;
|
|
156
|
+
const headLines = WORDMARK_LINES.map((line) => c.bold(c.white(line, stream), stream));
|
|
157
|
+
const parts = [c.cyan(trackedCaps('DAEMON'), stream)];
|
|
158
|
+
if (version) {
|
|
159
|
+
parts.push(c.muted('·', stream));
|
|
160
|
+
parts.push(c.muted(version, stream));
|
|
161
|
+
}
|
|
162
|
+
if (state) {
|
|
163
|
+
parts.push(c.muted('·', stream));
|
|
164
|
+
parts.push(c.muted(trackedCaps(state), stream));
|
|
165
|
+
}
|
|
166
|
+
const subtitle = parts.join(' ');
|
|
167
|
+
return [...headLines, '', subtitle].join('\n');
|
|
168
|
+
}
|
|
169
|
+
export function visibleLength(s) {
|
|
170
|
+
return s.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '').length;
|
|
171
|
+
}
|
|
172
|
+
export function renderWordmarkWithBadge(opts = {}) {
|
|
173
|
+
const stream = opts.stream ?? process.stdout;
|
|
174
|
+
const cols = terminalWidth(opts.width);
|
|
175
|
+
const color = opts.badgeColor ?? 'green';
|
|
176
|
+
const label = opts.badgeLabel ?? 'CONNECTED';
|
|
177
|
+
const sublines = opts.badgeSublines ?? [];
|
|
178
|
+
const dotColored = {
|
|
179
|
+
green: (s) => c.green(s, stream),
|
|
180
|
+
amber: (s) => c.amber(s, stream),
|
|
181
|
+
red: (s) => c.red(s, stream),
|
|
182
|
+
muted: (s) => c.muted(s, stream),
|
|
183
|
+
};
|
|
184
|
+
const minGap = 2;
|
|
185
|
+
const rightColWidth = cols - WORDMARK_WIDTH - minGap;
|
|
186
|
+
const headVisible = `● ${label}`;
|
|
187
|
+
const headRaw = `${dotColored[color]('●')} ${dotColored[color](label)}`;
|
|
188
|
+
const inlineSublines = [];
|
|
189
|
+
const overflowSublines = [];
|
|
190
|
+
for (const s of sublines) {
|
|
191
|
+
if (s.length <= rightColWidth && inlineSublines.length < WORDMARK_HEIGHT - 1) {
|
|
192
|
+
inlineSublines.push(s);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
overflowSublines.push(s);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const headFits = headVisible.length <= rightColWidth;
|
|
199
|
+
if (!headFits) {
|
|
200
|
+
overflowSublines.unshift(`${label}`);
|
|
201
|
+
}
|
|
202
|
+
const rightRowsVisible = [];
|
|
203
|
+
const rightRowsRaw = [];
|
|
204
|
+
if (headFits) {
|
|
205
|
+
rightRowsVisible.push(headVisible);
|
|
206
|
+
rightRowsRaw.push(headRaw);
|
|
207
|
+
}
|
|
208
|
+
for (const s of inlineSublines) {
|
|
209
|
+
rightRowsVisible.push(s);
|
|
210
|
+
rightRowsRaw.push(c.muted(s, stream));
|
|
211
|
+
}
|
|
212
|
+
const lines = [];
|
|
213
|
+
for (let r = 0; r < WORDMARK_LINES.length; r++) {
|
|
214
|
+
const wm = WORDMARK_LINES[r];
|
|
215
|
+
const wmPainted = c.bold(c.white(wm, stream), stream);
|
|
216
|
+
if (r >= rightRowsRaw.length) {
|
|
217
|
+
lines.push(wmPainted);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const rightRaw = rightRowsRaw[r];
|
|
221
|
+
const rightVisible = rightRowsVisible[r] ?? '';
|
|
222
|
+
const wmVisibleLen = wm.length;
|
|
223
|
+
const rightLen = rightVisible.length;
|
|
224
|
+
const gap = Math.max(minGap, cols - wmVisibleLen - rightLen);
|
|
225
|
+
lines.push(wmPainted + ' '.repeat(gap) + rightRaw);
|
|
226
|
+
}
|
|
227
|
+
for (const s of overflowSublines) {
|
|
228
|
+
lines.push(` ${c.muted(s, stream)}`);
|
|
229
|
+
}
|
|
230
|
+
return lines.join('\n');
|
|
231
|
+
}
|
|
232
|
+
export function statusBadge(color, label, sublines = [], opts = {}) {
|
|
233
|
+
const stream = opts.stream ?? process.stdout;
|
|
234
|
+
const dotColored = {
|
|
235
|
+
green: (s) => c.green(s, stream),
|
|
236
|
+
amber: (s) => c.amber(s, stream),
|
|
237
|
+
red: (s) => c.red(s, stream),
|
|
238
|
+
muted: (s) => c.muted(s, stream),
|
|
239
|
+
};
|
|
240
|
+
const dot = dotColored[color]('●');
|
|
241
|
+
const head = `${dot} ${dotColored[color](trackedCaps(label))}`;
|
|
242
|
+
if (sublines.length === 0)
|
|
243
|
+
return head;
|
|
244
|
+
const indented = sublines.map((s) => ` ${c.muted(s, stream)}`);
|
|
245
|
+
return [head, ...indented].join('\n');
|
|
246
|
+
}
|
|
247
|
+
function bracketMarker(marker) {
|
|
248
|
+
return `[ ${marker} ]`;
|
|
249
|
+
}
|
|
250
|
+
function colorize(s, color, stream) {
|
|
251
|
+
const map = {
|
|
252
|
+
green: (s) => c.green(s, stream),
|
|
253
|
+
amber: (s) => c.amber(s, stream),
|
|
254
|
+
red: (s) => c.red(s, stream),
|
|
255
|
+
muted: (s) => c.muted(s, stream),
|
|
256
|
+
};
|
|
257
|
+
return map[color](s);
|
|
258
|
+
}
|
|
259
|
+
export function activationCard(args) {
|
|
260
|
+
const stream = args.stream ?? process.stdout;
|
|
261
|
+
const marker = colorize(bracketMarker(args.marker), args.color, stream);
|
|
262
|
+
const id = c.bold(c.white(args.id, stream), stream);
|
|
263
|
+
const sep = c.muted('·', stream);
|
|
264
|
+
const suffix = colorize(args.headerSuffix, args.color, stream);
|
|
265
|
+
const head = `${marker} ${id} ${sep} ${suffix}`;
|
|
266
|
+
if (args.kv.length === 0)
|
|
267
|
+
return head;
|
|
268
|
+
const LABEL_WIDTH = 9;
|
|
269
|
+
const rows = args.kv.map(([k, v]) => {
|
|
270
|
+
const label = c.cyan(k.padEnd(LABEL_WIDTH, ' '), stream);
|
|
271
|
+
const value = c.white(v, stream);
|
|
272
|
+
return ` ${label} ${value}`;
|
|
273
|
+
});
|
|
274
|
+
return [head, ...rows].join('\n');
|
|
275
|
+
}
|
|
276
|
+
function formatMsToS(ms) {
|
|
277
|
+
if (ms < 1000)
|
|
278
|
+
return `${ms}ms`;
|
|
279
|
+
return `${Math.round(ms / 1000)}s`;
|
|
280
|
+
}
|
|
281
|
+
export function pollTick(args) {
|
|
282
|
+
const stream = args.stream ?? process.stdout;
|
|
283
|
+
const ts = c.cyan(clockTime(args.now ?? new Date()), stream);
|
|
284
|
+
const pollCount = args.pending > 0
|
|
285
|
+
? c.green(`poll +${args.pending}`, stream)
|
|
286
|
+
: c.white(`poll +${args.pending}`, stream);
|
|
287
|
+
const sep = c.muted('·', stream);
|
|
288
|
+
const sleepText = args.sleepMs !== undefined
|
|
289
|
+
? `sleep ${formatMsToS(args.sleepMs)}`
|
|
290
|
+
: 'backing off';
|
|
291
|
+
const sleepSeg = args.backingOff
|
|
292
|
+
? c.amber(sleepText, stream)
|
|
293
|
+
: c.muted(sleepText, stream);
|
|
294
|
+
const statusText = args.status ?? (args.pending > 0
|
|
295
|
+
? `fired ${args.pending}`
|
|
296
|
+
: 'no new activations');
|
|
297
|
+
const statusSeg = c.muted(statusText, stream);
|
|
298
|
+
return `${ts} ${pollCount} ${sep} ${sleepSeg} ${sep} ${statusSeg}`;
|
|
299
|
+
}
|
|
300
|
+
export function fixBlock(args) {
|
|
301
|
+
const stream = args.stream ?? process.stdout;
|
|
302
|
+
const title = c.amber(`| ${args.title.toUpperCase()} |`, stream);
|
|
303
|
+
const lines = [title];
|
|
304
|
+
args.steps.forEach((step, i) => {
|
|
305
|
+
const num = c.muted(`${i + 1}.`, stream);
|
|
306
|
+
const text = c.white(step.text, stream);
|
|
307
|
+
let line = `${num} ${text}`;
|
|
308
|
+
if (step.url) {
|
|
309
|
+
const url = c.underline(c.cyan(step.url, stream), stream);
|
|
310
|
+
line += ` ${url}`;
|
|
311
|
+
}
|
|
312
|
+
if (step.detail) {
|
|
313
|
+
line += ` ${c.muted(step.detail, stream)}`;
|
|
314
|
+
}
|
|
315
|
+
lines.push(line);
|
|
316
|
+
});
|
|
317
|
+
return lines.join('\n');
|
|
318
|
+
}
|
|
319
|
+
export const DAEMON_TAGLINES = [
|
|
320
|
+
'wired in. every Run lands here.',
|
|
321
|
+
"the canvas thinks. this runs. that's the whole deal.",
|
|
322
|
+
"click ▶ on the canvas. terminal opens here. that's it.",
|
|
323
|
+
'listening for chevrons. it\'s go time.',
|
|
324
|
+
'halfway between intent and shipped. ready when you are.',
|
|
325
|
+
'between the canvas and your terminal — door\'s open.',
|
|
326
|
+
'you click, this catches. no rituals required.',
|
|
327
|
+
'standing by. nothing fancy, just nothing missed.',
|
|
328
|
+
];
|
|
329
|
+
export function pickDaemonTagline(seed, pool = DAEMON_TAGLINES) {
|
|
330
|
+
const n = pool.length;
|
|
331
|
+
if (n === 0)
|
|
332
|
+
return '';
|
|
333
|
+
const idx = Math.abs(Math.trunc(seed)) % n;
|
|
334
|
+
return pool[idx];
|
|
335
|
+
}
|
|
336
|
+
export function daemonOpener(version, opts = {}) {
|
|
337
|
+
const stream = opts.stream ?? process.stdout;
|
|
338
|
+
const seed = opts.seed ?? Date.now();
|
|
339
|
+
const tagline = pickDaemonTagline(seed, opts.pool);
|
|
340
|
+
const state = opts.state ?? 'IDLE';
|
|
341
|
+
const breadcrumb = opts.breadcrumb ?? 'SYS://nonbot/daemon';
|
|
342
|
+
const breadcrumbLine = breadcrumb
|
|
343
|
+
? ` ${c.cyan(breadcrumb, stream)}\n\n`
|
|
344
|
+
: '';
|
|
345
|
+
const stamp = renderWordmarkWithBadge({
|
|
346
|
+
badgeColor: opts.badgeColor ?? 'green',
|
|
347
|
+
badgeLabel: opts.badgeLabel ?? 'CONNECTED',
|
|
348
|
+
badgeSublines: opts.badgeSublines ?? [],
|
|
349
|
+
width: opts.width,
|
|
350
|
+
stream,
|
|
351
|
+
});
|
|
352
|
+
const subParts = [c.cyan(trackedCaps('DAEMON'), stream)];
|
|
353
|
+
if (version) {
|
|
354
|
+
subParts.push(c.muted('·', stream));
|
|
355
|
+
subParts.push(c.muted(version, stream));
|
|
356
|
+
}
|
|
357
|
+
if (state) {
|
|
358
|
+
subParts.push(c.muted('·', stream));
|
|
359
|
+
subParts.push(c.muted(trackedCaps(state), stream));
|
|
360
|
+
}
|
|
361
|
+
const subtitleLine = ` ${subParts.join(' ')}`;
|
|
362
|
+
const taglineLine = tagline ? `\n\n ${c.muted(tagline, stream)}` : '';
|
|
363
|
+
return `${breadcrumbLine}${stamp}\n\n${subtitleLine}${taglineLine}`;
|
|
364
|
+
}
|
|
365
|
+
export function daemonCloser(opts = {}) {
|
|
366
|
+
const stream = opts.stream ?? process.stdout;
|
|
367
|
+
const pulse = c.green('░▒▓█', stream);
|
|
368
|
+
const wiredLabel = opts.activeCount && opts.activeCount > 0
|
|
369
|
+
? `wired · ${opts.activeCount} active`
|
|
370
|
+
: 'wired';
|
|
371
|
+
const wired = c.muted(wiredLabel, stream);
|
|
372
|
+
const sep = c.muted('·', stream);
|
|
373
|
+
const ctrlC = c.cyan('^C', stream);
|
|
374
|
+
const stopHint = c.muted('to stop daemon', stream);
|
|
375
|
+
const parts = [`${pulse} ${wired}`, ` ${sep} `, `${ctrlC} ${stopHint}`];
|
|
376
|
+
if (opts.inTmux) {
|
|
377
|
+
const ctrlBd = c.cyan('^B d', stream);
|
|
378
|
+
const detachHint = c.muted('to detach tmux', stream);
|
|
379
|
+
parts.push(` ${sep} `, `${ctrlBd} ${detachHint}`);
|
|
380
|
+
}
|
|
381
|
+
return parts.join('');
|
|
382
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { PROVIDER_PROFILES, PROVIDER_TEST_PROFILES } from './command-builders.js';
|
|
2
|
+
export class ValidationError extends Error {
|
|
3
|
+
field;
|
|
4
|
+
reason;
|
|
5
|
+
constructor(field, reason) {
|
|
6
|
+
super(`invalid payload (${field}): ${reason}`);
|
|
7
|
+
this.name = 'ValidationError';
|
|
8
|
+
this.field = field;
|
|
9
|
+
this.reason = reason;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const ACTIVATION_ID_RE = /^act_[a-f0-9]{8,32}$/i;
|
|
13
|
+
const STORY_TITLE_MAX = 200;
|
|
14
|
+
const AGENTS_MD_MAX = 24 * 1024;
|
|
15
|
+
const REPO_PATH_MAX = 1024;
|
|
16
|
+
const AGENTS_MD_HEREDOC_DELIMITER = 'NONBOT_AGENTS_EOF';
|
|
17
|
+
const THEME_ID_MAX = 128;
|
|
18
|
+
const THEME_NAME_MAX = 128;
|
|
19
|
+
const THEME_TMUX_CONF_MAX = 8 * 1024;
|
|
20
|
+
const THEME_ITERM_JSON_MAX = 16 * 1024;
|
|
21
|
+
export function validateActivationId(s) {
|
|
22
|
+
if (typeof s !== 'string') {
|
|
23
|
+
throw new ValidationError('activationId', 'must be a string');
|
|
24
|
+
}
|
|
25
|
+
if (!ACTIVATION_ID_RE.test(s)) {
|
|
26
|
+
throw new ValidationError('activationId', `must match /^act_[a-f0-9]{8,32}$/i (got ${JSON.stringify(s.slice(0, 64))})`);
|
|
27
|
+
}
|
|
28
|
+
return s;
|
|
29
|
+
}
|
|
30
|
+
export function validateRepoPath(s, opts = {}) {
|
|
31
|
+
if (s === undefined || s === null || s === '') {
|
|
32
|
+
if (opts.allowEmpty)
|
|
33
|
+
return '';
|
|
34
|
+
throw new ValidationError('repoPath', 'is required');
|
|
35
|
+
}
|
|
36
|
+
if (typeof s !== 'string') {
|
|
37
|
+
throw new ValidationError('repoPath', 'must be a string');
|
|
38
|
+
}
|
|
39
|
+
if (s.length > REPO_PATH_MAX) {
|
|
40
|
+
throw new ValidationError('repoPath', `exceeds ${REPO_PATH_MAX} chars`);
|
|
41
|
+
}
|
|
42
|
+
if (!s.startsWith('/')) {
|
|
43
|
+
throw new ValidationError('repoPath', 'must be an absolute POSIX path');
|
|
44
|
+
}
|
|
45
|
+
if (s.includes('..')) {
|
|
46
|
+
throw new ValidationError('repoPath', 'must not contain ".."');
|
|
47
|
+
}
|
|
48
|
+
if (/[\n\r\0;`$"\\]/.test(s)) {
|
|
49
|
+
throw new ValidationError('repoPath', 'contains shell metacharacters');
|
|
50
|
+
}
|
|
51
|
+
return s;
|
|
52
|
+
}
|
|
53
|
+
export function validateStoryTitle(s) {
|
|
54
|
+
if (s === undefined || s === null)
|
|
55
|
+
return undefined;
|
|
56
|
+
if (typeof s !== 'string') {
|
|
57
|
+
throw new ValidationError('storyTitle', 'must be a string');
|
|
58
|
+
}
|
|
59
|
+
if (s.length > STORY_TITLE_MAX) {
|
|
60
|
+
throw new ValidationError('storyTitle', `exceeds ${STORY_TITLE_MAX} chars`);
|
|
61
|
+
}
|
|
62
|
+
if (/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(s)) {
|
|
63
|
+
throw new ValidationError('storyTitle', 'contains control bytes');
|
|
64
|
+
}
|
|
65
|
+
return s;
|
|
66
|
+
}
|
|
67
|
+
export function validateAgentsMd(s) {
|
|
68
|
+
if (s === undefined || s === null || s === '')
|
|
69
|
+
return undefined;
|
|
70
|
+
if (typeof s !== 'string') {
|
|
71
|
+
throw new ValidationError('agentsMd', 'must be a string');
|
|
72
|
+
}
|
|
73
|
+
if (s.length > AGENTS_MD_MAX) {
|
|
74
|
+
throw new ValidationError('agentsMd', `exceeds ${AGENTS_MD_MAX} bytes`);
|
|
75
|
+
}
|
|
76
|
+
const heredocLine = new RegExp(`(^|\\n)\\s*${AGENTS_MD_HEREDOC_DELIMITER}\\s*(\\n|$)`);
|
|
77
|
+
if (heredocLine.test(s)) {
|
|
78
|
+
throw new ValidationError('agentsMd', `contains the heredoc-closing literal "${AGENTS_MD_HEREDOC_DELIMITER}" on its own line`);
|
|
79
|
+
}
|
|
80
|
+
if (s.includes('\0')) {
|
|
81
|
+
throw new ValidationError('agentsMd', 'contains NUL bytes');
|
|
82
|
+
}
|
|
83
|
+
return s;
|
|
84
|
+
}
|
|
85
|
+
export function validateProvider(s) {
|
|
86
|
+
if (typeof s !== 'string') {
|
|
87
|
+
throw new ValidationError('provider', 'must be a string');
|
|
88
|
+
}
|
|
89
|
+
if (!(s in PROVIDER_PROFILES)) {
|
|
90
|
+
throw new ValidationError('provider', `not in allowlist (${Object.keys(PROVIDER_PROFILES).join(', ')})`);
|
|
91
|
+
}
|
|
92
|
+
return s;
|
|
93
|
+
}
|
|
94
|
+
export function validateTerminalTheme(raw) {
|
|
95
|
+
if (raw === undefined || raw === null)
|
|
96
|
+
return null;
|
|
97
|
+
if (typeof raw !== 'object' || Array.isArray(raw))
|
|
98
|
+
return null;
|
|
99
|
+
const t = raw;
|
|
100
|
+
if (typeof t.id !== 'string' || t.id.length === 0 || t.id.length > THEME_ID_MAX)
|
|
101
|
+
return null;
|
|
102
|
+
if (typeof t.name !== 'string' || t.name.length === 0 || t.name.length > THEME_NAME_MAX)
|
|
103
|
+
return null;
|
|
104
|
+
if (/[\x00-\x1f\x7f]/.test(t.name))
|
|
105
|
+
return null;
|
|
106
|
+
if (typeof t.tmuxConf !== 'string' || t.tmuxConf.length > THEME_TMUX_CONF_MAX)
|
|
107
|
+
return null;
|
|
108
|
+
if (typeof t.itermProfileJson !== 'string' || t.itermProfileJson.length > THEME_ITERM_JSON_MAX)
|
|
109
|
+
return null;
|
|
110
|
+
return {
|
|
111
|
+
id: t.id,
|
|
112
|
+
name: t.name,
|
|
113
|
+
tmuxConf: t.tmuxConf,
|
|
114
|
+
itermProfileJson: t.itermProfileJson,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
export function validateProviderTest(s) {
|
|
118
|
+
if (typeof s !== 'string') {
|
|
119
|
+
throw new ValidationError('provider', 'must be a string');
|
|
120
|
+
}
|
|
121
|
+
if (!(s in PROVIDER_TEST_PROFILES)) {
|
|
122
|
+
throw new ValidationError('provider', `not in test allowlist (${Object.keys(PROVIDER_TEST_PROFILES).join(', ')})`);
|
|
123
|
+
}
|
|
124
|
+
return s;
|
|
125
|
+
}
|
|
126
|
+
export function extractTerminalTheme(rawPayload) {
|
|
127
|
+
if (!rawPayload || typeof rawPayload !== 'object')
|
|
128
|
+
return null;
|
|
129
|
+
const p = rawPayload;
|
|
130
|
+
return validateTerminalTheme(p.terminalTheme);
|
|
131
|
+
}
|
|
132
|
+
export function validatePayload(raw) {
|
|
133
|
+
if (!raw || typeof raw !== 'object') {
|
|
134
|
+
throw new ValidationError('payload', 'must be an object');
|
|
135
|
+
}
|
|
136
|
+
const p = raw;
|
|
137
|
+
const template = p.template;
|
|
138
|
+
if (typeof template !== 'string') {
|
|
139
|
+
throw new ValidationError('template', 'must be a string');
|
|
140
|
+
}
|
|
141
|
+
switch (template) {
|
|
142
|
+
case 'diagnostic': {
|
|
143
|
+
return {
|
|
144
|
+
template: 'diagnostic',
|
|
145
|
+
activationId: validateActivationId(p.activationId),
|
|
146
|
+
repoPath: validateRepoPath(p.repoPath, { allowEmpty: true }),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
case 'provider-test': {
|
|
150
|
+
return {
|
|
151
|
+
template: 'provider-test',
|
|
152
|
+
provider: validateProviderTest(p.provider),
|
|
153
|
+
activationId: validateActivationId(p.activationId),
|
|
154
|
+
repoPath: validateRepoPath(p.repoPath, { allowEmpty: true }),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
case 'real': {
|
|
158
|
+
return {
|
|
159
|
+
template: 'real',
|
|
160
|
+
provider: validateProvider(p.provider),
|
|
161
|
+
activationId: validateActivationId(p.activationId),
|
|
162
|
+
repoPath: validateRepoPath(p.repoPath),
|
|
163
|
+
storyTitle: validateStoryTitle(p.storyTitle),
|
|
164
|
+
agentsMd: validateAgentsMd(p.agentsMd),
|
|
165
|
+
perStoryOverride: p.perStoryOverride === true || p.perStoryOverride === false
|
|
166
|
+
? p.perStoryOverride
|
|
167
|
+
: undefined,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
default: {
|
|
171
|
+
throw new ValidationError('template', `must be one of: diagnostic, provider-test, real (got ${JSON.stringify(template)})`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|