@hwip/cli 0.0.1 → 0.1.1
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 +190 -0
- package/NOTICE +11 -0
- package/README.md +1983 -0
- package/dist/agent-list.d.ts +76 -0
- package/dist/agent-list.js +102 -0
- package/dist/agent-read.d.ts +105 -0
- package/dist/agent-read.js +309 -0
- package/dist/agent-rules-fetch.d.ts +51 -0
- package/dist/agent-rules-fetch.js +275 -0
- package/dist/agent-rules.default.d.ts +30 -0
- package/dist/agent-rules.default.js +305 -0
- package/dist/agent-session.d.ts +23 -0
- package/dist/agent-session.js +44 -0
- package/dist/agent-state.d.ts +84 -0
- package/dist/agent-state.js +188 -0
- package/dist/agent-target.d.ts +60 -0
- package/dist/agent-target.js +81 -0
- package/dist/agents.d.ts +31 -0
- package/dist/agents.js +75 -0
- package/dist/api.d.ts +28 -0
- package/dist/api.js +76 -0
- package/dist/args.d.ts +74 -0
- package/dist/args.js +254 -0
- package/dist/ask-context-attach.d.ts +73 -0
- package/dist/ask-context-attach.js +151 -0
- package/dist/ask-context-screen.d.ts +21 -0
- package/dist/ask-context-screen.js +49 -0
- package/dist/ask-context-switch.d.ts +31 -0
- package/dist/ask-context-switch.js +74 -0
- package/dist/ask-context.d.ts +84 -0
- package/dist/ask-context.js +172 -0
- package/dist/ask-pending.d.ts +47 -0
- package/dist/ask-pending.js +221 -0
- package/dist/ask-seal.d.ts +120 -0
- package/dist/ask-seal.js +179 -0
- package/dist/ask.d.ts +107 -0
- package/dist/ask.js +551 -0
- package/dist/blocked-prompt-seal.d.ts +39 -0
- package/dist/blocked-prompt-seal.js +92 -0
- package/dist/blocked-prompt.d.ts +62 -0
- package/dist/blocked-prompt.js +518 -0
- package/dist/channel.d.ts +67 -0
- package/dist/channel.js +136 -0
- package/dist/check-update.d.ts +28 -0
- package/dist/check-update.js +153 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +813 -0
- package/dist/codex-config.d.ts +24 -0
- package/dist/codex-config.js +169 -0
- package/dist/codex-transcript.d.ts +59 -0
- package/dist/codex-transcript.js +373 -0
- package/dist/command-refusal.d.ts +56 -0
- package/dist/command-refusal.js +127 -0
- package/dist/command-scan.d.ts +46 -0
- package/dist/command-scan.js +154 -0
- package/dist/command-seal.d.ts +57 -0
- package/dist/command-seal.js +88 -0
- package/dist/command-seq.d.ts +30 -0
- package/dist/command-seq.js +150 -0
- package/dist/config.d.ts +156 -0
- package/dist/config.js +392 -0
- package/dist/core-sections.d.ts +18 -0
- package/dist/core-sections.js +28 -0
- package/dist/crypto.d.ts +278 -0
- package/dist/crypto.js +698 -0
- package/dist/desktop-notify.d.ts +33 -0
- package/dist/desktop-notify.js +49 -0
- package/dist/device-directory.d.ts +36 -0
- package/dist/device-directory.js +97 -0
- package/dist/device-presence.d.ts +17 -0
- package/dist/device-presence.js +62 -0
- package/dist/downloads.d.ts +29 -0
- package/dist/downloads.js +106 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +28 -0
- package/dist/file-crypto.d.ts +94 -0
- package/dist/file-crypto.js +278 -0
- package/dist/file-view-budget.d.ts +15 -0
- package/dist/file-view-budget.js +34 -0
- package/dist/file-view-switch.d.ts +28 -0
- package/dist/file-view-switch.js +122 -0
- package/dist/file-view.d.ts +108 -0
- package/dist/file-view.js +327 -0
- package/dist/gate.d.ts +141 -0
- package/dist/gate.js +336 -0
- package/dist/gemini-transcript.d.ts +32 -0
- package/dist/gemini-transcript.js +189 -0
- package/dist/http-failure.d.ts +98 -0
- package/dist/http-failure.js +349 -0
- package/dist/hwip-core.generated.d.ts +7 -0
- package/dist/hwip-core.generated.js +14 -0
- package/dist/hwip-hook.d.ts +77 -0
- package/dist/hwip-hook.js +340 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +14 -0
- package/dist/input-sequencer.d.ts +65 -0
- package/dist/input-sequencer.js +142 -0
- package/dist/insecure-url.d.ts +16 -0
- package/dist/insecure-url.js +76 -0
- package/dist/installer.d.ts +96 -0
- package/dist/installer.js +850 -0
- package/dist/inventory-offload.d.ts +49 -0
- package/dist/inventory-offload.js +209 -0
- package/dist/inventory-worker.d.ts +2 -0
- package/dist/inventory-worker.js +27 -0
- package/dist/jsonc-doc.d.ts +106 -0
- package/dist/jsonc-doc.js +354 -0
- package/dist/keep-awake.d.ts +32 -0
- package/dist/keep-awake.js +46 -0
- package/dist/key-changes.d.ts +48 -0
- package/dist/key-changes.js +140 -0
- package/dist/lan-auth.d.ts +140 -0
- package/dist/lan-auth.js +217 -0
- package/dist/lan-endpoint.d.ts +41 -0
- package/dist/lan-endpoint.js +115 -0
- package/dist/lan-receiver.d.ts +122 -0
- package/dist/lan-receiver.js +510 -0
- package/dist/lan-sender.d.ts +76 -0
- package/dist/lan-sender.js +184 -0
- package/dist/lan-transfer.d.ts +36 -0
- package/dist/lan-transfer.js +54 -0
- package/dist/listener-device-id.d.ts +20 -0
- package/dist/listener-device-id.js +133 -0
- package/dist/listener-log.d.ts +4 -0
- package/dist/listener-log.js +23 -0
- package/dist/listener-process.d.ts +200 -0
- package/dist/listener-process.js +470 -0
- package/dist/listener-service.d.ts +225 -0
- package/dist/listener-service.js +682 -0
- package/dist/listener-worktree-cleanup.d.ts +27 -0
- package/dist/listener-worktree-cleanup.js +62 -0
- package/dist/listener.d.ts +1325 -0
- package/dist/listener.js +5681 -0
- package/dist/login-address.d.ts +25 -0
- package/dist/login-address.js +97 -0
- package/dist/login-page.d.ts +29 -0
- package/dist/login-page.js +93 -0
- package/dist/login.d.ts +98 -0
- package/dist/login.js +388 -0
- package/dist/mcp-command.d.ts +45 -0
- package/dist/mcp-command.js +50 -0
- package/dist/mcp.d.ts +19 -0
- package/dist/mcp.js +29 -0
- package/dist/node-support.d.ts +19 -0
- package/dist/node-support.js +38 -0
- package/dist/notify-failure.d.ts +57 -0
- package/dist/notify-failure.js +150 -0
- package/dist/opencode-transcript.d.ts +139 -0
- package/dist/opencode-transcript.js +390 -0
- package/dist/optional-features.d.ts +6 -0
- package/dist/optional-features.js +19 -0
- package/dist/pane-fit.d.ts +40 -0
- package/dist/pane-fit.js +119 -0
- package/dist/peer-key.d.ts +33 -0
- package/dist/peer-key.js +34 -0
- package/dist/pi-transcript.d.ts +26 -0
- package/dist/pi-transcript.js +255 -0
- package/dist/presence.d.ts +14 -0
- package/dist/presence.js +124 -0
- package/dist/private-files.d.ts +51 -0
- package/dist/private-files.js +218 -0
- package/dist/project-switch.d.ts +19 -0
- package/dist/project-switch.js +60 -0
- package/dist/redact.d.ts +10 -0
- package/dist/redact.js +21 -0
- package/dist/remote-agents.d.ts +410 -0
- package/dist/remote-agents.js +1218 -0
- package/dist/remote-hook.d.ts +12 -0
- package/dist/remote-hook.js +196 -0
- package/dist/replace-file.d.ts +50 -0
- package/dist/replace-file.js +328 -0
- package/dist/schedule-command.d.ts +35 -0
- package/dist/schedule-command.js +260 -0
- package/dist/schedule-launch.d.ts +15 -0
- package/dist/schedule-launch.js +24 -0
- package/dist/schedule-ready.d.ts +44 -0
- package/dist/schedule-ready.js +71 -0
- package/dist/schedule-remote.d.ts +77 -0
- package/dist/schedule-remote.js +167 -0
- package/dist/schedule-runner.d.ts +89 -0
- package/dist/schedule-runner.js +452 -0
- package/dist/schedule.d.ts +149 -0
- package/dist/schedule.js +428 -0
- package/dist/secret-mask.d.ts +29 -0
- package/dist/secret-mask.js +245 -0
- package/dist/self-register.d.ts +45 -0
- package/dist/self-register.js +67 -0
- package/dist/send.d.ts +56 -0
- package/dist/send.js +104 -0
- package/dist/session-diff.d.ts +161 -0
- package/dist/session-diff.js +445 -0
- package/dist/session-registry.d.ts +143 -0
- package/dist/session-registry.js +216 -0
- package/dist/sessions-report.d.ts +59 -0
- package/dist/sessions-report.js +99 -0
- package/dist/shell-audit.d.ts +34 -0
- package/dist/shell-audit.js +47 -0
- package/dist/subagent-transcripts.d.ts +167 -0
- package/dist/subagent-transcripts.js +437 -0
- package/dist/subscriber-trust.d.ts +128 -0
- package/dist/subscriber-trust.js +397 -0
- package/dist/templates.d.ts +105 -0
- package/dist/templates.js +871 -0
- package/dist/tmux-install.d.ts +44 -0
- package/dist/tmux-install.js +65 -0
- package/dist/tmux-liveness.d.ts +45 -0
- package/dist/tmux-liveness.js +71 -0
- package/dist/tmux-pane.d.ts +55 -0
- package/dist/tmux-pane.js +126 -0
- package/dist/tmux-socket.d.ts +20 -0
- package/dist/tmux-socket.js +292 -0
- package/dist/todo-fold.d.ts +31 -0
- package/dist/todo-fold.js +102 -0
- package/dist/transcript-tail.d.ts +312 -0
- package/dist/transcript-tail.js +718 -0
- package/dist/trust-command.d.ts +49 -0
- package/dist/trust-command.js +200 -0
- package/dist/trust-lock.d.ts +26 -0
- package/dist/trust-lock.js +94 -0
- package/dist/trust-pins.d.ts +37 -0
- package/dist/trust-pins.js +82 -0
- package/dist/turn-recorder-wire.d.ts +70 -0
- package/dist/turn-recorder-wire.js +165 -0
- package/dist/turn-recorder.d.ts +190 -0
- package/dist/turn-recorder.js +695 -0
- package/dist/turn-ring.d.ts +93 -0
- package/dist/turn-ring.js +418 -0
- package/dist/turn-watch.d.ts +177 -0
- package/dist/turn-watch.js +630 -0
- package/dist/types.d.ts +63 -0
- package/dist/types.js +2 -0
- package/dist/uninstall.d.ts +55 -0
- package/dist/uninstall.js +507 -0
- package/dist/usage.d.ts +17 -0
- package/dist/usage.js +421 -0
- package/dist/verify.d.ts +47 -0
- package/dist/verify.js +396 -0
- package/dist/worktree.d.ts +144 -0
- package/dist/worktree.js +310 -0
- package/dist/wrapper.d.ts +254 -0
- package/dist/wrapper.js +704 -0
- package/dist/ws-url.d.ts +109 -0
- package/dist/ws-url.js +202 -0
- package/package.json +78 -10
- package/bin/hwip.js +0 -3
package/dist/worktree.js
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `hwip <agent> --wt <name>`: a git worktree made for a session, and taken back
|
|
4
|
+
* when the session is over and nothing in it would be lost.
|
|
5
|
+
*
|
|
6
|
+
* hwip only ever removes a worktree it made itself — the rows in
|
|
7
|
+
* `<stateDir>/worktrees.json` are the whole list. A worktree the user made by
|
|
8
|
+
* hand, or one `--wt` merely found at the path, has no row and no code path
|
|
9
|
+
* here that removes it. Every doubt about a worktree (a git call that fails or
|
|
10
|
+
* times out, a count that does not parse) reads as "keep it": the cost of a
|
|
11
|
+
* leftover directory is a line in `hwip worktree`, the cost of a wrong removal
|
|
12
|
+
* is somebody's work. `--force`, `rm -rf` and `branch -D` are not used anywhere.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.readPaneCwds = exports.worktreeStatuses = exports.reconcileWorktrees = exports.inspectWorktree = exports.ensureWorktree = exports.isValidBranchName = exports.worktreeDirName = exports.updateWorktreeRows = exports.readWorktreeRows = exports.tmuxRun = exports.gitRun = exports.commandRunner = void 0;
|
|
16
|
+
const child_process_1 = require("child_process");
|
|
17
|
+
const fs_1 = require("fs");
|
|
18
|
+
const path_1 = require("path");
|
|
19
|
+
const config_js_1 = require("./config.js");
|
|
20
|
+
const gate_js_1 = require("./gate.js");
|
|
21
|
+
const private_files_js_1 = require("./private-files.js");
|
|
22
|
+
const replace_file_js_1 = require("./replace-file.js");
|
|
23
|
+
const GIT_TIMEOUT_MS = 10_000;
|
|
24
|
+
const firstLine = (text) => text.trim().split('\n')[0] ?? '';
|
|
25
|
+
/** `execFile` of one command with argv (no shell: branch names and paths are user input), as a `GitRun`. */
|
|
26
|
+
const commandRunner = (cmd) => (args, timeoutMs = GIT_TIMEOUT_MS) => new Promise((resolve) => {
|
|
27
|
+
(0, child_process_1.execFile)(cmd, args, { encoding: 'utf-8', timeout: timeoutMs }, (err, stdout, stderr) => {
|
|
28
|
+
if (!err)
|
|
29
|
+
return resolve({ ok: true, stdout });
|
|
30
|
+
if ('code' in err && err.code === 'ENOENT')
|
|
31
|
+
return resolve({ ok: false, error: `${cmd} not found` });
|
|
32
|
+
const timedOut = 'killed' in err && err.killed;
|
|
33
|
+
resolve({ ok: false, error: timedOut ? `${cmd} timed out` : firstLine(stderr) || err.message });
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
exports.commandRunner = commandRunner;
|
|
37
|
+
exports.gitRun = (0, exports.commandRunner)('git');
|
|
38
|
+
exports.tmuxRun = (0, exports.commandRunner)('tmux');
|
|
39
|
+
const registryPath = () => (0, path_1.join)((0, gate_js_1.stateDir)(), 'worktrees.json');
|
|
40
|
+
const isRow = (e) => {
|
|
41
|
+
if (!e || typeof e !== 'object')
|
|
42
|
+
return false;
|
|
43
|
+
const row = e; // shape-checked field by field below
|
|
44
|
+
return ['path', 'root', 'branch', 'baseSha', 'createdAt'].every((k) => typeof row[k] === 'string')
|
|
45
|
+
&& typeof row.createdBranch === 'boolean'
|
|
46
|
+
&& (row.reasons === undefined || (Array.isArray(row.reasons) && row.reasons.every((r) => typeof r === 'string')));
|
|
47
|
+
};
|
|
48
|
+
/** Rows that still make sense: a half-written or hand-edited file must not stop a launch. */
|
|
49
|
+
const parseRows = (text) => {
|
|
50
|
+
if (!text)
|
|
51
|
+
return [];
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(text);
|
|
54
|
+
return Array.isArray(parsed) ? parsed.filter(isRow) : [];
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const readWorktreeRows = () => {
|
|
61
|
+
try {
|
|
62
|
+
return parseRows((0, fs_1.readFileSync)(registryPath(), 'utf-8'));
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
exports.readWorktreeRows = readWorktreeRows;
|
|
69
|
+
/**
|
|
70
|
+
* Read, change, write. A row that cannot be written is a worktree that is not
|
|
71
|
+
* cleaned up automatically — the launch goes on. `expected` turns a write by
|
|
72
|
+
* the other process (wrapper vs listener) since the read into an error, which
|
|
73
|
+
* lands here too: a lost row only ever means "kept".
|
|
74
|
+
*/
|
|
75
|
+
const updateWorktreeRows = (change) => {
|
|
76
|
+
const path = registryPath();
|
|
77
|
+
try {
|
|
78
|
+
const text = (0, replace_file_js_1.readTextIfExists)(path);
|
|
79
|
+
(0, replace_file_js_1.replaceFile)(path, JSON.stringify(change(parseRows(text)), null, 2), text, 0o600);
|
|
80
|
+
(0, private_files_js_1.handBackToSudoUser)(path);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
/* the registry is a convenience, not a gate */
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
exports.updateWorktreeRows = updateWorktreeRows;
|
|
87
|
+
const refuse = (error, usage = false) => ({ ok: false, error, usage });
|
|
88
|
+
/** The worktrees `git worktree list --porcelain` knows: realpath → branch ref (null when detached). */
|
|
89
|
+
const listWorktrees = async (root, run) => {
|
|
90
|
+
const out = await run(['-C', root, 'worktree', 'list', '--porcelain']);
|
|
91
|
+
if (!out.ok)
|
|
92
|
+
return null;
|
|
93
|
+
const found = new Map();
|
|
94
|
+
for (const block of out.stdout.split('\n\n')) {
|
|
95
|
+
const lines = block.split('\n');
|
|
96
|
+
const path = lines.find((l) => l.startsWith('worktree '))?.slice('worktree '.length);
|
|
97
|
+
if (!path)
|
|
98
|
+
continue;
|
|
99
|
+
found.set(path, lines.find((l) => l.startsWith('branch '))?.slice('branch '.length) ?? null);
|
|
100
|
+
}
|
|
101
|
+
return found;
|
|
102
|
+
};
|
|
103
|
+
const realOrSelf = (path) => {
|
|
104
|
+
try {
|
|
105
|
+
return (0, fs_1.realpathSync)(path);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return path;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* The directory name of the worktree for branch `name` of the repo at `root`: `<repo>-wt-<name with separators as dashes>`.
|
|
113
|
+
* Null when nothing but separators is left. `hwip schedule add` names its session from this, so the two cannot drift.
|
|
114
|
+
*/
|
|
115
|
+
const worktreeDirName = (root, name) => {
|
|
116
|
+
const slug = name.replace(/[/.:;\s]+/g, '-');
|
|
117
|
+
return slug.replace(/-/g, '') ? `${(0, path_1.basename)(root)}-wt-${slug}` : null;
|
|
118
|
+
};
|
|
119
|
+
exports.worktreeDirName = worktreeDirName;
|
|
120
|
+
/** Whether `name` is a branch name git accepts as it stands (`--branch` also expands `@{-1}`-style names; one that changes is not what was typed). */
|
|
121
|
+
const isValidBranchName = async (root, name, run = exports.gitRun) => {
|
|
122
|
+
const checked = await run(['-C', root, 'check-ref-format', '--branch', name]);
|
|
123
|
+
return checked.ok && checked.stdout.trim() === name;
|
|
124
|
+
};
|
|
125
|
+
exports.isValidBranchName = isValidBranchName;
|
|
126
|
+
/**
|
|
127
|
+
* The worktree for branch `name` next to the main checkout of the repo `dir` is
|
|
128
|
+
* in — made, or found already made by this same command. Anything else at the
|
|
129
|
+
* path is refused and left alone.
|
|
130
|
+
*/
|
|
131
|
+
const ensureWorktree = async ({ dir, name }, run = exports.gitRun) => {
|
|
132
|
+
const root = (0, config_js_1.checkoutOf)(dir)?.root;
|
|
133
|
+
if (!root)
|
|
134
|
+
return refuse('--wt needs a git checkout with a main worktree');
|
|
135
|
+
if (!name || name.startsWith('-'))
|
|
136
|
+
return refuse(`--wt needs a branch name (got '${name}')`, true);
|
|
137
|
+
if (!(await (0, exports.isValidBranchName)(root, name, run)))
|
|
138
|
+
return refuse(`'${name}' is not a valid branch name`, true);
|
|
139
|
+
const dirName = (0, exports.worktreeDirName)(root, name);
|
|
140
|
+
if (!dirName)
|
|
141
|
+
return refuse(`'${name}' is not a usable worktree name`, true);
|
|
142
|
+
const path = (0, path_1.join)((0, path_1.dirname)(root), dirName);
|
|
143
|
+
const known = await listWorktrees(root, run);
|
|
144
|
+
if (!known)
|
|
145
|
+
return refuse(`could not list worktrees of ${root}`);
|
|
146
|
+
const there = [...known].find(([listed]) => realOrSelf(listed) === realOrSelf(path));
|
|
147
|
+
const present = (0, fs_1.lstatSync)(path, { throwIfNoEntry: false }) !== undefined;
|
|
148
|
+
if (present && there?.[1] === `refs/heads/${name}`)
|
|
149
|
+
return { ok: true, path: realOrSelf(path), branch: name, created: false };
|
|
150
|
+
// Registered but gone, a plain directory, a file, a worktree of another branch: none of them is ours to touch.
|
|
151
|
+
if (present || there)
|
|
152
|
+
return refuse(`${path} exists and is not the worktree for ${name}`);
|
|
153
|
+
const branchExists = (await run(['-C', root, 'show-ref', '--verify', '--quiet', `refs/heads/${name}`])).ok;
|
|
154
|
+
// A new branch starts where the command was typed; one that exists is checked out as it is.
|
|
155
|
+
const added = branchExists
|
|
156
|
+
? await run(['-C', root, 'worktree', 'add', path, name], 0)
|
|
157
|
+
: await run(['-C', dir, 'worktree', 'add', '-b', name, path], 0);
|
|
158
|
+
if (!added.ok)
|
|
159
|
+
return refuse(added.error);
|
|
160
|
+
const made = realOrSelf(path);
|
|
161
|
+
const head = await run(['-C', made, 'rev-parse', 'HEAD']);
|
|
162
|
+
if (head.ok) {
|
|
163
|
+
const row = {
|
|
164
|
+
path: made, root, branch: name, baseSha: head.stdout.trim(),
|
|
165
|
+
createdBranch: !branchExists, createdAt: new Date().toISOString(),
|
|
166
|
+
};
|
|
167
|
+
(0, exports.updateWorktreeRows)((rows) => [...rows.filter((r) => r.path !== made), row]);
|
|
168
|
+
}
|
|
169
|
+
return { ok: true, path: made, branch: name, created: true };
|
|
170
|
+
};
|
|
171
|
+
exports.ensureWorktree = ensureWorktree;
|
|
172
|
+
const plural = (n, noun) => `${n} ${noun}${n === 1 ? '' : 's'}`;
|
|
173
|
+
/**
|
|
174
|
+
* Whether this worktree can go without losing anything. Asked in git's own
|
|
175
|
+
* terms rather than left to `git worktree remove`, which deletes a worktree
|
|
176
|
+
* holding only ignored files (a hand-written `.env`) and one whose commits
|
|
177
|
+
* were never pushed (measured, git 2.47.0). Both checks always run, so the
|
|
178
|
+
* reasons are the whole list; a check that cannot be answered is a reason too.
|
|
179
|
+
*/
|
|
180
|
+
const inspectWorktree = async (row, run = exports.gitRun) => {
|
|
181
|
+
const reasons = [];
|
|
182
|
+
// A row ties a worktree to its path only. If the user removed this one and made their own at the same path, it is theirs.
|
|
183
|
+
const known = await listWorktrees(row.root, run);
|
|
184
|
+
const listed = known ? [...known].find(([path]) => realOrSelf(path) === realOrSelf(row.path)) : undefined;
|
|
185
|
+
if (!known)
|
|
186
|
+
reasons.push('could not check: worktrees not listed');
|
|
187
|
+
else if (!listed)
|
|
188
|
+
reasons.push('not a worktree git knows any more');
|
|
189
|
+
else if (listed[1] !== `refs/heads/${row.branch}`)
|
|
190
|
+
reasons.push(`no longer on branch ${row.branch}`);
|
|
191
|
+
// Explicit, because `status.showUntrackedFiles=no` and `diff.ignoreSubmodules` would otherwise hide what `worktree remove` deletes.
|
|
192
|
+
const status = await run(['-C', row.path, 'status', '--porcelain', '--ignored', '--untracked-files=normal', '--ignore-submodules=none']);
|
|
193
|
+
if (!status.ok) {
|
|
194
|
+
reasons.push(`could not check: ${status.error}`);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
const lines = status.stdout.split('\n').filter(Boolean);
|
|
198
|
+
const ignored = lines.filter((l) => l.startsWith('!!')).length;
|
|
199
|
+
if (lines.length > ignored)
|
|
200
|
+
reasons.push(`${lines.length - ignored} uncommitted or untracked`);
|
|
201
|
+
if (ignored)
|
|
202
|
+
reasons.push(`${ignored} ignored`);
|
|
203
|
+
}
|
|
204
|
+
// Not `--all`: that includes this worktree's own HEAD and always counts it.
|
|
205
|
+
const ahead = await run(['-C', row.path, 'rev-list', '--count', 'HEAD', '--not', row.baseSha, '--remotes']);
|
|
206
|
+
const count = ahead.ok ? Number(ahead.stdout.trim()) : NaN;
|
|
207
|
+
if (!ahead.ok)
|
|
208
|
+
reasons.push(`could not check: ${ahead.error}`);
|
|
209
|
+
else if (!Number.isInteger(count))
|
|
210
|
+
reasons.push('could not check: unreadable commit count');
|
|
211
|
+
else if (count > 0)
|
|
212
|
+
reasons.push(`${plural(count, 'unpushed commit')}`);
|
|
213
|
+
const distinct = [...new Set(reasons)];
|
|
214
|
+
return distinct.length ? { removable: false, reasons: distinct } : { removable: true };
|
|
215
|
+
};
|
|
216
|
+
exports.inspectWorktree = inspectWorktree;
|
|
217
|
+
const within = (cwd, path) => cwd === path || cwd.startsWith(path + path_1.sep);
|
|
218
|
+
/**
|
|
219
|
+
* Which of the worktrees hwip made to take back, and which to keep and say why.
|
|
220
|
+
*
|
|
221
|
+
* Automatic (`manual` off): a worktree is judged once, after a pane was last
|
|
222
|
+
* seen inside it and none is now. Judging again later would delete a directory
|
|
223
|
+
* somebody went back into and pushed from, so a worktree that was kept stays
|
|
224
|
+
* kept until `hwip worktree clean`, or until a session uses it again.
|
|
225
|
+
*
|
|
226
|
+
* Rows are re-read when written, so a row the wrapper added while git was
|
|
227
|
+
* being asked about is not lost.
|
|
228
|
+
*/
|
|
229
|
+
const reconcileWorktrees = async (input, run = exports.gitRun) => {
|
|
230
|
+
const { manual = false, dryRun = false } = input;
|
|
231
|
+
const now = (input.now ?? new Date()).toISOString();
|
|
232
|
+
const panes = input.paneCwds.map(realOrSelf);
|
|
233
|
+
const report = { removed: [], kept: [], inUse: [], gone: [] };
|
|
234
|
+
const drop = new Set();
|
|
235
|
+
const patch = new Map();
|
|
236
|
+
for (const row of (0, exports.readWorktreeRows)()) {
|
|
237
|
+
if ((0, fs_1.lstatSync)(row.path, { throwIfNoEntry: false }) === undefined) {
|
|
238
|
+
report.gone.push(row.path);
|
|
239
|
+
drop.add(row.path);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (panes.some((cwd) => within(cwd, row.path))) {
|
|
243
|
+
report.inUse.push(row.path);
|
|
244
|
+
patch.set(row.path, { sessionSeenAt: now, keptAt: undefined, reasons: undefined });
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (!manual && (!row.sessionSeenAt || row.keptAt))
|
|
248
|
+
continue;
|
|
249
|
+
const verdict = await (0, exports.inspectWorktree)(row, run);
|
|
250
|
+
if (!verdict.removable) {
|
|
251
|
+
report.kept.push({ path: row.path, branch: row.branch, reasons: verdict.reasons, removeFailed: false });
|
|
252
|
+
if (!manual)
|
|
253
|
+
patch.set(row.path, { keptAt: now, reasons: verdict.reasons });
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (dryRun) {
|
|
257
|
+
report.removed.push({ path: row.path, branch: row.branch, branchDeleted: false });
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
// No `--force`: git refuses a worktree that gained a change since the check.
|
|
261
|
+
const removed = await run(['-C', row.root, 'worktree', 'remove', row.path]);
|
|
262
|
+
if (!removed.ok) {
|
|
263
|
+
report.kept.push({ path: row.path, branch: row.branch, reasons: [removed.error], removeFailed: true });
|
|
264
|
+
if (!manual)
|
|
265
|
+
patch.set(row.path, { keptAt: now, reasons: [removed.error] });
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
// `-d`, never `-D`: git refuses a branch that is not merged, and that branch stays.
|
|
269
|
+
const branchDeleted = row.createdBranch && (await run(['-C', row.root, 'branch', '-d', row.branch])).ok;
|
|
270
|
+
report.removed.push({ path: row.path, branch: row.branch, branchDeleted });
|
|
271
|
+
drop.add(row.path);
|
|
272
|
+
}
|
|
273
|
+
if (!dryRun && (drop.size || patch.size)) {
|
|
274
|
+
(0, exports.updateWorktreeRows)((rows) => rows
|
|
275
|
+
.filter((r) => !drop.has(r.path))
|
|
276
|
+
.map((r) => (patch.has(r.path) ? { ...r, ...patch.get(r.path) } : r)));
|
|
277
|
+
}
|
|
278
|
+
return report;
|
|
279
|
+
};
|
|
280
|
+
exports.reconcileWorktrees = reconcileWorktrees;
|
|
281
|
+
/** What `hwip worktree` shows: every row with its state now. Read-only. */
|
|
282
|
+
const worktreeStatuses = async (paneCwds, run = exports.gitRun) => {
|
|
283
|
+
const panes = paneCwds.map(realOrSelf);
|
|
284
|
+
return Promise.all((0, exports.readWorktreeRows)().map(async (row) => {
|
|
285
|
+
if ((0, fs_1.lstatSync)(row.path, { throwIfNoEntry: false }) === undefined)
|
|
286
|
+
return { row, state: 'directory gone' };
|
|
287
|
+
if (panes.some((cwd) => within(cwd, row.path)))
|
|
288
|
+
return { row, state: 'in use' };
|
|
289
|
+
const verdict = await (0, exports.inspectWorktree)(row, run);
|
|
290
|
+
if (!verdict.removable)
|
|
291
|
+
return { row, state: verdict.reasons.join(', ') };
|
|
292
|
+
return { row, state: row.sessionSeenAt ? 'clean' : 'never used' };
|
|
293
|
+
}));
|
|
294
|
+
};
|
|
295
|
+
exports.worktreeStatuses = worktreeStatuses;
|
|
296
|
+
// Measured on tmux 3.5a: a server that is not there says `no server running on <socket>` (stale socket file) or
|
|
297
|
+
// `error connecting to <socket> (No such file or directory)`; `(Permission denied)` and the rest are not "no panes".
|
|
298
|
+
const NO_TMUX_SERVER = /^(no server running on |error connecting to .+ \(No such file or directory\)$)/;
|
|
299
|
+
/**
|
|
300
|
+
* The cwd of every live tmux pane, `[]` when there is no tmux server, null when
|
|
301
|
+
* tmux could not be asked. The difference matters: an unreadable list read as
|
|
302
|
+
* empty would make every worktree in use look abandoned.
|
|
303
|
+
*/
|
|
304
|
+
const readPaneCwds = async (tmux = exports.tmuxRun) => {
|
|
305
|
+
const out = await tmux(['list-panes', '-a', '-F', '#{pane_current_path}']);
|
|
306
|
+
if (out.ok)
|
|
307
|
+
return out.stdout.split('\n').filter(Boolean);
|
|
308
|
+
return NO_TMUX_SERVER.test(out.error) ? [] : null;
|
|
309
|
+
};
|
|
310
|
+
exports.readPaneCwds = readPaneCwds;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { type TmuxInstallPlan } from './tmux-install.js';
|
|
2
|
+
import type { RemoteAgent } from './remote-agents.js';
|
|
3
|
+
/** basename(), with a stable fallback for edge paths like `/`. */
|
|
4
|
+
export declare const safeBasename: (path: string) => string;
|
|
5
|
+
/** The directory the session is named for: env > git root > cwd. Null when git is the answer and there is no repo. */
|
|
6
|
+
export declare const detectProjectDir: () => string;
|
|
7
|
+
/** Resolve a project name for the tmux session: env > git root > cwd basename. */
|
|
8
|
+
export declare const detectProjectName: () => string;
|
|
9
|
+
/**
|
|
10
|
+
* tmux commands, chained after `new`, that tell the listener which project
|
|
11
|
+
* the session belongs to when its name does not say so: a `--label` tail and a
|
|
12
|
+
* worktree's directory name both read as part of the project otherwise, and
|
|
13
|
+
* each opened a card of its own. Nothing when the name already says it all, so
|
|
14
|
+
* a plain family session is reported exactly as before. The rule is config
|
|
15
|
+
* `groupOf`, which the listener also applies to sessions started without these.
|
|
16
|
+
*
|
|
17
|
+
* Each option names its session (`-t =<session>:`). An untargeted `set-option`
|
|
18
|
+
* applies to the client's current session, and a launch from inside tmux (a
|
|
19
|
+
* `--detach` from an agent's pane) would relabel the caller instead of the new
|
|
20
|
+
* session (measured on tmux 3.5a).
|
|
21
|
+
*/
|
|
22
|
+
export declare const sessionSidecarArgs: (session: string, projectDir: string, label?: string) => string[];
|
|
23
|
+
/**
|
|
24
|
+
* tmux commands, chained after `new -A`, that point config
|
|
25
|
+
* `HERDR_SESSION_OPTIONS` at the herdr pane this attach runs in, or clear them
|
|
26
|
+
* outside herdr.
|
|
27
|
+
*/
|
|
28
|
+
export declare const herdrSessionArgs: (session: string, env?: NodeJS.ProcessEnv) => string[];
|
|
29
|
+
/**
|
|
30
|
+
* `hwip-<project>` — the canonical tmux session base name. tmux itself turns
|
|
31
|
+
* `.` and `:` into `_` when it creates a session (3.5a: `hwip-foo.bar` becomes
|
|
32
|
+
* `hwip-foo_bar`), so the name is written that way up front: every later
|
|
33
|
+
* `-t =<name>` must match what tmux actually holds. `;` goes too — an argv
|
|
34
|
+
* element ending in one is a tmux command separator.
|
|
35
|
+
*/
|
|
36
|
+
export declare const tmuxSessionName: (project: string) => string;
|
|
37
|
+
/** The first name in the family no live session holds — for a launch that can only create. */
|
|
38
|
+
export declare const firstFreeSession: (base: string) => string;
|
|
39
|
+
/**
|
|
40
|
+
* Pick a tmux session name that won't steal focus from another live
|
|
41
|
+
* `hwip cc`. Strategy:
|
|
42
|
+
* - Reuse the **highest-numbered detached** session in the family.
|
|
43
|
+
* - Otherwise take the lowest name that doesn't exist yet (create new),
|
|
44
|
+
* so an all-attached family gets an independent `<base>-N`.
|
|
45
|
+
*
|
|
46
|
+
* Highest-detached rather than lowest is what keeps the family reachable.
|
|
47
|
+
* The old scan walked the family in order and returned the first session that
|
|
48
|
+
* was missing *or* detached, which meant a detached `<base>` ended the scan
|
|
49
|
+
* every time: `<base>-2` could not be reached by any later `hwip cc`, not even
|
|
50
|
+
* after `<base>` was used and exited (a missing `<base>` is taken fresh). A
|
|
51
|
+
* session nobody can reach still holds a live agent — a Claude Code CLI plus
|
|
52
|
+
* its MCP fleet, 250-550MB measured — so stranding one leaks memory the user
|
|
53
|
+
* can neither see nor reclaim. Draining from the tail makes every suffix
|
|
54
|
+
* reachable again: the tail goes first, and `<base>` is picked on the run
|
|
55
|
+
* after that. Reuse also wins over a gap in the numbering, for the same
|
|
56
|
+
* reason — a free `<base>-2` must not hide a detached `<base>-3`.
|
|
57
|
+
*
|
|
58
|
+
* Reclaiming is deliberately all this does. Killing a detached session would
|
|
59
|
+
* take a live agent with it, and nothing here can tell "the user is done with
|
|
60
|
+
* this" from "the user closed the terminal and will come back".
|
|
61
|
+
*/
|
|
62
|
+
export declare const findAvailableSession: (base: string,
|
|
63
|
+
/** The agent about to be started. A detached session that is running a
|
|
64
|
+
* different one is not a slot for it — see `sameAgent` below. Omitted by
|
|
65
|
+
* callers that have no kind to match, which keeps the old behaviour. */
|
|
66
|
+
agentKind?: string,
|
|
67
|
+
/** Test seam: what this machine recorded about a session name. */
|
|
68
|
+
recall?: (name: string) => {
|
|
69
|
+
agentKind: string;
|
|
70
|
+
} | null) => string;
|
|
71
|
+
export interface SpawnTarget {
|
|
72
|
+
/**
|
|
73
|
+
* `tmux-new` opens (or reattaches) a session and needs a terminal to
|
|
74
|
+
* attach to; `direct` runs the agent in the pane we are already in and
|
|
75
|
+
* does not. Several decisions downstream turn on which one this is, so
|
|
76
|
+
* `targetForAgent` states it rather than letting them re-derive it from
|
|
77
|
+
* `cmd === 'tmux'`.
|
|
78
|
+
*/
|
|
79
|
+
kind: 'direct' | 'tmux-new' | 'tmux-detached';
|
|
80
|
+
cmd: string;
|
|
81
|
+
args: string[];
|
|
82
|
+
/** The tmux session name for the two tmux kinds — carried, not re-read off argv. */
|
|
83
|
+
session?: string;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* `hwip <agent> --detach --label <x> --wt <name> …` — the wrapper-owned flags. They
|
|
87
|
+
* are read off the FRONT of the passthrough only, so an agent's own flags
|
|
88
|
+
* (`pi -n`, `claude --resume`) and its positional messages pass untouched.
|
|
89
|
+
*/
|
|
90
|
+
export interface AgentLaunchOptions {
|
|
91
|
+
/** Create the tmux session without attaching, print its name, exit 0. */
|
|
92
|
+
detach?: boolean;
|
|
93
|
+
/** Name the session `hwip-<project>-<label>` instead of the `-2/-3` family. */
|
|
94
|
+
label?: string;
|
|
95
|
+
/** `--wt <name>`: run in the git worktree for this branch, made next to the repo if it is not there. */
|
|
96
|
+
worktree?: string;
|
|
97
|
+
/** Set by `handleAgentSession` once the worktree exists — not a flag. The directory the session is named for, in place of `detectProjectDir()`. */
|
|
98
|
+
projectDir?: string;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* tmux forbids `.` and `:` in session names; whitespace would split the target;
|
|
102
|
+
* and an argv element ending in `;` is a tmux command separator.
|
|
103
|
+
*/
|
|
104
|
+
export declare const sanitizeLabel: (label: string) => string;
|
|
105
|
+
export declare const splitAgentOptions: (extra: string[]) => {
|
|
106
|
+
opts: AgentLaunchOptions;
|
|
107
|
+
rest: string[];
|
|
108
|
+
error?: string;
|
|
109
|
+
};
|
|
110
|
+
export declare const targetForAgent: (agent: string, extra: string[], agentKind?: string, opts?: AgentLaunchOptions) => SpawnTarget;
|
|
111
|
+
/**
|
|
112
|
+
* Whether the daemon on record is running a different build than the one
|
|
113
|
+
* we're launching from. `npm i -g @hwip/cli` swaps dist/ but leaves the
|
|
114
|
+
* live process untouched, so a daemon can stay days behind the installed
|
|
115
|
+
* package — answering pushes (chat looks fine) while silently ignoring every
|
|
116
|
+
* message subtype added since it booted. That is exactly how a pre-1.24
|
|
117
|
+
* listener left the phone's live terminal spinning until a reboot.
|
|
118
|
+
*
|
|
119
|
+
* A missing stamp means the daemon predates version stamping, which puts it
|
|
120
|
+
* behind by definition — treat unknown as drifted.
|
|
121
|
+
*
|
|
122
|
+
* Strictly "installed is newer", never a plain inequality: one account can
|
|
123
|
+
* have several installs at different versions (nvm node versions, a repo
|
|
124
|
+
* build alongside the global one), and `!==` would make each `hwip cc` kill
|
|
125
|
+
* and respawn the other's daemon forever. An older `hwip cc` leaves a newer
|
|
126
|
+
* daemon alone — downgrading it would reintroduce the very gap this closes.
|
|
127
|
+
*/
|
|
128
|
+
export declare const listenerVersionDrifted: (running: string | null, installed: string) => boolean;
|
|
129
|
+
/**
|
|
130
|
+
* Whether the listener started this process for a schedule (`LISTENER_LAUNCH_ENV`), and the marker used up: left in the
|
|
131
|
+
* environment it would reach the git hooks of a `--wt` worktree and the tmux server this launch may start, and so every pane after it.
|
|
132
|
+
*/
|
|
133
|
+
export declare const takeListenerLaunchMarker: () => boolean;
|
|
134
|
+
/**
|
|
135
|
+
* Make sure the phone-bridge daemon is running AND is the build we just
|
|
136
|
+
* launched from. `hwip cc` is the right moment to replace a drifted one: the
|
|
137
|
+
* user is starting fresh work rather than mid-task, so the ~1s restart window
|
|
138
|
+
* (in which a push would be dropped — WS fan-out has no queue) costs nothing.
|
|
139
|
+
*/
|
|
140
|
+
export declare const ensureListenerRunning: (launchedByListener?: boolean) => Promise<void>;
|
|
141
|
+
/**
|
|
142
|
+
* Launch the agent in a named tmux session (or directly if nested) and
|
|
143
|
+
* forward its exit code. `extra` is appended to the agent invocation, so
|
|
144
|
+
* `hwip cc --resume foo` runs `claude --resume foo` inside the session.
|
|
145
|
+
* Returns when the agent exits.
|
|
146
|
+
*/
|
|
147
|
+
/** `process.execve` with the optionality resolved — see handOffExec. */
|
|
148
|
+
export type ProcessHandOff = NonNullable<typeof process.execve>;
|
|
149
|
+
/** The hand-off handOffExec returns: execve with the environment already bound. */
|
|
150
|
+
type HandOff = (file: string, argv: string[]) => never;
|
|
151
|
+
/**
|
|
152
|
+
* The exec that replaces this process with the child, or null when the wrapper
|
|
153
|
+
* has to stay and wait instead. Returning the function rather than a boolean
|
|
154
|
+
* keeps one check where two would otherwise drift: the platform gate and the
|
|
155
|
+
* policy are the same answer.
|
|
156
|
+
*
|
|
157
|
+
* `process.execve` is declared optional by @types/node, and is absent on node
|
|
158
|
+
* <22.15 (added there) and on Windows (its doc: "not available on Windows or
|
|
159
|
+
* IBM i"), so its presence covers both without a version check. `engines.node`
|
|
160
|
+
* still allows 20.x, 22.13–22.14 and 23.5–23.10 (added in 22.15 and 23.11, per
|
|
161
|
+
* the Node docs), so the spawn path below is not a transitional fallback — it
|
|
162
|
+
* stays.
|
|
163
|
+
*
|
|
164
|
+
* `isTTY` is `undefined` rather than `false` on a pipe, hence every `=== true`.
|
|
165
|
+
*
|
|
166
|
+
* The environment is bound here rather than left to execve's default: node
|
|
167
|
+
* 22.15.1 execs with an empty environment when the argument is omitted
|
|
168
|
+
* (measured — 22.23.2 and 24.x inherit process.env), and a tmux started without
|
|
169
|
+
* TERM dies with "open terminal failed: terminal does not support clear".
|
|
170
|
+
*/
|
|
171
|
+
export declare const handOffExec: (deps: {
|
|
172
|
+
execve: ProcessHandOff | undefined;
|
|
173
|
+
env: NodeJS.ProcessEnv;
|
|
174
|
+
kind: SpawnTarget["kind"];
|
|
175
|
+
stdinTTY: boolean | undefined;
|
|
176
|
+
stdoutTTY: boolean | undefined;
|
|
177
|
+
}) => HandOff | null;
|
|
178
|
+
/**
|
|
179
|
+
* Inside tmux the agent runs in the current pane, and the listener reports
|
|
180
|
+
* only `hwip-*` sessions — so unless this pane already sits in one, the phone
|
|
181
|
+
* never sees the agent. Say so, and name the two ways to a session it does
|
|
182
|
+
* see. `session` is this pane's tmux session, null when tmux would not say.
|
|
183
|
+
*/
|
|
184
|
+
export declare const inPaneNote: (subcommand: string, session: string | null) => string | undefined;
|
|
185
|
+
/**
|
|
186
|
+
* What to say when tmux is missing — the install hint `hwip install` gives,
|
|
187
|
+
* not just "not found". Undefined when tmux is there. Exported for tests.
|
|
188
|
+
*/
|
|
189
|
+
export declare const tmuxMissingMessage: (command: string, plan: TmuxInstallPlan) => string | undefined;
|
|
190
|
+
/** What `handleAgentSession` does to the machine, swappable in tests. */
|
|
191
|
+
export interface AgentSessionDeps {
|
|
192
|
+
startListener?: () => Promise<void>;
|
|
193
|
+
launch?: (target: SpawnTarget, what: string) => Promise<number>;
|
|
194
|
+
}
|
|
195
|
+
export declare const handleAgentSession: (agent: RemoteAgent, extra?: string[], opts?: AgentLaunchOptions, deps?: AgentSessionDeps) => Promise<number>;
|
|
196
|
+
/** What tmux holds under a `hwip sh` session name. */
|
|
197
|
+
export type ShellSessionState = 'none' | 'marked' | 'unmarked';
|
|
198
|
+
/** A tmux call, injectable for tests. */
|
|
199
|
+
type RunTmux = (args: string[]) => {
|
|
200
|
+
status: number | null;
|
|
201
|
+
stdout: string;
|
|
202
|
+
stderr?: string;
|
|
203
|
+
};
|
|
204
|
+
/**
|
|
205
|
+
* How `hwip sh` proceeds, given what tmux holds under the name. `create` is
|
|
206
|
+
* the detached target, `attach` the attaching one (null for `--detach`).
|
|
207
|
+
*
|
|
208
|
+
* A missing session is created with `new -d` and `@hwip_shell` set to its own
|
|
209
|
+
* name in the same tmux call, so no sweep ever sees it unmarked, and `new -d`
|
|
210
|
+
* fails on a taken name: a session that appeared since the probe is never
|
|
211
|
+
* attached to and marked. The listener accepts the marker only when it names
|
|
212
|
+
* the session, so a stray `set -g @hwip_shell …` cannot mark anything. A
|
|
213
|
+
* session this command opened before is reattached (or, detached, left as it
|
|
214
|
+
* is — running `!hwip sh --detach` twice from the phone must not fail). Any
|
|
215
|
+
* other session under that name is refused: the marker would turn it into a
|
|
216
|
+
* typable shell.
|
|
217
|
+
*/
|
|
218
|
+
export declare const planShellSession: (create: SpawnTarget, attach: SpawnTarget | null, state: ShellSessionState) => {
|
|
219
|
+
kind: "create";
|
|
220
|
+
args: string[];
|
|
221
|
+
attach: SpawnTarget | null;
|
|
222
|
+
} | {
|
|
223
|
+
kind: "attach";
|
|
224
|
+
target: SpawnTarget;
|
|
225
|
+
} | {
|
|
226
|
+
kind: "running";
|
|
227
|
+
} | {
|
|
228
|
+
kind: "refuse";
|
|
229
|
+
reason: string;
|
|
230
|
+
};
|
|
231
|
+
/**
|
|
232
|
+
* Read the marker off the session itself. `show-options` without `-A` shows
|
|
233
|
+
* only what is set on that session, never an inherited global value. A failed
|
|
234
|
+
* `has-session` reads as "none", which is safe: creation is `new -d`, and it
|
|
235
|
+
* fails on a name that does exist.
|
|
236
|
+
*/
|
|
237
|
+
export declare const readShellSessionState: (session: string, run?: RunTmux) => ShellSessionState;
|
|
238
|
+
/**
|
|
239
|
+
* The two targets `hwip sh` works with. Inside tmux there is no attaching —
|
|
240
|
+
* `new -A` would nest and exit 1 after the session already exists — so the
|
|
241
|
+
* run is a detached one, as `--detach` is.
|
|
242
|
+
*/
|
|
243
|
+
export declare const shellTargets: (shell: string, opts: AgentLaunchOptions, inTmux?: boolean) => {
|
|
244
|
+
create: SpawnTarget;
|
|
245
|
+
attach: SpawnTarget | null;
|
|
246
|
+
};
|
|
247
|
+
/**
|
|
248
|
+
* `hwip sh [--label x] [--detach]` — open (or reattach) `hwip-<project>-sh`
|
|
249
|
+
* running the login shell, marked so the phone may type into it. The label
|
|
250
|
+
* always pins a tmux session, so this never runs in the current pane.
|
|
251
|
+
*/
|
|
252
|
+
export declare const handleShellSession: (opts: AgentLaunchOptions) => Promise<number>;
|
|
253
|
+
export {};
|
|
254
|
+
//# sourceMappingURL=wrapper.d.ts.map
|