@berthdev/berth 0.0.0-stage → 0.2.1-rc.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 +16 -2
- package/dist/src/agents/claude-name.d.ts +1 -0
- package/dist/src/agents/claude-name.js +57 -0
- package/dist/src/agents/claude-name.js.map +1 -0
- package/dist/src/agents/commands.d.ts +23 -0
- package/dist/src/agents/commands.js +48 -0
- package/dist/src/agents/commands.js.map +1 -0
- package/dist/src/agents/hooks.d.ts +13 -0
- package/dist/src/agents/hooks.js +141 -0
- package/dist/src/agents/hooks.js.map +1 -0
- package/dist/src/agents/registry.d.ts +33 -0
- package/dist/src/agents/registry.js +141 -0
- package/dist/src/agents/registry.js.map +1 -0
- package/dist/src/cli/main.d.ts +4 -0
- package/dist/src/cli/main.js +236 -0
- package/dist/src/cli/main.js.map +1 -0
- package/dist/src/config/index.d.ts +21 -0
- package/dist/src/config/index.js +77 -0
- package/dist/src/config/index.js.map +1 -0
- package/dist/src/context.d.ts +9 -0
- package/dist/src/context.js +12 -0
- package/dist/src/context.js.map +1 -0
- package/dist/src/errors.d.ts +13 -0
- package/dist/src/errors.js +17 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/index.d.ts +17 -0
- package/dist/src/index.js +18 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/iterm2/install.d.ts +29 -0
- package/dist/src/iterm2/install.js +112 -0
- package/dist/src/iterm2/install.js.map +1 -0
- package/dist/src/present/attach.d.ts +16 -0
- package/dist/src/present/attach.js +31 -0
- package/dist/src/present/attach.js.map +1 -0
- package/dist/src/present/state.d.ts +14 -0
- package/dist/src/present/state.js +63 -0
- package/dist/src/present/state.js.map +1 -0
- package/dist/src/registry/doctor.d.ts +7 -0
- package/dist/src/registry/doctor.js +31 -0
- package/dist/src/registry/doctor.js.map +1 -0
- package/dist/src/registry/sessions.d.ts +47 -0
- package/dist/src/registry/sessions.js +138 -0
- package/dist/src/registry/sessions.js.map +1 -0
- package/dist/src/restore/ensure.d.ts +17 -0
- package/dist/src/restore/ensure.js +57 -0
- package/dist/src/restore/ensure.js.map +1 -0
- package/dist/src/restore/lock.d.ts +6 -0
- package/dist/src/restore/lock.js +74 -0
- package/dist/src/restore/lock.js.map +1 -0
- package/dist/src/shell.d.ts +6 -0
- package/dist/src/shell.js +68 -0
- package/dist/src/shell.js.map +1 -0
- package/dist/src/snapshot/store.d.ts +29 -0
- package/dist/src/snapshot/store.js +90 -0
- package/dist/src/snapshot/store.js.map +1 -0
- package/dist/src/snapshot/take.d.ts +5 -0
- package/dist/src/snapshot/take.js +33 -0
- package/dist/src/snapshot/take.js.map +1 -0
- package/dist/src/tmux/client.d.ts +49 -0
- package/dist/src/tmux/client.js +111 -0
- package/dist/src/tmux/client.js.map +1 -0
- package/dist/src/tmux/install.d.ts +4 -0
- package/dist/src/tmux/install.js +64 -0
- package/dist/src/tmux/install.js.map +1 -0
- package/dist/src/tools.d.ts +5 -0
- package/dist/src/tools.js +16 -0
- package/dist/src/tools.js.map +1 -0
- package/package.json +49 -4
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { appendFileSync } from 'node:fs';
|
|
2
|
+
import { stamp } from '../registry/sessions.js';
|
|
3
|
+
import { isIterm2, presentState } from './state.js';
|
|
4
|
+
/** iTerm2 user variable naming the session a pane shows (spec §7.3). */
|
|
5
|
+
export const PANE_VARIABLE = 'berthSession';
|
|
6
|
+
export function setUserVar(name, value) {
|
|
7
|
+
return `\x1b]1337;SetUserVar=${name}=${Buffer.from(value).toString('base64')}\x07`;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A client attached to a session, or switched to one (tmux hooks
|
|
11
|
+
* client-attached / client-session-changed). For iTerm2 the pane is marked
|
|
12
|
+
* with the session's name and the session's origin becomes iterm2; every
|
|
13
|
+
* client is shown the session's current state.
|
|
14
|
+
*/
|
|
15
|
+
export function clientAttached(ctx, client) {
|
|
16
|
+
const live = ctx.tmux.listSessions().find((s) => s.name === client.session);
|
|
17
|
+
if (!live?.meta.agent)
|
|
18
|
+
return;
|
|
19
|
+
if (isIterm2(client.termtype)) {
|
|
20
|
+
try {
|
|
21
|
+
appendFileSync(client.tty, setUserVar(PANE_VARIABLE, client.session));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// The client went away; nothing to mark.
|
|
25
|
+
}
|
|
26
|
+
if (live.meta.origin !== 'iterm2')
|
|
27
|
+
stamp(ctx, client.session, { origin: 'iterm2' });
|
|
28
|
+
}
|
|
29
|
+
presentState(ctx.tmux, client.session, live.meta.state ?? '', { note: live.meta.note, clients: [{ tty: client.tty, termtype: client.termtype }] });
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=attach.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attach.js","sourceRoot":"","sources":["../../../src/present/attach.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAEzC,OAAO,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAEpD,wEAAwE;AACxE,MAAM,CAAC,MAAM,aAAa,GAAG,cAAc,CAAC;AAE5C,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,KAAa;IACpD,OAAO,wBAAwB,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC;AACrF,CAAC;AAQD;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,GAAY,EAAE,MAAsB;IACjE,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,OAAO,CAAC,CAAC;IAC5E,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK;QAAE,OAAO;IAC9B,IAAI,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC9B,IAAI,CAAC;YACH,cAAc,CAAC,MAAM,CAAC,GAAG,EAAE,UAAU,CAAC,aAAa,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;QACxE,CAAC;QAAC,MAAM,CAAC;YACP,yCAAyC;QAC3C,CAAC;QACD,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,KAAK,QAAQ;YAAE,KAAK,CAAC,GAAG,EAAE,MAAM,CAAC,OAAO,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;IACtF,CAAC;IACD,YAAY,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAC;AACrJ,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Tmux } from '../tmux/client.js';
|
|
2
|
+
export declare function isIterm2(termtype: string): boolean;
|
|
3
|
+
export declare function sequencesFor(state: string, session: string, iterm2: boolean, note?: string): string;
|
|
4
|
+
export interface Client {
|
|
5
|
+
tty: string;
|
|
6
|
+
termtype: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function clientsOf(tmux: Tmux, session: string): Client[];
|
|
9
|
+
export interface PresentOptions {
|
|
10
|
+
note?: string;
|
|
11
|
+
clients?: Client[];
|
|
12
|
+
}
|
|
13
|
+
/** Write the state to each attached terminal; failures are ignored (best effort). */
|
|
14
|
+
export declare function presentState(tmux: Tmux, session: string, state: string, options?: PresentOptions): number;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { appendFileSync } from 'node:fs';
|
|
2
|
+
/**
|
|
3
|
+
* Show a session's agent state on every terminal attached to it (spec §6, §7.4).
|
|
4
|
+
*
|
|
5
|
+
* Sequences are written straight to each client's tty, past tmux, so the
|
|
6
|
+
* outer terminal sees them. Which ones depends on the terminal, read from
|
|
7
|
+
* tmux's client_termtype:
|
|
8
|
+
* - iTerm2: its tab status sequence (OSC 21337) — a dot and a subtitle;
|
|
9
|
+
* - others (VS Code's xterm.js among them): OSC 9;4 progress, which VS Code
|
|
10
|
+
* shows through `${progress}`, and an OSC 2 title.
|
|
11
|
+
*/
|
|
12
|
+
const GLYPH = { working: '●', waiting: '◐', done: '✓' };
|
|
13
|
+
const PROGRESS = { working: '3', waiting: '4', done: '0' };
|
|
14
|
+
const ITERM_COLOUR = { working: '#4f8fe6', waiting: '#ebaa28', done: '#50b464' };
|
|
15
|
+
const ITERM_TEXT = { working: 'working', waiting: 'waiting for you', done: 'done' };
|
|
16
|
+
const OSC = '\x1b]';
|
|
17
|
+
const ST = '\x07';
|
|
18
|
+
export function isIterm2(termtype) {
|
|
19
|
+
return /^iTerm2\b/i.test(termtype);
|
|
20
|
+
}
|
|
21
|
+
/** iTerm2's tab status may not contain the sequence separators. */
|
|
22
|
+
function statusText(text) {
|
|
23
|
+
return text.replace(/[;\x07\x1b]/g, ' ').trim().slice(0, 80);
|
|
24
|
+
}
|
|
25
|
+
export function sequencesFor(state, session, iterm2, note = '') {
|
|
26
|
+
if (iterm2) {
|
|
27
|
+
const colour = ITERM_COLOUR[state];
|
|
28
|
+
if (!colour)
|
|
29
|
+
return `${OSC}21337;indicator=;status=${ST}`;
|
|
30
|
+
const text = statusText(state === 'waiting' && note ? note : ITERM_TEXT[state]);
|
|
31
|
+
return `${OSC}21337;indicator=${colour};status=${text};status-color=${colour}${ST}`;
|
|
32
|
+
}
|
|
33
|
+
const glyph = GLYPH[state];
|
|
34
|
+
return `${OSC}9;4;${PROGRESS[state] ?? '0'};0${ST}${OSC}2;${glyph ? `${glyph} ` : ''}${session}${ST}`;
|
|
35
|
+
}
|
|
36
|
+
export function clientsOf(tmux, session) {
|
|
37
|
+
const result = tmux.run(['list-clients', '-t', `=${session}`, '-F', '#{client_tty}\t#{client_termtype}'], true);
|
|
38
|
+
if (!result.ok)
|
|
39
|
+
return [];
|
|
40
|
+
return result.stdout
|
|
41
|
+
.split('\n')
|
|
42
|
+
.filter(Boolean)
|
|
43
|
+
.map((line) => {
|
|
44
|
+
const [tty, termtype = ''] = line.split('\t');
|
|
45
|
+
return { tty, termtype };
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/** Write the state to each attached terminal; failures are ignored (best effort). */
|
|
49
|
+
export function presentState(tmux, session, state, options = {}) {
|
|
50
|
+
const clients = options.clients ?? clientsOf(tmux, session);
|
|
51
|
+
let written = 0;
|
|
52
|
+
for (const client of clients) {
|
|
53
|
+
try {
|
|
54
|
+
appendFileSync(client.tty, sequencesFor(state, session, isIterm2(client.termtype), options.note ?? ''));
|
|
55
|
+
written++;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// A client that went away or a tty we cannot write is not an error.
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return written;
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state.js","sourceRoot":"","sources":["../../../src/present/state.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Context } from '../context.js';
|
|
2
|
+
export interface Finding {
|
|
3
|
+
session?: string;
|
|
4
|
+
problem: string;
|
|
5
|
+
}
|
|
6
|
+
/** Drift and gaps worth a user's attention; empty when all is well. */
|
|
7
|
+
export declare function doctor(ctx: Context, env?: NodeJS.ProcessEnv): Finding[];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { getAdapter } from '../agents/registry.js';
|
|
3
|
+
import { hooksInstalled } from '../agents/hooks.js';
|
|
4
|
+
import { list } from './sessions.js';
|
|
5
|
+
/** Drift and gaps worth a user's attention; empty when all is well. */
|
|
6
|
+
export function doctor(ctx, env = process.env) {
|
|
7
|
+
const findings = [];
|
|
8
|
+
const agentsInUse = new Set();
|
|
9
|
+
for (const row of list(ctx)) {
|
|
10
|
+
if (!row.managed)
|
|
11
|
+
continue;
|
|
12
|
+
agentsInUse.add(row.agent);
|
|
13
|
+
const adapter = getAdapter(row.agent);
|
|
14
|
+
if (!adapter) {
|
|
15
|
+
findings.push({ session: row.name, problem: `unknown agent ${row.agent}` });
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (!existsSync(row.cwd))
|
|
19
|
+
findings.push({ session: row.name, problem: `working directory missing: ${row.cwd}` });
|
|
20
|
+
if (adapter.resume && !row.conversation)
|
|
21
|
+
findings.push({ session: row.name, problem: 'conversation not bound; restore will start fresh' });
|
|
22
|
+
}
|
|
23
|
+
for (const agent of agentsInUse) {
|
|
24
|
+
const adapter = getAdapter(agent);
|
|
25
|
+
if (adapter?.hookTarget && !hooksInstalled(agent, env)) {
|
|
26
|
+
findings.push({ problem: `hooks for ${adapter.displayName} not installed (berth hooks install ${agent})` });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return findings;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=doctor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor.js","sourceRoot":"","sources":["../../../src/registry/doctor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAErC,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAOrC,uEAAuE;AACvE,MAAM,UAAU,MAAM,CAAC,GAAY,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG;IACpD,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,MAAM,WAAW,GAAG,IAAI,GAAG,EAAU,CAAC;IACtC,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5B,IAAI,CAAC,GAAG,CAAC,OAAO;YAAE,SAAS;QAC3B,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAC3B,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACtC,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,IAAI,EAAE,OAAO,EAAE,iBAAiB,GAAG,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YAC5E,SAAS;QACX,CAAC;QACD,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,IAAI,EAAE,OAAO,EAAE,8BAA8B,GAAG,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QACjH,IAAI,OAAO,CAAC,MAAM,IAAI,CAAC,GAAG,CAAC,YAAY;YAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,IAAI,EAAE,OAAO,EAAE,kDAAkD,EAAE,CAAC,CAAC;IAC7I,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,WAAW,EAAE,CAAC;QAChC,MAAM,OAAO,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC;QAClC,IAAI,OAAO,EAAE,UAAU,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,GAAG,CAAC,EAAE,CAAC;YACvD,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,aAAa,OAAO,CAAC,WAAW,uCAAuC,KAAK,GAAG,EAAE,CAAC,CAAC;QAC9G,CAAC;IACH,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Context } from '../context.js';
|
|
2
|
+
/** One row of `berth ls` (implementation §2.2). */
|
|
3
|
+
export interface SessionRow {
|
|
4
|
+
name: string;
|
|
5
|
+
managed: boolean;
|
|
6
|
+
agent: string;
|
|
7
|
+
cwd: string;
|
|
8
|
+
conversation: string;
|
|
9
|
+
origin: string;
|
|
10
|
+
state: string;
|
|
11
|
+
note: string;
|
|
12
|
+
attachedClients: number;
|
|
13
|
+
live: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare const ORIGINS: readonly ["vscode", "iterm2", "none"];
|
|
16
|
+
export declare function list(ctx: Context): SessionRow[];
|
|
17
|
+
export declare function sanitizeName(raw: string): string;
|
|
18
|
+
/** Default name from the cwd's basename; suffix -2, -3, … on collision with any live session. */
|
|
19
|
+
export declare function uniqueName(ctx: Context, base: string): string;
|
|
20
|
+
export declare function checkOrigin(origin: string): string;
|
|
21
|
+
export interface NewOptions {
|
|
22
|
+
agent: string;
|
|
23
|
+
cwd: string;
|
|
24
|
+
name?: string;
|
|
25
|
+
origin?: string;
|
|
26
|
+
extraArgs?: string[];
|
|
27
|
+
}
|
|
28
|
+
export declare function create(ctx: Context, options: NewOptions): string;
|
|
29
|
+
export declare function requireLive(ctx: Context, name: string): void;
|
|
30
|
+
/**
|
|
31
|
+
* Make a live session managed. Its directory is the one it records, else the
|
|
32
|
+
* active pane's current directory (a launcher may have changed into the
|
|
33
|
+
* project), else the session's start directory.
|
|
34
|
+
*/
|
|
35
|
+
export declare function adopt(ctx: Context, name: string, agent: string, origin?: string): void;
|
|
36
|
+
export declare function stamp(ctx: Context, name: string, values: Record<string, string>): void;
|
|
37
|
+
/** Note that marks a waiting state raised by a terminal bell rather than a hook. */
|
|
38
|
+
export declare const BELL_NOTE = "bell";
|
|
39
|
+
/**
|
|
40
|
+
* A terminal bell from a managed session (tmux alert-bell). Agents without
|
|
41
|
+
* Berth hooks get state `waiting`, so every agent can ask for attention; for
|
|
42
|
+
* agents whose hooks are installed the hooks are more precise and the bell
|
|
43
|
+
* is ignored. Returns whether the state changed.
|
|
44
|
+
*/
|
|
45
|
+
export declare function bellRang(ctx: Context, name: string, env?: NodeJS.ProcessEnv): boolean;
|
|
46
|
+
/** Seeing a session answers its bell: clear a bell-raised waiting state. */
|
|
47
|
+
export declare function clearBell(ctx: Context, name: string): void;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { basename } from 'node:path';
|
|
2
|
+
import { BerthError, ExitCode } from '../errors.js';
|
|
3
|
+
import { launchCommand, requireAdapter } from '../agents/commands.js';
|
|
4
|
+
import { hooksInstalled } from '../agents/hooks.js';
|
|
5
|
+
import { getAdapter } from '../agents/registry.js';
|
|
6
|
+
import { snapshotQuietly } from '../snapshot/take.js';
|
|
7
|
+
import { presentState } from '../present/state.js';
|
|
8
|
+
export const ORIGINS = ['vscode', 'iterm2', 'none'];
|
|
9
|
+
export function list(ctx) {
|
|
10
|
+
const live = ctx.tmux.listSessions();
|
|
11
|
+
const rows = live.map((s) => ({
|
|
12
|
+
name: s.name,
|
|
13
|
+
managed: Boolean(s.meta.agent),
|
|
14
|
+
agent: s.meta.agent ?? '',
|
|
15
|
+
cwd: s.meta.cwd ?? s.path,
|
|
16
|
+
conversation: s.meta.conversation ?? '',
|
|
17
|
+
origin: s.meta.origin ?? (s.meta.agent ? 'none' : ''),
|
|
18
|
+
state: s.meta.state ?? '',
|
|
19
|
+
note: s.meta.note ?? '',
|
|
20
|
+
attachedClients: s.attachedClients,
|
|
21
|
+
live: true,
|
|
22
|
+
}));
|
|
23
|
+
const liveNames = new Set(live.map((s) => s.name));
|
|
24
|
+
for (const s of ctx.store.newest()?.sessions ?? []) {
|
|
25
|
+
if (liveNames.has(s.name))
|
|
26
|
+
continue;
|
|
27
|
+
rows.push({ ...s, managed: true, state: '', note: '', attachedClients: 0, live: false });
|
|
28
|
+
}
|
|
29
|
+
return rows;
|
|
30
|
+
}
|
|
31
|
+
export function sanitizeName(raw) {
|
|
32
|
+
const cleaned = raw.replace(/[^A-Za-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
33
|
+
return cleaned || 'session';
|
|
34
|
+
}
|
|
35
|
+
/** Default name from the cwd's basename; suffix -2, -3, … on collision with any live session. */
|
|
36
|
+
export function uniqueName(ctx, base) {
|
|
37
|
+
const taken = new Set(ctx.tmux.listSessions().map((s) => s.name));
|
|
38
|
+
const name = sanitizeName(base);
|
|
39
|
+
if (!taken.has(name))
|
|
40
|
+
return name;
|
|
41
|
+
for (let n = 2;; n++)
|
|
42
|
+
if (!taken.has(`${name}-${n}`))
|
|
43
|
+
return `${name}-${n}`;
|
|
44
|
+
}
|
|
45
|
+
export function checkOrigin(origin) {
|
|
46
|
+
if (!ORIGINS.includes(origin)) {
|
|
47
|
+
throw new BerthError(`origin must be one of ${ORIGINS.join(', ')}`, ExitCode.usage);
|
|
48
|
+
}
|
|
49
|
+
return origin;
|
|
50
|
+
}
|
|
51
|
+
export function create(ctx, options) {
|
|
52
|
+
const adapter = requireAdapter(options.agent);
|
|
53
|
+
const name = options.name ? sanitizeName(options.name) : uniqueName(ctx, basename(options.cwd));
|
|
54
|
+
if (ctx.tmux.hasSession(name))
|
|
55
|
+
throw new BerthError(`session already exists: ${name}`, ExitCode.usage);
|
|
56
|
+
const command = launchCommand({ adapter, name, cwd: options.cwd, extraArgs: options.extraArgs, overrides: ctx.config.agents });
|
|
57
|
+
ctx.tmux.newSession(name, options.cwd, command);
|
|
58
|
+
ctx.tmux.setMeta(name, {
|
|
59
|
+
agent: adapter.id,
|
|
60
|
+
cwd: options.cwd,
|
|
61
|
+
argv: command,
|
|
62
|
+
origin: checkOrigin(options.origin ?? 'none'),
|
|
63
|
+
});
|
|
64
|
+
ctx.tmux.setTitle(name);
|
|
65
|
+
snapshotQuietly(ctx);
|
|
66
|
+
return name;
|
|
67
|
+
}
|
|
68
|
+
export function requireLive(ctx, name) {
|
|
69
|
+
if (!ctx.tmux.hasSession(name))
|
|
70
|
+
throw new BerthError(`no such session: ${name}`, ExitCode.notFound);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Make a live session managed. Its directory is the one it records, else the
|
|
74
|
+
* active pane's current directory (a launcher may have changed into the
|
|
75
|
+
* project), else the session's start directory.
|
|
76
|
+
*/
|
|
77
|
+
export function adopt(ctx, name, agent, origin) {
|
|
78
|
+
requireLive(ctx, name);
|
|
79
|
+
const adapter = requireAdapter(agent);
|
|
80
|
+
const live = ctx.tmux.listSessions().find((s) => s.name === name);
|
|
81
|
+
ctx.tmux.setMeta(name, {
|
|
82
|
+
agent: adapter.id,
|
|
83
|
+
cwd: live?.meta.cwd ?? ctx.tmux.paneCurrentPath(name) ?? live?.path ?? '',
|
|
84
|
+
argv: live?.meta.argv ?? adapter.launch.replace(/\{\w+\}/g, '').trim(),
|
|
85
|
+
origin: checkOrigin(origin ?? live?.meta.origin ?? 'none'),
|
|
86
|
+
});
|
|
87
|
+
ctx.tmux.setTitle(name);
|
|
88
|
+
snapshotQuietly(ctx);
|
|
89
|
+
}
|
|
90
|
+
const STAMPABLE = new Set(['agent', 'cwd', 'argv', 'conversation', 'origin', 'state', 'note']);
|
|
91
|
+
export function stamp(ctx, name, values) {
|
|
92
|
+
requireLive(ctx, name);
|
|
93
|
+
const meta = {};
|
|
94
|
+
for (const [key, value] of Object.entries(values)) {
|
|
95
|
+
if (!STAMPABLE.has(key))
|
|
96
|
+
throw new BerthError(`cannot stamp ${key}`, ExitCode.usage);
|
|
97
|
+
if (key === 'origin')
|
|
98
|
+
checkOrigin(value);
|
|
99
|
+
if (key === 'agent')
|
|
100
|
+
requireAdapter(value);
|
|
101
|
+
meta[key] = value;
|
|
102
|
+
}
|
|
103
|
+
ctx.tmux.setMeta(name, meta);
|
|
104
|
+
// A state change shows on attached terminals whoever reports it (spec §6, §7).
|
|
105
|
+
if (meta.state !== undefined) {
|
|
106
|
+
const note = meta.note ?? ctx.tmux.listSessions().find((s) => s.name === name)?.meta.note;
|
|
107
|
+
presentState(ctx.tmux, name, meta.state, { note });
|
|
108
|
+
}
|
|
109
|
+
// What a restore needs changed: record it now, not at the next session event.
|
|
110
|
+
if (['agent', 'cwd', 'argv', 'conversation', 'origin'].some((k) => k in meta))
|
|
111
|
+
snapshotQuietly(ctx);
|
|
112
|
+
}
|
|
113
|
+
/** Note that marks a waiting state raised by a terminal bell rather than a hook. */
|
|
114
|
+
export const BELL_NOTE = 'bell';
|
|
115
|
+
/**
|
|
116
|
+
* A terminal bell from a managed session (tmux alert-bell). Agents without
|
|
117
|
+
* Berth hooks get state `waiting`, so every agent can ask for attention; for
|
|
118
|
+
* agents whose hooks are installed the hooks are more precise and the bell
|
|
119
|
+
* is ignored. Returns whether the state changed.
|
|
120
|
+
*/
|
|
121
|
+
export function bellRang(ctx, name, env = process.env) {
|
|
122
|
+
const live = ctx.tmux.listSessions().find((s) => s.name === name);
|
|
123
|
+
const agent = live?.meta.agent;
|
|
124
|
+
if (!agent)
|
|
125
|
+
return false;
|
|
126
|
+
const adapter = getAdapter(agent);
|
|
127
|
+
if (adapter?.hookTarget && hooksInstalled(agent, env))
|
|
128
|
+
return false;
|
|
129
|
+
stamp(ctx, name, { state: 'waiting', note: BELL_NOTE });
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
/** Seeing a session answers its bell: clear a bell-raised waiting state. */
|
|
133
|
+
export function clearBell(ctx, name) {
|
|
134
|
+
const live = ctx.tmux.listSessions().find((s) => s.name === name);
|
|
135
|
+
if (live?.meta.state === 'waiting' && live.meta.note === BELL_NOTE)
|
|
136
|
+
stamp(ctx, name, { state: '', note: '' });
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=sessions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessions.js","sourceRoot":"","sources":["../../../src/registry/sessions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Context } from '../context.js';
|
|
2
|
+
import type { StoredSnapshot } from '../snapshot/store.js';
|
|
3
|
+
export type RestoreOutcome = 'resumed' | 'launched' | 'shell' | 'skipped-live' | 'skipped-missing-cwd';
|
|
4
|
+
export interface RestoreResult {
|
|
5
|
+
name: string;
|
|
6
|
+
outcome: RestoreOutcome;
|
|
7
|
+
detail?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface RestoreOptions {
|
|
10
|
+
origin?: string;
|
|
11
|
+
session?: string;
|
|
12
|
+
/** How long to wait for another restore to finish; editors pass a short one so they never freeze. */
|
|
13
|
+
lockTimeoutMs?: number;
|
|
14
|
+
}
|
|
15
|
+
/** Rebuild missing managed sessions from a snapshot; live ones are left alone. */
|
|
16
|
+
export declare function restoreFrom(ctx: Context, snapshot: StoredSnapshot | undefined, options?: RestoreOptions): RestoreResult[];
|
|
17
|
+
export declare function ensure(ctx: Context, options?: RestoreOptions): RestoreResult[];
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { delimiter, join } from 'node:path';
|
|
3
|
+
import { launchCommand, resumeCommand, requireAdapter } from '../agents/commands.js';
|
|
4
|
+
import { shellSplit } from '../shell.js';
|
|
5
|
+
import { withLock } from './lock.js';
|
|
6
|
+
function onPath(binary, env = process.env) {
|
|
7
|
+
if (!binary)
|
|
8
|
+
return true;
|
|
9
|
+
if (binary.includes('/'))
|
|
10
|
+
return existsSync(binary);
|
|
11
|
+
return (env.PATH ?? '').split(delimiter).some((dir) => dir && existsSync(join(dir, binary)));
|
|
12
|
+
}
|
|
13
|
+
function selected(s, options) {
|
|
14
|
+
if (options.session && s.name !== options.session)
|
|
15
|
+
return false;
|
|
16
|
+
if (options.origin && options.origin !== 'all' && s.origin !== options.origin)
|
|
17
|
+
return false;
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
function rebuild(ctx, s) {
|
|
21
|
+
if (!existsSync(s.cwd))
|
|
22
|
+
return { name: s.name, outcome: 'skipped-missing-cwd', detail: s.cwd };
|
|
23
|
+
const adapter = requireAdapter(s.agent);
|
|
24
|
+
const input = { adapter, name: s.name, cwd: s.cwd, overrides: ctx.config.agents };
|
|
25
|
+
const resumed = s.conversation
|
|
26
|
+
? resumeCommand({ ...input, conversation: s.conversation, originalArgv: s.argv })
|
|
27
|
+
: undefined;
|
|
28
|
+
let command = resumed ?? launchCommand({ ...input, extraArgs: [] });
|
|
29
|
+
let outcome = resumed ? 'resumed' : 'launched';
|
|
30
|
+
const [binary] = command ? shellSplit(command) : [''];
|
|
31
|
+
if (!onPath(binary ?? '')) {
|
|
32
|
+
command = '';
|
|
33
|
+
outcome = 'shell';
|
|
34
|
+
}
|
|
35
|
+
ctx.tmux.newSession(s.name, s.cwd, command);
|
|
36
|
+
ctx.tmux.setMeta(s.name, {
|
|
37
|
+
agent: s.agent,
|
|
38
|
+
cwd: s.cwd,
|
|
39
|
+
argv: s.argv,
|
|
40
|
+
conversation: s.conversation || undefined,
|
|
41
|
+
origin: s.origin,
|
|
42
|
+
});
|
|
43
|
+
ctx.tmux.setTitle(s.name);
|
|
44
|
+
return { name: s.name, outcome, detail: outcome === 'shell' ? `${binary} not found` : undefined };
|
|
45
|
+
}
|
|
46
|
+
/** Rebuild missing managed sessions from a snapshot; live ones are left alone. */
|
|
47
|
+
export function restoreFrom(ctx, snapshot, options = {}) {
|
|
48
|
+
if (!snapshot)
|
|
49
|
+
return [];
|
|
50
|
+
return withLock(ctx.config.stateDir, () => snapshot.sessions
|
|
51
|
+
.filter((s) => selected(s, options))
|
|
52
|
+
.map((s) => (ctx.tmux.hasSession(s.name) ? { name: s.name, outcome: 'skipped-live' } : rebuild(ctx, s))), options.lockTimeoutMs);
|
|
53
|
+
}
|
|
54
|
+
export function ensure(ctx, options = {}) {
|
|
55
|
+
return restoreFrom(ctx, ctx.store.newest(), options);
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=ensure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ensure.js","sourceRoot":"","sources":["../../../src/restore/ensure.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Directory lock (implementation §2.4): created atomically, holds the owner
|
|
3
|
+
* PID. An abandoned lock is taken over by renaming it away first, so two
|
|
4
|
+
* waiters cannot both remove it and both proceed. Others wait up to `timeoutMs`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function withLock<T>(stateDir: string, fn: () => T, timeoutMs?: number): T;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { BerthError } from '../errors.js';
|
|
4
|
+
/** A lock directory with no PID yet is its owner's first moment; older than this, the owner died there. */
|
|
5
|
+
const PIDLESS_GRACE_MS = 10_000;
|
|
6
|
+
function alive(pid) {
|
|
7
|
+
try {
|
|
8
|
+
process.kill(pid, 0);
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
return error.code === 'EPERM';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function sleep(ms) {
|
|
16
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
17
|
+
}
|
|
18
|
+
/** Whether the lock at `dir` belongs to nobody any more. */
|
|
19
|
+
function abandoned(dir) {
|
|
20
|
+
try {
|
|
21
|
+
const owner = Number(readFileSync(join(dir, 'pid'), 'utf8'));
|
|
22
|
+
return Number.isInteger(owner) && owner > 0 && !alive(owner);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
try {
|
|
26
|
+
return Date.now() - statSync(dir).mtimeMs > PIDLESS_GRACE_MS;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false; // released meanwhile
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Directory lock (implementation §2.4): created atomically, holds the owner
|
|
35
|
+
* PID. An abandoned lock is taken over by renaming it away first, so two
|
|
36
|
+
* waiters cannot both remove it and both proceed. Others wait up to `timeoutMs`.
|
|
37
|
+
*/
|
|
38
|
+
export function withLock(stateDir, fn, timeoutMs = 30_000) {
|
|
39
|
+
mkdirSync(stateDir, { recursive: true });
|
|
40
|
+
const dir = join(stateDir, 'ensure.lock');
|
|
41
|
+
const deadline = Date.now() + timeoutMs;
|
|
42
|
+
for (;;) {
|
|
43
|
+
try {
|
|
44
|
+
mkdirSync(dir);
|
|
45
|
+
writeFileSync(join(dir, 'pid'), String(process.pid));
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (error.code !== 'EEXIST')
|
|
50
|
+
throw error;
|
|
51
|
+
if (abandoned(dir)) {
|
|
52
|
+
const stale = `${dir}.stale-${process.pid}-${Date.now()}`;
|
|
53
|
+
try {
|
|
54
|
+
renameSync(dir, stale);
|
|
55
|
+
rmSync(stale, { recursive: true, force: true });
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Another waiter took it over first; try again.
|
|
59
|
+
}
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (Date.now() > deadline)
|
|
63
|
+
throw new BerthError(`another restore is running (${dir})`);
|
|
64
|
+
sleep(100);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
return fn();
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
rmSync(dir, { recursive: true, force: true });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock.js","sourceRoot":"","sources":["../../../src/restore/lock.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC/F,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,2GAA2G;AAC3G,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAEhC,SAAS,KAAK,CAAC,GAAW;IACxB,IAAI,CAAC;QACH,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACrB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAQ,KAA+B,CAAC,IAAI,KAAK,OAAO,CAAC;IAC3D,CAAC;AACH,CAAC;AAED,SAAS,KAAK,CAAC,EAAU;IACvB,OAAO,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,IAAI,iBAAiB,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;AACnE,CAAC;AAED,4DAA4D;AAC5D,SAAS,SAAS,CAAC,GAAW;IAC5B,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;QAC7D,OAAO,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC/D,CAAC;IAAC,MAAM,CAAC;QACP,IAAI,CAAC;YACH,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,GAAG,gBAAgB,CAAC;QAC/D,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC,CAAC,qBAAqB;QACrC,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,QAAQ,CAAI,QAAgB,EAAE,EAAW,EAAE,SAAS,GAAG,MAAM;IAC3E,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACzC,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC;IAC1C,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC;IACxC,SAAS,CAAC;QACR,IAAI,CAAC;YACH,SAAS,CAAC,GAAG,CAAC,CAAC;YACf,aAAa,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;YACrD,MAAM;QACR,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;gBAAE,MAAM,KAAK,CAAC;YACpE,IAAI,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC;gBACnB,MAAM,KAAK,GAAG,GAAG,GAAG,UAAU,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;gBAC1D,IAAI,CAAC;oBACH,UAAU,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;oBACvB,MAAM,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;gBAClD,CAAC;gBAAC,MAAM,CAAC;oBACP,gDAAgD;gBAClD,CAAC;gBACD,SAAS;YACX,CAAC;YACD,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ;gBAAE,MAAM,IAAI,UAAU,CAAC,+BAA+B,GAAG,GAAG,CAAC,CAAC;YACvF,KAAK,CAAC,GAAG,CAAC,CAAC;QACb,CAAC;IACH,CAAC;IACD,IAAI,CAAC;QACH,OAAO,EAAE,EAAE,CAAC;IACd,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAChD,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Minimal POSIX shell word handling for launch commands. */
|
|
2
|
+
export declare function shellQuote(word: string): string;
|
|
3
|
+
/** Split a command line into words: honours single quotes, double quotes and backslashes. */
|
|
4
|
+
export declare function shellSplit(line: string): string[];
|
|
5
|
+
/** Fill `{key}` placeholders; values are shell-quoted, `{args}` is inserted as already-quoted words. */
|
|
6
|
+
export declare function renderTemplate(template: string, values: Record<string, string>): string;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** Minimal POSIX shell word handling for launch commands. */
|
|
2
|
+
// No '=': zsh expands a word starting with '=' to a command path.
|
|
3
|
+
const SAFE = /^[A-Za-z0-9_@%+:,./-]+$/;
|
|
4
|
+
export function shellQuote(word) {
|
|
5
|
+
if (word === '')
|
|
6
|
+
return "''";
|
|
7
|
+
if (SAFE.test(word))
|
|
8
|
+
return word;
|
|
9
|
+
return `'${word.replace(/'/g, `'\\''`)}'`;
|
|
10
|
+
}
|
|
11
|
+
/** Split a command line into words: honours single quotes, double quotes and backslashes. */
|
|
12
|
+
export function shellSplit(line) {
|
|
13
|
+
const words = [];
|
|
14
|
+
let current = '';
|
|
15
|
+
let inWord = false;
|
|
16
|
+
let quote = null;
|
|
17
|
+
for (let i = 0; i < line.length; i++) {
|
|
18
|
+
const ch = line[i];
|
|
19
|
+
if (quote === "'") {
|
|
20
|
+
if (ch === "'")
|
|
21
|
+
quote = null;
|
|
22
|
+
else
|
|
23
|
+
current += ch;
|
|
24
|
+
}
|
|
25
|
+
else if (quote === '"') {
|
|
26
|
+
if (ch === '"')
|
|
27
|
+
quote = null;
|
|
28
|
+
else if (ch === '\\' && i + 1 < line.length && '"\\$`'.includes(line[i + 1]))
|
|
29
|
+
current += line[++i];
|
|
30
|
+
else
|
|
31
|
+
current += ch;
|
|
32
|
+
}
|
|
33
|
+
else if (ch === "'" || ch === '"') {
|
|
34
|
+
quote = ch;
|
|
35
|
+
inWord = true;
|
|
36
|
+
}
|
|
37
|
+
else if (ch === '\\' && i + 1 < line.length) {
|
|
38
|
+
current += line[++i];
|
|
39
|
+
inWord = true;
|
|
40
|
+
}
|
|
41
|
+
else if (/\s/.test(ch)) {
|
|
42
|
+
if (inWord)
|
|
43
|
+
words.push(current);
|
|
44
|
+
current = '';
|
|
45
|
+
inWord = false;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
current += ch;
|
|
49
|
+
inWord = true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (quote)
|
|
53
|
+
throw new Error(`unterminated quote in: ${line}`);
|
|
54
|
+
if (inWord)
|
|
55
|
+
words.push(current);
|
|
56
|
+
return words;
|
|
57
|
+
}
|
|
58
|
+
/** Fill `{key}` placeholders; values are shell-quoted, `{args}` is inserted as already-quoted words. */
|
|
59
|
+
export function renderTemplate(template, values) {
|
|
60
|
+
return template
|
|
61
|
+
.replace(/\{(\w+)\}/g, (match, key) => {
|
|
62
|
+
if (!(key in values))
|
|
63
|
+
return match;
|
|
64
|
+
return key === 'args' ? values[key] : shellQuote(values[key]);
|
|
65
|
+
})
|
|
66
|
+
.replace(/\s+$/, '');
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=shell.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shell.js","sourceRoot":"","sources":["../../src/shell.ts"],"names":[],"mappings":"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"}
|