@zenera/cli 1.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/LICENSE +21 -0
- package/README.md +239 -0
- package/dist/args.d.ts +40 -0
- package/dist/args.js +99 -0
- package/dist/audit.d.ts +53 -0
- package/dist/audit.js +144 -0
- package/dist/banner.d.ts +13 -0
- package/dist/banner.js +103 -0
- package/dist/command.d.ts +14 -0
- package/dist/command.js +12 -0
- package/dist/commands/check.d.ts +3 -0
- package/dist/commands/check.js +287 -0
- package/dist/commands/index.d.ts +22 -0
- package/dist/commands/index.js +56 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.js +157 -0
- package/dist/commands/inspect.d.ts +3 -0
- package/dist/commands/inspect.js +158 -0
- package/dist/commands/key.d.ts +3 -0
- package/dist/commands/key.js +335 -0
- package/dist/commands/list.d.ts +3 -0
- package/dist/commands/list.js +101 -0
- package/dist/commands/models.d.ts +9 -0
- package/dist/commands/models.js +120 -0
- package/dist/commands/open.d.ts +9 -0
- package/dist/commands/open.js +270 -0
- package/dist/commands/run.d.ts +3 -0
- package/dist/commands/run.js +167 -0
- package/dist/commands/sandbox.d.ts +3 -0
- package/dist/commands/sandbox.js +112 -0
- package/dist/commands/version.d.ts +6 -0
- package/dist/commands/version.js +39 -0
- package/dist/engine.d.ts +49 -0
- package/dist/engine.js +208 -0
- package/dist/external.d.ts +10 -0
- package/dist/external.js +56 -0
- package/dist/home.d.ts +31 -0
- package/dist/home.js +108 -0
- package/dist/ids.d.ts +12 -0
- package/dist/ids.js +44 -0
- package/dist/keys.d.ts +124 -0
- package/dist/keys.js +309 -0
- package/dist/lib.d.ts +9 -0
- package/dist/lib.js +31 -0
- package/dist/liveness.d.ts +23 -0
- package/dist/liveness.js +221 -0
- package/dist/main.d.ts +3 -0
- package/dist/main.js +155 -0
- package/dist/narrate.d.ts +19 -0
- package/dist/narrate.js +124 -0
- package/dist/podman.d.ts +46 -0
- package/dist/podman.js +254 -0
- package/dist/projects.d.ts +70 -0
- package/dist/projects.js +232 -0
- package/dist/resolve.d.ts +27 -0
- package/dist/resolve.js +138 -0
- package/dist/sandbox.d.ts +36 -0
- package/dist/sandbox.js +104 -0
- package/dist/scaffold.d.ts +29 -0
- package/dist/scaffold.js +220 -0
- package/dist/session.d.ts +77 -0
- package/dist/session.js +156 -0
- package/dist/term.d.ts +69 -0
- package/dist/term.js +242 -0
- package/dist/tui/app.d.ts +8 -0
- package/dist/tui/app.js +257 -0
- package/dist/tui/theme.d.ts +23 -0
- package/dist/tui/theme.js +134 -0
- package/dist/tui/wrap.d.ts +12 -0
- package/dist/tui/wrap.js +62 -0
- package/dist/validate.d.ts +145 -0
- package/dist/validate.js +959 -0
- package/package.json +76 -0
- package/templates/.github/copilot-instructions.md +1579 -0
- package/templates/.github/prompts/new-agent.prompt.md +38 -0
- package/templates/.github/prompts/new-skill.prompt.md +37 -0
- package/templates/.github/prompts/review-project.prompt.md +31 -0
- package/templates/.github/skills/zen-cli/SKILL.md +110 -0
package/dist/session.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { hostname } from 'node:os';
|
|
3
|
+
import { join, relative, resolve } from 'node:path';
|
|
4
|
+
import { readJson, writeJson } from "./home.js";
|
|
5
|
+
import { isStamp, stamp } from "./ids.js";
|
|
6
|
+
import { alive, isBusy, runIds, sessionIds, sessionsDir } from "./projects.js";
|
|
7
|
+
import { CliError, EXIT, invalidError, usageError } from "./term.js";
|
|
8
|
+
export function sessionPaths(projectDir, id) {
|
|
9
|
+
if (!isStamp(id)) {
|
|
10
|
+
throw usageError(`"${id}" is not a session id`, 'ids look like 20260825-143012-a7f3');
|
|
11
|
+
}
|
|
12
|
+
const dir = join(sessionsDir(projectDir), id);
|
|
13
|
+
const data = join(dir, '.data');
|
|
14
|
+
return {
|
|
15
|
+
id,
|
|
16
|
+
dir,
|
|
17
|
+
workspace: join(dir, 'workspace'),
|
|
18
|
+
data,
|
|
19
|
+
state: join(data, 'state.json'),
|
|
20
|
+
memory: join(data, 'memory'),
|
|
21
|
+
blobs: join(data, 'blobs'),
|
|
22
|
+
runs: join(dir, 'runs'),
|
|
23
|
+
lock: join(dir, '.lock'),
|
|
24
|
+
meta: join(data, 'session.json'),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Creating and finding
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
export function createSession(projectDir, id, workspace) {
|
|
31
|
+
const p = sessionPaths(projectDir, id);
|
|
32
|
+
mkdirSync(p.data, { recursive: true });
|
|
33
|
+
mkdirSync(p.runs, { recursive: true });
|
|
34
|
+
mkdirSync(resolve(workspace), { recursive: true });
|
|
35
|
+
const meta = {
|
|
36
|
+
version: 1,
|
|
37
|
+
id: p.id,
|
|
38
|
+
createdAt: new Date().toISOString(),
|
|
39
|
+
workspace: resolve(workspace),
|
|
40
|
+
};
|
|
41
|
+
writeJson(p.meta, meta, 0o644);
|
|
42
|
+
return p;
|
|
43
|
+
}
|
|
44
|
+
export async function readSessionMeta(p) {
|
|
45
|
+
const meta = await readJson(p.meta, {});
|
|
46
|
+
return {
|
|
47
|
+
version: 1,
|
|
48
|
+
id: p.id,
|
|
49
|
+
createdAt: meta.createdAt ?? new Date().toISOString(),
|
|
50
|
+
// Sessions from before the workspace was recorded fall back to their
|
|
51
|
+
// own directory, which is where it would have been.
|
|
52
|
+
workspace: meta.workspace ?? p.workspace,
|
|
53
|
+
lastRunAt: meta.lastRunAt,
|
|
54
|
+
title: meta.title,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function writeSessionMeta(p, meta) {
|
|
58
|
+
writeJson(p.meta, meta, 0o644);
|
|
59
|
+
}
|
|
60
|
+
/** Newest first — the order a picker wants. */
|
|
61
|
+
export async function listSessions(projectDir) {
|
|
62
|
+
const out = [];
|
|
63
|
+
for (const id of sessionIds(projectDir).reverse()) {
|
|
64
|
+
const p = sessionPaths(projectDir, id);
|
|
65
|
+
const meta = await readSessionMeta(p);
|
|
66
|
+
const ids = runIds(p.dir);
|
|
67
|
+
out.push({
|
|
68
|
+
id,
|
|
69
|
+
createdAt: meta.createdAt,
|
|
70
|
+
runs: ids.length,
|
|
71
|
+
lastRunAt: meta.lastRunAt,
|
|
72
|
+
busy: isBusy(p.dir),
|
|
73
|
+
title: meta.title,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
export function newestSession(projectDir) {
|
|
79
|
+
return sessionIds(projectDir).at(-1);
|
|
80
|
+
}
|
|
81
|
+
export function requireSession(projectDir, id) {
|
|
82
|
+
const p = sessionPaths(projectDir, id);
|
|
83
|
+
if (!existsSync(p.dir)) {
|
|
84
|
+
throw invalidError(`no session ${id}`, 'see: zen list --sessions');
|
|
85
|
+
}
|
|
86
|
+
return p;
|
|
87
|
+
}
|
|
88
|
+
export function acquire(p) {
|
|
89
|
+
const lock = { pid: process.pid, host: hostname(), startedAt: new Date().toISOString() };
|
|
90
|
+
const body = `${JSON.stringify(lock, null, 2)}\n`;
|
|
91
|
+
try {
|
|
92
|
+
// 'wx' fails when the file exists — the create and the check are one
|
|
93
|
+
// operation, so two `zen run`s racing cannot both win.
|
|
94
|
+
writeFileSync(p.lock, body, { flag: 'wx' });
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
if (err.code !== 'EEXIST') {
|
|
98
|
+
throw err;
|
|
99
|
+
}
|
|
100
|
+
const held = current(p);
|
|
101
|
+
if (held && alive(held.pid) && held.host === hostname()) {
|
|
102
|
+
throw new CliError(`session ${p.id} is already running (pid ${held.pid})`, EXIT.failed, 'wait for it, or start another with --new');
|
|
103
|
+
}
|
|
104
|
+
// Stale, or from another machine's run that cannot be verified here.
|
|
105
|
+
writeFileSync(p.lock, body);
|
|
106
|
+
}
|
|
107
|
+
let released = false;
|
|
108
|
+
const release = () => {
|
|
109
|
+
if (released) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
released = true;
|
|
113
|
+
rmSync(p.lock, { force: true });
|
|
114
|
+
};
|
|
115
|
+
return { release };
|
|
116
|
+
}
|
|
117
|
+
function current(p) {
|
|
118
|
+
try {
|
|
119
|
+
return JSON.parse(readFileSync(p.lock, 'utf8'));
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
export function runPaths(session, id) {
|
|
126
|
+
if (!isStamp(id)) {
|
|
127
|
+
throw usageError(`"${id}" is not a run id`);
|
|
128
|
+
}
|
|
129
|
+
const dir = join(session.runs, id);
|
|
130
|
+
return {
|
|
131
|
+
id,
|
|
132
|
+
dir,
|
|
133
|
+
input: join(dir, 'input.md'),
|
|
134
|
+
output: join(dir, 'output.md'),
|
|
135
|
+
state: join(dir, 'state.json'),
|
|
136
|
+
report: join(dir, 'report.html'),
|
|
137
|
+
meta: join(dir, 'meta.json'),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
export function createRun(session) {
|
|
141
|
+
const p = runPaths(session, stamp());
|
|
142
|
+
mkdirSync(p.dir, { recursive: true });
|
|
143
|
+
return p;
|
|
144
|
+
}
|
|
145
|
+
export function writeRunMeta(p, meta) {
|
|
146
|
+
writeJson(p.meta, meta, 0o644);
|
|
147
|
+
}
|
|
148
|
+
export function newestRun(session) {
|
|
149
|
+
return runIds(session.dir).at(-1);
|
|
150
|
+
}
|
|
151
|
+
/** A path to show a human: relative when that is shorter, absolute otherwise. */
|
|
152
|
+
export function display(path, from = process.cwd()) {
|
|
153
|
+
const rel = relative(from, path);
|
|
154
|
+
return !rel.startsWith('..') && rel.length < path.length ? rel || '.' : path;
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=session.js.map
|
package/dist/term.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export declare const EXIT: {
|
|
2
|
+
readonly ok: 0;
|
|
3
|
+
readonly failed: 1;
|
|
4
|
+
readonly usage: 2;
|
|
5
|
+
readonly invalid: 3;
|
|
6
|
+
readonly credentials: 4;
|
|
7
|
+
readonly sandbox: 5;
|
|
8
|
+
};
|
|
9
|
+
export type ExitCode = (typeof EXIT)[keyof typeof EXIT];
|
|
10
|
+
/**
|
|
11
|
+
* An error that already knows what it means. Anything else reaching the top is
|
|
12
|
+
* a bug and exits `failed`, which is the distinction a script needs: a wrong
|
|
13
|
+
* invocation and a wrong answer are not the same failure.
|
|
14
|
+
*/
|
|
15
|
+
export declare class CliError extends Error {
|
|
16
|
+
readonly code: ExitCode;
|
|
17
|
+
readonly hint?: string;
|
|
18
|
+
constructor(message: string, code?: ExitCode, hint?: string);
|
|
19
|
+
}
|
|
20
|
+
export declare const usageError: (m: string, hint?: string) => CliError;
|
|
21
|
+
export declare const invalidError: (m: string, hint?: string) => CliError;
|
|
22
|
+
export declare const credentialError: (m: string, hint?: string) => CliError;
|
|
23
|
+
export declare const bold: (s: string) => string;
|
|
24
|
+
export declare const dim: (s: string) => string;
|
|
25
|
+
export declare const red: (s: string) => string;
|
|
26
|
+
export declare const green: (s: string) => string;
|
|
27
|
+
export declare const yellow: (s: string) => string;
|
|
28
|
+
export declare const cyan: (s: string) => string;
|
|
29
|
+
export declare function pad(s: string, to: number): string;
|
|
30
|
+
/** Column-aligned rows. Trailing whitespace is trimmed so `diff` stays quiet. */
|
|
31
|
+
export declare function table(rows: readonly (readonly string[])[], gap?: number): string[];
|
|
32
|
+
export declare function write(line?: string): void;
|
|
33
|
+
export declare function writeAll(lines: readonly string[]): void;
|
|
34
|
+
/** Machine-readable output. Pretty-printed: it is read by people too. */
|
|
35
|
+
export declare function json(value: unknown): void;
|
|
36
|
+
export declare function note(line?: string): void;
|
|
37
|
+
/**
|
|
38
|
+
* One line of narration that rewrites itself while work is in flight, so a
|
|
39
|
+
* command that waits on the network says what it is waiting for. Without a
|
|
40
|
+
* terminal there is nothing to rewrite over, so each update is its own line —
|
|
41
|
+
* which is what a CI log wants anyway.
|
|
42
|
+
*/
|
|
43
|
+
export declare function progress(): {
|
|
44
|
+
update: (line: string) => void;
|
|
45
|
+
done: () => void;
|
|
46
|
+
};
|
|
47
|
+
export declare function warn(message: string): void;
|
|
48
|
+
export declare function fail(message: string, hint?: string): void;
|
|
49
|
+
export declare function isInteractive(): boolean;
|
|
50
|
+
export declare function ask(question: string, fallback?: string): Promise<string>;
|
|
51
|
+
/**
|
|
52
|
+
* Reads without echoing. `readline` writes what you type through its `output`
|
|
53
|
+
* stream, so the interface is built without one — there is then nothing for it
|
|
54
|
+
* to echo to, and no window in which the secret is on screen.
|
|
55
|
+
*/
|
|
56
|
+
export declare function askSecret(question: string): Promise<string>;
|
|
57
|
+
export declare function confirm(question: string, fallback?: boolean): Promise<boolean>;
|
|
58
|
+
export interface Choice<T> {
|
|
59
|
+
label: string;
|
|
60
|
+
detail?: string;
|
|
61
|
+
value: T;
|
|
62
|
+
}
|
|
63
|
+
/** A numbered list. The pretty picker is the TUI's; this is the fallback. */
|
|
64
|
+
export declare function choose<T>(title: string, choices: readonly Choice<T>[]): Promise<T>;
|
|
65
|
+
/** Piped input, or undefined when stdin is a terminal (i.e. nobody piped). */
|
|
66
|
+
export declare function readStdin(): Promise<string | undefined>;
|
|
67
|
+
export declare function ago(iso: string | undefined): string;
|
|
68
|
+
export declare function count(n: number, singular: string, plural?: string): string;
|
|
69
|
+
//# sourceMappingURL=term.d.ts.map
|
package/dist/term.js
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline/promises';
|
|
2
|
+
import { styleText } from 'node:util';
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Terminal I/O
|
|
5
|
+
//
|
|
6
|
+
// One rule decides where everything goes: **stdout is the answer, stderr is the
|
|
7
|
+
// narration.** Prompts, progress, warnings and errors are narration, so a
|
|
8
|
+
// pipeline that only wants the answer gets exactly that and nothing else.
|
|
9
|
+
//
|
|
10
|
+
// `styleText` no-ops when the stream is not a tty and honours NO_COLOR itself,
|
|
11
|
+
// so there is no flag to thread through and no piped output to garble.
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
export const EXIT = {
|
|
14
|
+
ok: 0,
|
|
15
|
+
failed: 1,
|
|
16
|
+
usage: 2,
|
|
17
|
+
invalid: 3,
|
|
18
|
+
credentials: 4,
|
|
19
|
+
sandbox: 5,
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* An error that already knows what it means. Anything else reaching the top is
|
|
23
|
+
* a bug and exits `failed`, which is the distinction a script needs: a wrong
|
|
24
|
+
* invocation and a wrong answer are not the same failure.
|
|
25
|
+
*/
|
|
26
|
+
export class CliError extends Error {
|
|
27
|
+
code;
|
|
28
|
+
hint;
|
|
29
|
+
constructor(message, code = EXIT.failed, hint) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = 'CliError';
|
|
32
|
+
this.code = code;
|
|
33
|
+
this.hint = hint;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export const usageError = (m, hint) => new CliError(m, EXIT.usage, hint);
|
|
37
|
+
export const invalidError = (m, hint) => new CliError(m, EXIT.invalid, hint);
|
|
38
|
+
export const credentialError = (m, hint) => new CliError(m, EXIT.credentials, hint);
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Styling
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
export const bold = (s) => styleText('bold', s);
|
|
43
|
+
export const dim = (s) => styleText('dim', s);
|
|
44
|
+
export const red = (s) => styleText('red', s);
|
|
45
|
+
export const green = (s) => styleText('green', s);
|
|
46
|
+
export const yellow = (s) => styleText('yellow', s);
|
|
47
|
+
export const cyan = (s) => styleText('cyan', s);
|
|
48
|
+
/** Visible width — style codes must not count towards column alignment. */
|
|
49
|
+
// eslint-disable-next-line no-control-regex
|
|
50
|
+
const ANSI = /\u001b\[[0-9;]*m/g;
|
|
51
|
+
const width = (s) => s.replace(ANSI, '');
|
|
52
|
+
export function pad(s, to) {
|
|
53
|
+
return s + ' '.repeat(Math.max(0, to - width(s).length));
|
|
54
|
+
}
|
|
55
|
+
/** Column-aligned rows. Trailing whitespace is trimmed so `diff` stays quiet. */
|
|
56
|
+
export function table(rows, gap = 2) {
|
|
57
|
+
if (rows.length === 0) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
const columns = Math.max(...rows.map((r) => r.length));
|
|
61
|
+
const widths = [];
|
|
62
|
+
for (let c = 0; c < columns; c++) {
|
|
63
|
+
widths[c] = Math.max(...rows.map((r) => width(r[c] ?? '').length));
|
|
64
|
+
}
|
|
65
|
+
return rows.map((r) => r
|
|
66
|
+
.map((cell, c) => (c === r.length - 1 ? cell : pad(cell, widths[c])))
|
|
67
|
+
.join(' '.repeat(gap))
|
|
68
|
+
.trimEnd());
|
|
69
|
+
}
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Output
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
export function write(line = '') {
|
|
74
|
+
process.stdout.write(`${line}\n`);
|
|
75
|
+
}
|
|
76
|
+
export function writeAll(lines) {
|
|
77
|
+
for (const l of lines) {
|
|
78
|
+
write(l);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Machine-readable output. Pretty-printed: it is read by people too. */
|
|
82
|
+
export function json(value) {
|
|
83
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
84
|
+
}
|
|
85
|
+
export function note(line = '') {
|
|
86
|
+
process.stderr.write(`${line}\n`);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* One line of narration that rewrites itself while work is in flight, so a
|
|
90
|
+
* command that waits on the network says what it is waiting for. Without a
|
|
91
|
+
* terminal there is nothing to rewrite over, so each update is its own line —
|
|
92
|
+
* which is what a CI log wants anyway.
|
|
93
|
+
*/
|
|
94
|
+
export function progress() {
|
|
95
|
+
const tty = Boolean(process.stderr.isTTY);
|
|
96
|
+
let painted = false;
|
|
97
|
+
return {
|
|
98
|
+
update(line) {
|
|
99
|
+
if (!tty) {
|
|
100
|
+
note(line);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
process.stderr.write(`\r\u001b[2K${line}`);
|
|
104
|
+
painted = true;
|
|
105
|
+
},
|
|
106
|
+
done() {
|
|
107
|
+
if (painted) {
|
|
108
|
+
process.stderr.write('\r\u001b[2K');
|
|
109
|
+
}
|
|
110
|
+
painted = false;
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
export function warn(message) {
|
|
115
|
+
note(`${yellow('warning')} ${message}`);
|
|
116
|
+
}
|
|
117
|
+
export function fail(message, hint) {
|
|
118
|
+
note(`${red('error')} ${message}`);
|
|
119
|
+
if (hint) {
|
|
120
|
+
note(` ${dim(hint)}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Asking
|
|
125
|
+
//
|
|
126
|
+
// Every prompt below refuses to run without a terminal rather than blocking on
|
|
127
|
+
// a stdin that will never answer. A CLI that hangs in CI is worse than one that
|
|
128
|
+
// fails in CI, because only one of the two tells you which flag you forgot.
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
export function isInteractive() {
|
|
131
|
+
return Boolean(process.stdin.isTTY && process.stderr.isTTY);
|
|
132
|
+
}
|
|
133
|
+
function requireTty(what, flag) {
|
|
134
|
+
if (!isInteractive()) {
|
|
135
|
+
throw usageError(`cannot ask for ${what} without a terminal`, `pass ${flag} instead`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
export async function ask(question, fallback) {
|
|
139
|
+
requireTty(question, '--yes or the matching flag');
|
|
140
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
141
|
+
try {
|
|
142
|
+
const suffix = fallback ? dim(` [${fallback}]`) : '';
|
|
143
|
+
const answer = (await rl.question(`${question}${suffix} `)).trim();
|
|
144
|
+
return answer || fallback || '';
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
rl.close();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Reads without echoing. `readline` writes what you type through its `output`
|
|
152
|
+
* stream, so the interface is built without one — there is then nothing for it
|
|
153
|
+
* to echo to, and no window in which the secret is on screen.
|
|
154
|
+
*/
|
|
155
|
+
export async function askSecret(question) {
|
|
156
|
+
requireTty('a secret', 'a piped value on stdin');
|
|
157
|
+
process.stderr.write(`${question} `);
|
|
158
|
+
const rl = createInterface({ input: process.stdin, terminal: true });
|
|
159
|
+
try {
|
|
160
|
+
const answer = await rl.question('');
|
|
161
|
+
return answer.trim();
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
rl.close();
|
|
165
|
+
process.stderr.write('\n');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
export async function confirm(question, fallback = false) {
|
|
169
|
+
requireTty(question, '--yes');
|
|
170
|
+
const answer = await ask(`${question} ${dim(fallback ? '(Y/n)' : '(y/N)')}`, '');
|
|
171
|
+
if (!answer) {
|
|
172
|
+
return fallback;
|
|
173
|
+
}
|
|
174
|
+
return /^y(es)?$/i.test(answer);
|
|
175
|
+
}
|
|
176
|
+
/** A numbered list. The pretty picker is the TUI's; this is the fallback. */
|
|
177
|
+
export async function choose(title, choices) {
|
|
178
|
+
if (choices.length === 0) {
|
|
179
|
+
throw usageError(`nothing to choose from: ${title}`);
|
|
180
|
+
}
|
|
181
|
+
if (choices.length === 1) {
|
|
182
|
+
return choices[0].value;
|
|
183
|
+
}
|
|
184
|
+
requireTty(title, 'the matching flag');
|
|
185
|
+
note(bold(title));
|
|
186
|
+
const rows = choices.map((c, i) => [` ${dim(`${i + 1}.`)}`, c.label, dim(c.detail ?? '')]);
|
|
187
|
+
for (const line of table(rows)) {
|
|
188
|
+
note(line);
|
|
189
|
+
}
|
|
190
|
+
for (;;) {
|
|
191
|
+
const answer = await ask('Choose', '1');
|
|
192
|
+
const n = Number(answer);
|
|
193
|
+
if (Number.isInteger(n) && n >= 1 && n <= choices.length) {
|
|
194
|
+
return choices[n - 1].value;
|
|
195
|
+
}
|
|
196
|
+
note(dim(`Enter a number between 1 and ${choices.length}.`));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// stdin
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
/** Piped input, or undefined when stdin is a terminal (i.e. nobody piped). */
|
|
203
|
+
export async function readStdin() {
|
|
204
|
+
if (process.stdin.isTTY) {
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
const chunks = [];
|
|
208
|
+
for await (const chunk of process.stdin) {
|
|
209
|
+
chunks.push(chunk);
|
|
210
|
+
}
|
|
211
|
+
const text = Buffer.concat(chunks).toString('utf8').trim();
|
|
212
|
+
return text || undefined;
|
|
213
|
+
}
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
// Formatting helpers shared by several commands
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
export function ago(iso) {
|
|
218
|
+
if (!iso) {
|
|
219
|
+
return 'never';
|
|
220
|
+
}
|
|
221
|
+
const then = Date.parse(iso);
|
|
222
|
+
if (Number.isNaN(then)) {
|
|
223
|
+
return 'unknown';
|
|
224
|
+
}
|
|
225
|
+
const seconds = Math.max(0, Math.round((Date.now() - then) / 1000));
|
|
226
|
+
const scale = [
|
|
227
|
+
[31536000, 'y'],
|
|
228
|
+
[86400, 'd'],
|
|
229
|
+
[3600, 'h'],
|
|
230
|
+
[60, 'm'],
|
|
231
|
+
];
|
|
232
|
+
for (const [size, unit] of scale) {
|
|
233
|
+
if (seconds >= size) {
|
|
234
|
+
return `${Math.floor(seconds / size)}${unit} ago`;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return seconds < 5 ? 'just now' : `${seconds}s ago`;
|
|
238
|
+
}
|
|
239
|
+
export function count(n, singular, plural = `${singular}s`) {
|
|
240
|
+
return `${n} ${n === 1 ? singular : plural}`;
|
|
241
|
+
}
|
|
242
|
+
//# sourceMappingURL=term.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as Engine from '../engine.ts';
|
|
2
|
+
export interface AppOptions {
|
|
3
|
+
readOnly: boolean;
|
|
4
|
+
/** `dark`, `light` or `auto`. Unset means `auto`. */
|
|
5
|
+
theme?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function start(engine: Engine.Engine, options: AppOptions): Promise<void>;
|
|
8
|
+
//# sourceMappingURL=app.d.ts.map
|