@nonbot/cli 0.5.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/LICENSE +43 -0
- package/README.md +93 -0
- package/dist/commands/daemon.js +286 -0
- package/dist/commands/doctor.js +262 -0
- package/dist/commands/login.js +112 -0
- package/dist/commands/logs.js +113 -0
- package/dist/commands/profiles.js +24 -0
- package/dist/commands/run.js +63 -0
- package/dist/commands/status.js +103 -0
- package/dist/commands/test.js +4 -0
- package/dist/index.js +135 -0
- package/dist/lib/activations.js +480 -0
- package/dist/lib/activity-log.js +48 -0
- package/dist/lib/auth.js +106 -0
- package/dist/lib/banner.js +94 -0
- package/dist/lib/command-builders.js +279 -0
- package/dist/lib/completion.js +67 -0
- package/dist/lib/output.js +382 -0
- package/dist/lib/payload-validator.js +174 -0
- package/dist/lib/service.js +145 -0
- package/dist/lib/terminal.js +313 -0
- package/dist/version.js +1 -0
- package/package.json +33 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# @nonbot/cli changelog
|
|
2
|
+
|
|
3
|
+
## 0.5.10
|
|
4
|
+
|
|
5
|
+
- Build now strips source comments from the published output, so no internal
|
|
6
|
+
file paths or developer environment details ship in the package.
|
|
7
|
+
|
|
8
|
+
## 0.5.9
|
|
9
|
+
|
|
10
|
+
- Docs: rewrote the README as clean end-user docs; removed internal
|
|
11
|
+
implementation and security-mechanism detail not meant for a public package.
|
|
12
|
+
- Scrubbed an example email address from the `nonbot status` banner output.
|
|
13
|
+
- First release published via npm trusted publishing (OIDC) — no token.
|
|
14
|
+
|
|
15
|
+
## 0.5.8
|
|
16
|
+
|
|
17
|
+
- Runs are now marked complete automatically when their terminal closes, so the
|
|
18
|
+
Recent Activations view on non.bot stays accurate.
|
|
19
|
+
|
|
20
|
+
## 0.5.x
|
|
21
|
+
|
|
22
|
+
- `nonbot doctor` health check (auth, server, provider CLIs, terminal).
|
|
23
|
+
- Headless mode (`--headless`) to run inline without opening a window.
|
|
24
|
+
- Multiple accounts via `--profile`.
|
|
25
|
+
- Local launch history (`nonbot logs`).
|
|
26
|
+
- Run the daemon at login (`daemon --install` / `--uninstall`).
|
|
27
|
+
- tmux support: Runs open as tmux windows when the daemon runs inside tmux, and can be
|
|
28
|
+
stopped from non.bot.
|
|
29
|
+
- Adaptive polling — the daemon checks more often while a Run is active and backs off
|
|
30
|
+
when idle.
|
|
31
|
+
|
|
32
|
+
## 0.2.0 – 0.4.0
|
|
33
|
+
|
|
34
|
+
- Initial release: `login`, `daemon`, `run`, `status`, and cross-platform terminal
|
|
35
|
+
launching (macOS, Linux, Windows).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Copyright (c) 2026 non.bot. All rights reserved.
|
|
2
|
+
|
|
3
|
+
PROPRIETARY SOFTWARE — NOT OPEN SOURCE
|
|
4
|
+
|
|
5
|
+
This software (the "@nonbot/cli" package and all of its contents, the
|
|
6
|
+
"Software") is the proprietary and confidential property of non.bot. It is
|
|
7
|
+
published to the npm registry for the sole purpose of allowing authorized
|
|
8
|
+
non.bot account holders to install and run it as the local host for the
|
|
9
|
+
non.bot "Run" feature.
|
|
10
|
+
|
|
11
|
+
PERMITTED USE
|
|
12
|
+
|
|
13
|
+
Subject to your compliance with the non.bot Terms of Service, you are granted
|
|
14
|
+
a limited, non-exclusive, non-transferable, revocable license to download,
|
|
15
|
+
install, and execute unmodified copies of the Software on machines you control,
|
|
16
|
+
solely to connect to and operate the non.bot service.
|
|
17
|
+
|
|
18
|
+
PROHIBITED WITHOUT PRIOR WRITTEN PERMISSION FROM non.bot
|
|
19
|
+
|
|
20
|
+
- Copying, redistributing, sublicensing, selling, or publishing the Software
|
|
21
|
+
or any portion of it;
|
|
22
|
+
- Modifying, adapting, translating, or creating derivative works;
|
|
23
|
+
- Reverse-engineering, decompiling, or disassembling the Software except to
|
|
24
|
+
the limited extent such restriction is prohibited by applicable law;
|
|
25
|
+
- Using the Software, or any technique, method, or design embodied in it, to
|
|
26
|
+
build, train, or operate a competing product or service.
|
|
27
|
+
|
|
28
|
+
NO PATENT OR OTHER IP LICENSE
|
|
29
|
+
|
|
30
|
+
No license to any patent, patent application, trademark, trade secret, or other
|
|
31
|
+
intellectual-property right of non.bot is granted, whether expressly, by
|
|
32
|
+
implication, by estoppel, or otherwise, except for the limited execution right
|
|
33
|
+
stated above. All such rights are expressly reserved.
|
|
34
|
+
|
|
35
|
+
NO WARRANTY
|
|
36
|
+
|
|
37
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
38
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
39
|
+
FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
40
|
+
non.bot BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM, OUT
|
|
41
|
+
OF, OR IN CONNECTION WITH THE SOFTWARE OR ITS USE.
|
|
42
|
+
|
|
43
|
+
Contact: legal@non.bot
|
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# @nonbot/cli
|
|
2
|
+
|
|
3
|
+
The local host for non.bot **▶ Run**. Install it, log in once, and leave the daemon
|
|
4
|
+
running — when you click ▶ Run on a story in non.bot, a terminal opens on your machine
|
|
5
|
+
and starts the work in your linked repo.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @nonbot/cli
|
|
9
|
+
nonbot login
|
|
10
|
+
nonbot daemon
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## How it works
|
|
14
|
+
|
|
15
|
+
The daemon runs as an ordinary process in your shell. It polls non.bot for Runs you've
|
|
16
|
+
started, and when one arrives it opens a terminal in your linked repository and launches
|
|
17
|
+
your CLI agent (`claude`, `codex`, or `gemini`). Because it's a plain Node process, it
|
|
18
|
+
uses your existing `PATH` and whatever CLI logins you already have — no extra setup.
|
|
19
|
+
|
|
20
|
+
## Getting started
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# 1. Install
|
|
24
|
+
npm install -g @nonbot/cli
|
|
25
|
+
|
|
26
|
+
# 2. Create a Personal Access Token on non.bot
|
|
27
|
+
# Settings → Connections → API tokens → "New token"
|
|
28
|
+
|
|
29
|
+
# 3. Log in (paste the token when prompted)
|
|
30
|
+
nonbot login
|
|
31
|
+
|
|
32
|
+
# 4. Leave the daemon running in a terminal tab
|
|
33
|
+
nonbot daemon
|
|
34
|
+
|
|
35
|
+
# 5. Click ▶ Run on non.bot — a terminal opens and starts the work.
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Non-interactive login (for CI or scripting):
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
nonbot login --token "$NONBOT_PAT"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Commands
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
nonbot login Log in with a Personal Access Token
|
|
48
|
+
nonbot daemon Run the poll loop and launch terminals (the main command)
|
|
49
|
+
nonbot run <id> Launch a single Run by id, then exit
|
|
50
|
+
nonbot status Show login + daemon status
|
|
51
|
+
nonbot doctor Health check — auth, server, providers, terminal
|
|
52
|
+
nonbot logs Show recent local Run launches
|
|
53
|
+
nonbot profiles List configured accounts
|
|
54
|
+
nonbot test Print a local diagnostic banner (no server needed)
|
|
55
|
+
nonbot --help Full command list
|
|
56
|
+
nonbot --version Print the version
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Useful flags
|
|
60
|
+
|
|
61
|
+
| Flag | Effect |
|
|
62
|
+
|---|---|
|
|
63
|
+
| `--headless` | Run inline without opening a terminal window (`daemon`, `run`) |
|
|
64
|
+
| `--profile <name>` | Use a named account instead of the default |
|
|
65
|
+
| `daemon --install` / `--uninstall` | Run the daemon automatically at login |
|
|
66
|
+
|
|
67
|
+
## Terminals
|
|
68
|
+
|
|
69
|
+
On macOS the daemon launches Terminal or iTerm; on Linux it uses your system terminal;
|
|
70
|
+
on Windows it uses Windows Terminal. If you run the daemon inside a `tmux` session, Runs
|
|
71
|
+
open as new tmux windows instead of separate terminal windows.
|
|
72
|
+
|
|
73
|
+
Override the terminal with `NONBOT_TERMINAL` (e.g. `NONBOT_TERMINAL=iterm`), or set a
|
|
74
|
+
preferred terminal in your non.bot settings.
|
|
75
|
+
|
|
76
|
+
## Configuration
|
|
77
|
+
|
|
78
|
+
Credentials are stored in `~/.config/nonbot/auth.json` (file mode `0600`). Override the
|
|
79
|
+
config directory with `NONBOT_CONFIG_DIR`.
|
|
80
|
+
|
|
81
|
+
If a token is lost or compromised, rotate it at
|
|
82
|
+
**non.bot → Settings → Connections → API tokens**.
|
|
83
|
+
|
|
84
|
+
## Requirements
|
|
85
|
+
|
|
86
|
+
- Node.js 18 or newer
|
|
87
|
+
- A non.bot account with a linked repository
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
Proprietary — all rights reserved. See [LICENSE](./LICENSE). This package is
|
|
92
|
+
published to npm solely to function as the local host for non.bot ▶ Run; it is
|
|
93
|
+
not open-source software.
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { spawnSync as nodeSpawnSync } from 'node:child_process';
|
|
2
|
+
import { loadAuth, getActiveProfile } from '../lib/auth.js';
|
|
3
|
+
import * as activations from '../lib/activations.js';
|
|
4
|
+
import { fireActivation, makeHeadlessSpawner, } from '../lib/activations.js';
|
|
5
|
+
import { checkCompletions } from '../lib/completion.js';
|
|
6
|
+
import { installService, uninstallService } from '../lib/service.js';
|
|
7
|
+
import { detectTmuxSession, inTmuxSession, nonbotTmuxOptOut, resolveTerminal, } from '../lib/terminal.js';
|
|
8
|
+
import { VERSION } from '../version.js';
|
|
9
|
+
import { errorBlock, statusRow, daemonOpener, daemonCloser, pollTick, activationCard, c, } from '../lib/output.js';
|
|
10
|
+
export const POLL_FAST_MS = 2000;
|
|
11
|
+
export const POLL_MAX_MS = 30000;
|
|
12
|
+
export const POLL_INTERVAL_MS = POLL_FAST_MS;
|
|
13
|
+
export function applyNonbotTmuxConfig(deps = {}) {
|
|
14
|
+
const spawnSync = deps.spawnSync ?? nodeSpawnSync;
|
|
15
|
+
const stdoutWrite = deps.stdoutWrite ?? ((s) => process.stdout.write(s));
|
|
16
|
+
const env = deps.env ?? process.env;
|
|
17
|
+
const stdoutIsTTY = deps.stdoutIsTTY ?? (process.stdout?.isTTY !== false);
|
|
18
|
+
const tmux = (...args) => {
|
|
19
|
+
try {
|
|
20
|
+
spawnSync('tmux', args, { encoding: 'utf-8', timeout: 1000, windowsHide: true });
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
tmux('set-option', '-wg', 'automatic-rename', 'off');
|
|
26
|
+
tmux('set-option', '-wg', 'allow-rename', 'off');
|
|
27
|
+
tmux('rename-window', 'NONBOT TMUX');
|
|
28
|
+
tmux('set-option', 'set-titles', 'on');
|
|
29
|
+
tmux('set-option', 'set-titles-string', 'NONBOT TMUX │ #W');
|
|
30
|
+
tmux('refresh-client', '-S');
|
|
31
|
+
if (!stdoutIsTTY)
|
|
32
|
+
return;
|
|
33
|
+
const insideTmux = inTmuxSession(env);
|
|
34
|
+
if (insideTmux) {
|
|
35
|
+
tmux('set-option', '-p', 'allow-passthrough', 'on');
|
|
36
|
+
}
|
|
37
|
+
const NONBOT_TMUX_OSC = '\x1b]2;NONBOT TMUX\x07';
|
|
38
|
+
const payload = insideTmux
|
|
39
|
+
? `\x1bPtmux;\x1b${NONBOT_TMUX_OSC}\x1b\\`
|
|
40
|
+
: NONBOT_TMUX_OSC;
|
|
41
|
+
try {
|
|
42
|
+
stdoutWrite(payload);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function maybeReexecIntoTmux(deps = {}) {
|
|
48
|
+
const env = deps.env ?? process.env;
|
|
49
|
+
const spawn = deps.spawnSync ?? nodeSpawnSync;
|
|
50
|
+
const argv = deps.argv ?? process.argv;
|
|
51
|
+
const stdinIsTTY = deps.stdinIsTTY ?? (process.stdin?.isTTY !== false);
|
|
52
|
+
const exit = deps.exit ?? ((code) => process.exit(code));
|
|
53
|
+
const log = deps.log ?? ((s) => process.stderr.write(s));
|
|
54
|
+
if (env.VITEST)
|
|
55
|
+
return 'skipped';
|
|
56
|
+
if (inTmuxSession(env))
|
|
57
|
+
return 'in-tmux';
|
|
58
|
+
const userArgs = argv.slice(2);
|
|
59
|
+
if (userArgs.includes('--no-tmux') || nonbotTmuxOptOut(env))
|
|
60
|
+
return 'skipped';
|
|
61
|
+
if (userArgs.includes('--headless') || userArgs.includes('--no-terminal'))
|
|
62
|
+
return 'skipped';
|
|
63
|
+
if (!stdinIsTTY)
|
|
64
|
+
return 'skipped';
|
|
65
|
+
let probeStatus = null;
|
|
66
|
+
try {
|
|
67
|
+
const probe = spawn('tmux', ['-V'], { stdio: 'ignore' });
|
|
68
|
+
probeStatus = probe.status;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
probeStatus = 1;
|
|
72
|
+
}
|
|
73
|
+
if (probeStatus !== 0) {
|
|
74
|
+
log('▶ tmux not installed — running daemon bare (each activation will open a new terminal window).\n');
|
|
75
|
+
log(' Install tmux for the multi-pane experience: brew install tmux\n');
|
|
76
|
+
return 'skipped';
|
|
77
|
+
}
|
|
78
|
+
log("▶ wrapping daemon in tmux session 'nonbot' — activations will land as tabs in this window.\n");
|
|
79
|
+
log(" (use `nonbot daemon --no-tmux` if you want bare iTerm windows instead.)\n");
|
|
80
|
+
const wrap = spawn('tmux', ['new-session', '-A', '-s', 'nonbot', '--', ...argv], { stdio: 'inherit' });
|
|
81
|
+
exit(wrap.status ?? 0);
|
|
82
|
+
return 'reexeced';
|
|
83
|
+
}
|
|
84
|
+
export async function runDaemonCommand(args = [], options = {}, deps = {}) {
|
|
85
|
+
const log = deps.log ?? ((s) => process.stdout.write(s));
|
|
86
|
+
const errLog = deps.errLog ?? ((s) => process.stderr.write(s));
|
|
87
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
88
|
+
const loader = deps.loadAuth ?? loadAuth;
|
|
89
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
90
|
+
const fixedInterval = options.pollIntervalMs;
|
|
91
|
+
const maxIntervalMs = options.maxPollIntervalMs ?? POLL_MAX_MS;
|
|
92
|
+
let currentInterval = fixedInterval ?? POLL_FAST_MS;
|
|
93
|
+
if (args.includes('--install')) {
|
|
94
|
+
const install = deps.installService ?? installService;
|
|
95
|
+
const res = await install();
|
|
96
|
+
if (res.ok)
|
|
97
|
+
log(statusRow('✓', 'Service installed', res.message) + '\n');
|
|
98
|
+
else
|
|
99
|
+
errLog(statusRow('✗', 'Service install failed', res.message, { stream: process.stderr }) + '\n');
|
|
100
|
+
return res.ok ? 0 : 1;
|
|
101
|
+
}
|
|
102
|
+
if (args.includes('--uninstall')) {
|
|
103
|
+
const uninstall = deps.uninstallService ?? uninstallService;
|
|
104
|
+
const res = await uninstall();
|
|
105
|
+
if (res.ok)
|
|
106
|
+
log(statusRow('✓', 'Service removed', res.message) + '\n');
|
|
107
|
+
else
|
|
108
|
+
errLog(statusRow('✗', 'Service uninstall failed', res.message, { stream: process.stderr }) + '\n');
|
|
109
|
+
return res.ok ? 0 : 1;
|
|
110
|
+
}
|
|
111
|
+
const autoWrap = deps.maybeReexecIntoTmux ?? maybeReexecIntoTmux;
|
|
112
|
+
const wrapResult = autoWrap();
|
|
113
|
+
if (wrapResult === 'reexeced')
|
|
114
|
+
return 0;
|
|
115
|
+
const headless = args.includes('--headless') || args.includes('--no-terminal');
|
|
116
|
+
if (headless) {
|
|
117
|
+
deps.headless = true;
|
|
118
|
+
if (!deps.spawnTerminal) {
|
|
119
|
+
deps.spawnTerminal = makeHeadlessSpawner({ wait: false, log, errLog });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const auth = await loader();
|
|
123
|
+
if (!auth) {
|
|
124
|
+
errLog(errorBlock('Not logged in', 'Run `nonbot login` to authenticate.', { stream: process.stderr }));
|
|
125
|
+
return 1;
|
|
126
|
+
}
|
|
127
|
+
const detectTmux = deps.detectTmuxSession ?? detectTmuxSession;
|
|
128
|
+
const tmuxSessionName = detectTmux();
|
|
129
|
+
if (tmuxSessionName) {
|
|
130
|
+
applyNonbotTmuxConfig({ spawnSync: deps.spawnSync });
|
|
131
|
+
}
|
|
132
|
+
const seen = new Set();
|
|
133
|
+
const trackedPanes = new Map();
|
|
134
|
+
const killedByStop = new Set();
|
|
135
|
+
let running = true;
|
|
136
|
+
let sigHandler;
|
|
137
|
+
if (!options.oneShot) {
|
|
138
|
+
sigHandler = () => {
|
|
139
|
+
running = false;
|
|
140
|
+
log('\n' + statusRow('✓', 'daemon stopped', 'Ctrl-C received') + '\n');
|
|
141
|
+
process.exit(0);
|
|
142
|
+
};
|
|
143
|
+
process.on('SIGINT', sigHandler);
|
|
144
|
+
}
|
|
145
|
+
const hostUrl = new URL(auth.baseUrl).host;
|
|
146
|
+
const profileName = getActiveProfile();
|
|
147
|
+
const pollingLabel = fixedInterval !== undefined
|
|
148
|
+
? `${fixedInterval}ms fixed`
|
|
149
|
+
: `${Math.round(POLL_FAST_MS / 1000)}s -> ${Math.round(maxIntervalMs / 1000)}s adaptive`;
|
|
150
|
+
let terminalLabel;
|
|
151
|
+
if (headless) {
|
|
152
|
+
terminalLabel = 'headless (inline, no terminal window)';
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
const profile = resolveTerminal(undefined);
|
|
156
|
+
terminalLabel = tmuxSessionName
|
|
157
|
+
? `${profile.displayName} · tmux pane (in session '${tmuxSessionName}')`
|
|
158
|
+
: profile.displayName;
|
|
159
|
+
}
|
|
160
|
+
const badgeSublines = [
|
|
161
|
+
`listening on ${auth.baseUrl}`,
|
|
162
|
+
`poll ${Math.round(POLL_FAST_MS / 1000)}s · ${Math.round(maxIntervalMs / 1000)}s on idle · adaptive`,
|
|
163
|
+
];
|
|
164
|
+
if (tmuxSessionName)
|
|
165
|
+
badgeSublines.push(`tmux host: ${tmuxSessionName}`);
|
|
166
|
+
log(daemonOpener(`v${VERSION}`, {
|
|
167
|
+
state: 'IDLE',
|
|
168
|
+
badgeColor: 'green',
|
|
169
|
+
badgeLabel: 'CONNECTED',
|
|
170
|
+
badgeSublines,
|
|
171
|
+
}) + '\n\n');
|
|
172
|
+
const headerMarker = c.green('[ + ]');
|
|
173
|
+
const headerLabel = c.white('LISTENING · READY FOR ACTIVATIONS');
|
|
174
|
+
log(`${headerMarker} ${c.bold(headerLabel)}\n`);
|
|
175
|
+
const ctxRows = [
|
|
176
|
+
['YOU', auth.email],
|
|
177
|
+
['LISTENING', `${hostUrl}/api/cli/activations/pending`],
|
|
178
|
+
['PROFILE', profileName],
|
|
179
|
+
['LANDING', terminalLabel],
|
|
180
|
+
];
|
|
181
|
+
const LABEL_WIDTH = 9;
|
|
182
|
+
for (const [k, v] of ctxRows) {
|
|
183
|
+
const label = c.cyan(k.padEnd(LABEL_WIDTH, ' '));
|
|
184
|
+
const value = c.white(v);
|
|
185
|
+
log(` ${label} ${value}\n`);
|
|
186
|
+
}
|
|
187
|
+
log('\n');
|
|
188
|
+
log(`${c.bold(c.white('LIVESTREAM'))}\n`);
|
|
189
|
+
log('\n');
|
|
190
|
+
log(daemonCloser({
|
|
191
|
+
inTmux: tmuxSessionName !== null,
|
|
192
|
+
}) + '\n');
|
|
193
|
+
log('\n');
|
|
194
|
+
while (running) {
|
|
195
|
+
let firedThisPoll = false;
|
|
196
|
+
try {
|
|
197
|
+
const headers = {
|
|
198
|
+
Authorization: `Bearer ${auth.pat}`,
|
|
199
|
+
'X-Requested-With': 'ConradPM-Native',
|
|
200
|
+
'X-CLI-Version': VERSION,
|
|
201
|
+
};
|
|
202
|
+
if (tmuxSessionName)
|
|
203
|
+
headers['X-Tmux-Session'] = tmuxSessionName;
|
|
204
|
+
const res = await fetchImpl(`${auth.baseUrl}/api/cli/activations/pending`, {
|
|
205
|
+
headers,
|
|
206
|
+
});
|
|
207
|
+
if (res.status === 401) {
|
|
208
|
+
errLog(errorBlock('Auth failed (HTTP 401)', 'Re-run: nonbot login', { stream: process.stderr }));
|
|
209
|
+
if (sigHandler)
|
|
210
|
+
process.off('SIGINT', sigHandler);
|
|
211
|
+
return 1;
|
|
212
|
+
}
|
|
213
|
+
if (res.ok) {
|
|
214
|
+
const body = (await res.json());
|
|
215
|
+
const pendingKills = body?.pendingKills ?? [];
|
|
216
|
+
if (pendingKills.length > 0) {
|
|
217
|
+
for (const k of pendingKills) {
|
|
218
|
+
log(activationCard({
|
|
219
|
+
marker: '■',
|
|
220
|
+
color: 'amber',
|
|
221
|
+
id: k.activationId,
|
|
222
|
+
headerSuffix: 'STOPPING',
|
|
223
|
+
kv: [
|
|
224
|
+
['PANE', `${k.tmuxPaneId} · ^C -> 2s grace -> kill-pane`],
|
|
225
|
+
['REASON', 'stop received from web canvas'],
|
|
226
|
+
],
|
|
227
|
+
}) + '\n');
|
|
228
|
+
}
|
|
229
|
+
for (const k of pendingKills)
|
|
230
|
+
killedByStop.add(k.activationId);
|
|
231
|
+
void activations.executePendingKills(pendingKills, auth.baseUrl, auth.pat);
|
|
232
|
+
}
|
|
233
|
+
const acts = body?.activations ?? [];
|
|
234
|
+
for (const act of acts) {
|
|
235
|
+
if (!act?.id || seen.has(act.id))
|
|
236
|
+
continue;
|
|
237
|
+
seen.add(act.id);
|
|
238
|
+
firedThisPoll = true;
|
|
239
|
+
const outcome = await fireActivation(auth, act, deps, log, errLog);
|
|
240
|
+
if (outcome.status === 'launched' && outcome.kind === 'real' && outcome.tmuxPaneId) {
|
|
241
|
+
trackedPanes.set(outcome.id, outcome.tmuxPaneId);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (trackedPanes.size > 0) {
|
|
245
|
+
await checkCompletions({
|
|
246
|
+
tracked: trackedPanes,
|
|
247
|
+
killed: killedByStop,
|
|
248
|
+
baseUrl: auth.baseUrl,
|
|
249
|
+
pat: auth.pat,
|
|
250
|
+
spawnImpl: deps.spawnSync,
|
|
251
|
+
fetchImpl,
|
|
252
|
+
log,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
if (!firedThisPoll && pendingKills.length === 0) {
|
|
256
|
+
log(pollTick({
|
|
257
|
+
pending: 0,
|
|
258
|
+
sleepMs: fixedInterval ?? currentInterval,
|
|
259
|
+
backingOff: fixedInterval === undefined && currentInterval > POLL_FAST_MS,
|
|
260
|
+
}) + '\n');
|
|
261
|
+
}
|
|
262
|
+
if (seen.size > 500)
|
|
263
|
+
seen.clear();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch (e) {
|
|
267
|
+
errLog(statusRow('⚠', 'poll error', e.message, { stream: process.stderr }) + '\n');
|
|
268
|
+
}
|
|
269
|
+
if (options.oneShot)
|
|
270
|
+
break;
|
|
271
|
+
if (fixedInterval !== undefined) {
|
|
272
|
+
await sleep(fixedInterval);
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
if (firedThisPoll)
|
|
276
|
+
currentInterval = POLL_FAST_MS;
|
|
277
|
+
await sleep(currentInterval);
|
|
278
|
+
if (!firedThisPoll) {
|
|
279
|
+
currentInterval = Math.min(currentInterval * 2, maxIntervalMs);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (sigHandler)
|
|
284
|
+
process.off('SIGINT', sigHandler);
|
|
285
|
+
return 0;
|
|
286
|
+
}
|