@nonbot/cli 0.10.14 → 0.10.15
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 +11 -0
- package/README.md +7 -5
- package/dist/commands/daemon.js +2 -0
- package/dist/commands/doctor.js +140 -126
- package/dist/commands/logout.js +78 -0
- package/dist/commands/update.js +157 -0
- package/dist/index.js +105 -41
- package/dist/lib/mcp-config.js +124 -0
- package/dist/lib/mcp-status.js +127 -0
- package/dist/lib/output.js +40 -6
- package/dist/lib/update-check.js +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
What changed for the person running the CLI, newest first. Update with
|
|
4
4
|
`npm i -g @nonbot/cli@latest`.
|
|
5
5
|
|
|
6
|
+
## 0.10.15 (2026-11-11)
|
|
7
|
+
|
|
8
|
+
- New: `nonbot logout` signs you out on this machine and stops the daemon.
|
|
9
|
+
- New: `nonbot update` installs the latest version. The daemon's update notice now points at it.
|
|
10
|
+
- The daemon's startup list now says how to stop it (Ctrl-C in its window, or `nonbot daemon stop`) and how to sign out.
|
|
11
|
+
- `nonbot doctor` is easier to read: grouped sections, bare version numbers, no wrapped lines, no rows for retired MCP servers, and a short list of commands at the end. If Run is switched off for your account, it now says so and links to where you switch it on, instead of showing "HTTP 403".
|
|
12
|
+
- `nonbot doctor` now finds the non.bot MCP in a project's `.mcp.json` and in Claude Desktop, not only in `~/.claude.json`, and shows its version and how it is installed (npx, remote, global or a source checkout).
|
|
13
|
+
- New: `nonbot update mcp` shows the non.bot MCP's version and updates it. A global install is updated with npm. The npx setup updates itself each time Claude Code starts it and the remote server is always current, so for those it tells you what to do instead of running anything.
|
|
14
|
+
- The non.bot block wordmark is back with a shaded aqua → violet → orchid gradient, on the daemon and on the bare `nonbot` screen. The dot takes the same gradient as the letters beside it.
|
|
15
|
+
- Typing `nonbot` alone shows the wordmark and a short start-here list (login · daemon · doctor · logout · update). `nonbot --help` is grouped, fits 80 columns, and no longer lists the commands only Claude Code runs.
|
|
16
|
+
|
|
6
17
|
## 0.10.14 (2026-11-11)
|
|
7
18
|
|
|
8
19
|
- The README now lists what the daemon sends from your machine and who can read it.
|
package/README.md
CHANGED
|
@@ -58,10 +58,10 @@ nonbot daemon
|
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
`nonbot doctor` is the one-stop health check and the first thing to run when something
|
|
61
|
-
looks off. It reports your
|
|
62
|
-
heartbeat, which
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
looks off. It reports your login, server reachability, whether the Run app is on for
|
|
62
|
+
your account, the last daemon heartbeat, which agent CLIs are installed, which terminal
|
|
63
|
+
Runs open in, the macOS Automation state for iTerm, and whether the `nonbot` MCP server
|
|
64
|
+
is set up in Claude Code — then lists how to start, stop and sign out.
|
|
65
65
|
|
|
66
66
|
Non-interactive login (for CI or scripting):
|
|
67
67
|
|
|
@@ -73,11 +73,13 @@ nonbot login --token "$NONBOT_PAT"
|
|
|
73
73
|
|
|
74
74
|
```
|
|
75
75
|
nonbot login Log in with a Personal Access Token
|
|
76
|
+
nonbot logout Sign out on this machine and stop the daemon
|
|
76
77
|
nonbot daemon Run the poll loop and launch terminals (the main command)
|
|
77
78
|
nonbot run <id> Launch a single Run by id, then exit
|
|
78
79
|
nonbot status Show login + daemon status
|
|
79
80
|
nonbot doctor Health check — platform, auth, server, providers, terminal, MCP
|
|
80
81
|
nonbot logs Show recent local Run launches
|
|
82
|
+
nonbot update Install the latest version (npm i -g @nonbot/cli@latest)
|
|
81
83
|
nonbot profiles List configured accounts
|
|
82
84
|
nonbot choir <session> Coordinated-run engine — one agent per git worktree, merged back by hand
|
|
83
85
|
nonbot run-mcp Per-pane MCP server over stdio (launched by .mcp.json, not by hand)
|
|
@@ -95,7 +97,7 @@ nonbot --version Print the version
|
|
|
95
97
|
| `--profile <name>` | Use a named account instead of the default (works anywhere in the command) |
|
|
96
98
|
| `daemon --install` / `--uninstall` | Run the daemon automatically at login (macOS launchd / Linux systemd user unit; not available on Windows) |
|
|
97
99
|
| `daemon --cloud` | Containerized runner mode — see **Cloud runners** below |
|
|
98
|
-
| `daemon stop` | Stop the local daemon; `--machine <id>` / `--all` stop daemons on other machines |
|
|
100
|
+
| `daemon stop` | Stop the local daemon from another terminal (or press Ctrl-C in the daemon's own window); `--machine <id>` / `--all` stop daemons on other machines |
|
|
99
101
|
|
|
100
102
|
## Cost ceilings
|
|
101
103
|
|
package/dist/commands/daemon.js
CHANGED
|
@@ -572,6 +572,8 @@ export async function runDaemonCommand(args = [], options = {}, deps = {}) {
|
|
|
572
572
|
['LISTENING', `${hostUrl}/api/cli/activations/pending`],
|
|
573
573
|
['PROFILE', profileName],
|
|
574
574
|
['LANDING', terminalLabel],
|
|
575
|
+
['STOP', 'Ctrl-C in this window · or nonbot daemon stop from another terminal'],
|
|
576
|
+
['SIGN OUT', 'nonbot logout · signs out on this machine and stops the daemon'],
|
|
575
577
|
];
|
|
576
578
|
const LABEL_WIDTH = 9;
|
|
577
579
|
for (const [k, v] of ctxRows) {
|
package/dist/commands/doctor.js
CHANGED
|
@@ -3,66 +3,16 @@ import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs';
|
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { loadAuth } from '../lib/auth.js';
|
|
6
|
+
import { scanMcpConfigs, describeNonbotMcp } from '../lib/mcp-config.js';
|
|
7
|
+
import { nonbotMcpStatuses, mcpRow } from '../lib/mcp-status.js';
|
|
6
8
|
import { resolveTerminal } from '../lib/terminal.js';
|
|
7
9
|
import { VERSION } from '../version.js';
|
|
8
|
-
import { header, statusRow, errorBlock } from '../lib/output.js';
|
|
10
|
+
import { header, statusRow, errorBlock, isTTY, ANSI } from '../lib/output.js';
|
|
9
11
|
const HEARTBEAT_FRESH_MS = 35_000;
|
|
10
12
|
const PROVIDER_CLIS = ['claude', 'codex', 'gemini'];
|
|
11
|
-
const NONBOT_MCP_ADD_HINT = 'claude mcp add nonbot
|
|
12
|
-
const
|
|
13
|
-
{
|
|
14
|
-
name: 'nonbot',
|
|
15
|
-
required: true,
|
|
16
|
-
present: '@nonbot/mcp',
|
|
17
|
-
absent: `not configured in ~/.claude.json — ${NONBOT_MCP_ADD_HINT}`,
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
name: 'portfolio-mcp',
|
|
21
|
-
required: false,
|
|
22
|
-
present: 'legacy — superseded by @nonbot/mcp',
|
|
23
|
-
absent: 'optional — not configured (legacy, superseded by @nonbot/mcp)',
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
name: 'context-graph',
|
|
27
|
-
required: false,
|
|
28
|
-
present: 'legacy — superseded by @nonbot/mcp',
|
|
29
|
-
absent: 'optional — not configured (legacy, superseded by @nonbot/mcp)',
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
name: 'jobshop-mcp',
|
|
33
|
-
required: false,
|
|
34
|
-
present: 'optional — JobShop is hidden for launch',
|
|
35
|
-
absent: 'optional — not configured (JobShop is hidden for launch)',
|
|
36
|
-
},
|
|
37
|
-
];
|
|
13
|
+
export const NONBOT_MCP_ADD_HINT = 'claude mcp add nonbot -- npx -y @nonbot/mcp';
|
|
14
|
+
const LEGACY_MCPS = ['portfolio-mcp', 'context-graph', 'jobshop-mcp'];
|
|
38
15
|
const EXPECTED_SKILLS = [];
|
|
39
|
-
function readConfiguredMcpNames() {
|
|
40
|
-
const out = new Set();
|
|
41
|
-
try {
|
|
42
|
-
const path = join(homedir(), '.claude.json');
|
|
43
|
-
if (!existsSync(path))
|
|
44
|
-
return out;
|
|
45
|
-
const json = JSON.parse(readFileSync(path, 'utf-8'));
|
|
46
|
-
const projects = json?.projects && typeof json.projects === 'object' ? json.projects : null;
|
|
47
|
-
if (projects) {
|
|
48
|
-
for (const proj of Object.values(projects)) {
|
|
49
|
-
const servers = proj?.mcpServers;
|
|
50
|
-
if (servers && typeof servers === 'object') {
|
|
51
|
-
for (const name of Object.keys(servers))
|
|
52
|
-
out.add(name);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
const top = json?.mcpServers;
|
|
57
|
-
if (top && typeof top === 'object') {
|
|
58
|
-
for (const name of Object.keys(top))
|
|
59
|
-
out.add(name);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
}
|
|
64
|
-
return out;
|
|
65
|
-
}
|
|
66
16
|
function isSkillInstalled(skillName) {
|
|
67
17
|
const base = join(homedir(), '.claude');
|
|
68
18
|
const candidates = [];
|
|
@@ -136,7 +86,12 @@ function probe(cmd, args, interpret = (out) => ({
|
|
|
136
86
|
function defaultCheckBinary(bin) {
|
|
137
87
|
if (bin.startsWith('mcp:')) {
|
|
138
88
|
const name = bin.slice('mcp:'.length);
|
|
139
|
-
|
|
89
|
+
const scan = scanMcpConfigs();
|
|
90
|
+
if (name === 'nonbot') {
|
|
91
|
+
const first = scan.nonbot[0];
|
|
92
|
+
return first ? { installed: true, version: describeNonbotMcp(first) } : { installed: false };
|
|
93
|
+
}
|
|
94
|
+
return scan.names.has(name) ? { installed: true, version: scan.where.get(name) } : { installed: false };
|
|
140
95
|
}
|
|
141
96
|
if (bin.startsWith('skill:')) {
|
|
142
97
|
const name = bin.slice('skill:'.length);
|
|
@@ -160,6 +115,15 @@ function defaultCheckBinary(bin) {
|
|
|
160
115
|
return { installed: false };
|
|
161
116
|
}
|
|
162
117
|
}
|
|
118
|
+
export function bareVersion(raw) {
|
|
119
|
+
if (!raw)
|
|
120
|
+
return '';
|
|
121
|
+
const first = raw.split('\n')[0].trim();
|
|
122
|
+
const m = first.match(/\d+(?:\.\d+)+[0-9A-Za-z.+-]*/);
|
|
123
|
+
if (m)
|
|
124
|
+
return m[0];
|
|
125
|
+
return first.length > 40 ? first.slice(0, 39) + '…' : first;
|
|
126
|
+
}
|
|
163
127
|
export async function runDoctorCommand(args = [], deps = {}) {
|
|
164
128
|
void args;
|
|
165
129
|
const log = deps.log ?? ((s) => process.stdout.write(s));
|
|
@@ -169,46 +133,42 @@ export async function runDoctorCommand(args = [], deps = {}) {
|
|
|
169
133
|
const now = deps.now ?? (() => Date.now());
|
|
170
134
|
const checkBinary = deps.checkBinary ?? defaultCheckBinary;
|
|
171
135
|
let exitCode = 0;
|
|
136
|
+
const row = (sig, label, detail = '') => log(statusRow(sig, label, detail, { labelWidth: LABEL_WIDTH }) + '\n');
|
|
137
|
+
const errRow = (sig, label, detail = '') => errLog(statusRow(sig, label, detail, { labelWidth: LABEL_WIDTH, stream: process.stderr }) + '\n');
|
|
138
|
+
const hint = (text) => log(' '.repeat(HINT_INDENT) + paintDim(text) + '\n');
|
|
139
|
+
const section = (title) => log('\n' + paintSection(title) + '\n');
|
|
172
140
|
log(header('non.bot doctor', `v${VERSION} · ${process.platform}/${process.arch}`) + '\n');
|
|
173
|
-
log('\n');
|
|
174
141
|
const auth = await loader();
|
|
175
142
|
if (!auth) {
|
|
143
|
+
log('\n');
|
|
176
144
|
log(errorBlock('Not logged in', 'Run `nonbot login` to authenticate.'));
|
|
177
145
|
return 1;
|
|
178
146
|
}
|
|
179
|
-
|
|
180
|
-
|
|
147
|
+
section('ACCOUNT');
|
|
148
|
+
row('✓', 'Signed in', auth.email);
|
|
149
|
+
const headers = {
|
|
150
|
+
Authorization: `Bearer ${auth.pat}`,
|
|
151
|
+
'X-Requested-With': 'ConradPM-Native',
|
|
152
|
+
'X-CLI-Version': VERSION,
|
|
153
|
+
};
|
|
181
154
|
try {
|
|
182
|
-
const res = await fetchImpl(`${auth.baseUrl}/api/auth/me`, {
|
|
183
|
-
headers: {
|
|
184
|
-
Authorization: `Bearer ${auth.pat}`,
|
|
185
|
-
'X-Requested-With': 'ConradPM-Native',
|
|
186
|
-
'X-CLI-Version': VERSION,
|
|
187
|
-
},
|
|
188
|
-
});
|
|
155
|
+
const res = await fetchImpl(`${auth.baseUrl}/api/auth/me`, { headers });
|
|
189
156
|
if (res.ok) {
|
|
190
|
-
|
|
191
|
-
log(statusRow('✓', 'Server reachable', `${auth.baseUrl} · PAT valid`) + '\n');
|
|
157
|
+
row('✓', 'Server', `${auth.baseUrl} · token accepted`);
|
|
192
158
|
}
|
|
193
159
|
else if (res.status === 401) {
|
|
194
|
-
|
|
160
|
+
errRow('✗', 'Server', 'token rejected (HTTP 401) — sign in again: nonbot login');
|
|
195
161
|
exitCode = 1;
|
|
196
162
|
}
|
|
197
163
|
else {
|
|
198
|
-
|
|
164
|
+
row('⚠', 'Server', `${auth.baseUrl} answered HTTP ${res.status}`);
|
|
199
165
|
}
|
|
200
166
|
}
|
|
201
167
|
catch (e) {
|
|
202
|
-
|
|
168
|
+
row('⚠', 'Server', `Could not reach ${auth.baseUrl} (${e.message})`);
|
|
203
169
|
}
|
|
204
170
|
try {
|
|
205
|
-
const res = await fetchImpl(`${auth.baseUrl}/api/cli/daemon-status`, {
|
|
206
|
-
headers: {
|
|
207
|
-
Authorization: `Bearer ${auth.pat}`,
|
|
208
|
-
'X-Requested-With': 'ConradPM-Native',
|
|
209
|
-
'X-CLI-Version': VERSION,
|
|
210
|
-
},
|
|
211
|
-
});
|
|
171
|
+
const res = await fetchImpl(`${auth.baseUrl}/api/cli/daemon-status`, { headers });
|
|
212
172
|
if (res.ok) {
|
|
213
173
|
const body = (await res.json());
|
|
214
174
|
const last = body.lastSeenAt ?? body.lastHeartbeatAt;
|
|
@@ -216,86 +176,140 @@ export async function runDoctorCommand(args = [], deps = {}) {
|
|
|
216
176
|
(typeof last === 'number' && now() - last <= HEARTBEAT_FRESH_MS);
|
|
217
177
|
if (fresh) {
|
|
218
178
|
const detail = typeof last === 'number'
|
|
219
|
-
? `last heartbeat ${Math.round((now() - last) / 1000)}s ago`
|
|
220
|
-
: '
|
|
221
|
-
|
|
179
|
+
? `running · last heartbeat ${Math.round((now() - last) / 1000)}s ago`
|
|
180
|
+
: 'running';
|
|
181
|
+
row('✓', 'Daemon', detail);
|
|
222
182
|
}
|
|
223
183
|
else {
|
|
224
|
-
|
|
184
|
+
row('✗', 'Daemon', 'not running — start it: nonbot daemon');
|
|
225
185
|
}
|
|
226
186
|
}
|
|
227
187
|
else if (res.status === 401) {
|
|
228
|
-
|
|
188
|
+
row('⚠', 'Daemon', 'status unknown (auth rejected)');
|
|
189
|
+
}
|
|
190
|
+
else if (res.status === 403) {
|
|
191
|
+
let gate = {};
|
|
192
|
+
try {
|
|
193
|
+
gate = (await res.json());
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
}
|
|
197
|
+
const storeUrl = `${auth.baseUrl}/whygraph/store?tab=apps&app=${gate.app ?? 'run'}`;
|
|
198
|
+
if (gate.error === 'app_disabled') {
|
|
199
|
+
row('✗', 'Run app', 'switched off — the daemon gets no runs until it is on');
|
|
200
|
+
hint(`turn it on: ${storeUrl}`);
|
|
201
|
+
}
|
|
202
|
+
else if (gate.error === 'app_not_installed') {
|
|
203
|
+
row('✗', 'Run app', 'not added — the daemon gets no runs until it is');
|
|
204
|
+
hint(`add it: ${storeUrl}`);
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
row('⚠', 'Daemon', 'status unknown (HTTP 403)');
|
|
208
|
+
}
|
|
229
209
|
}
|
|
230
210
|
else {
|
|
231
|
-
|
|
211
|
+
row('⚠', 'Daemon', `status unknown (HTTP ${res.status})`);
|
|
232
212
|
}
|
|
233
213
|
}
|
|
234
214
|
catch (e) {
|
|
235
|
-
|
|
215
|
+
row('⚠', 'Daemon', `status unknown (${e.message})`);
|
|
236
216
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
217
|
+
section('AGENTS');
|
|
218
|
+
const providers = PROVIDER_CLIS.map((bin) => ({ bin, r: checkBinary(bin) }));
|
|
219
|
+
const anyProvider = providers.some((p) => p.r.installed);
|
|
220
|
+
for (const { bin, r } of providers) {
|
|
221
|
+
if (r.installed)
|
|
222
|
+
row('✓', bin, bareVersion(r.version));
|
|
223
|
+
else if (anyProvider)
|
|
224
|
+
row('ℹ', bin, 'not installed (optional)');
|
|
225
|
+
else
|
|
226
|
+
row('⚠', bin, 'not on PATH');
|
|
245
227
|
}
|
|
228
|
+
if (!anyProvider)
|
|
229
|
+
hint('Runs need one of these — install Claude Code: npm i -g @anthropic-ai/claude-code');
|
|
230
|
+
section('TERMINAL');
|
|
246
231
|
const profile = resolveTerminal(undefined);
|
|
247
232
|
const { cmd } = profile.launch('/tmp/nonbot-doctor-probe.sh');
|
|
248
233
|
const launcher = checkBinary(cmd);
|
|
249
234
|
if (launcher.installed) {
|
|
250
|
-
|
|
235
|
+
row('✓', 'Opens runs in', `${profile.displayName} · via ${cmd}`);
|
|
251
236
|
}
|
|
252
237
|
else {
|
|
253
|
-
|
|
238
|
+
row('⚠', 'Terminal', `${profile.displayName} — launcher "${cmd}" not found on PATH`);
|
|
254
239
|
}
|
|
255
240
|
if (process.platform === 'win32') {
|
|
256
241
|
const bash = checkBinary('bash');
|
|
257
|
-
if (bash.installed)
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
log(statusRow('⚠', 'Windows via WSL or Git Bash', 'bash not found on PATH — install WSL or Git Bash; Run scripts are #!/bin/bash') + '\n');
|
|
262
|
-
}
|
|
242
|
+
if (bash.installed)
|
|
243
|
+
row('✓', 'bash', 'found · Windows via WSL or Git Bash — runs are #!/bin/bash');
|
|
244
|
+
else
|
|
245
|
+
row('⚠', 'bash', 'not found · Windows via WSL or Git Bash — runs are #!/bin/bash');
|
|
263
246
|
}
|
|
264
247
|
if (process.platform === 'darwin') {
|
|
265
248
|
const running = checkBinary('iterm-running');
|
|
266
|
-
if (running.installed) {
|
|
267
|
-
|
|
268
|
-
}
|
|
269
|
-
else {
|
|
270
|
-
log(statusRow('⚠', 'iTerm not running', 'to start iTerm: open -a iTerm') + '\n');
|
|
271
|
-
}
|
|
272
|
-
const appl = checkBinary('iterm-applescript');
|
|
273
|
-
if (appl.installed) {
|
|
274
|
-
log(statusRow('✓', 'iTerm AppleScript reachable', appl.version ?? '') + '\n');
|
|
249
|
+
if (!running.installed) {
|
|
250
|
+
row('ℹ', 'iTerm', 'not running — only needed if runs open in iTerm');
|
|
275
251
|
}
|
|
276
252
|
else {
|
|
277
|
-
|
|
278
|
-
|
|
253
|
+
const appl = checkBinary('iterm-applescript');
|
|
254
|
+
if (appl.installed) {
|
|
255
|
+
const v = bareVersion(appl.version);
|
|
256
|
+
row('✓', 'iTerm', v ? `${v} · running · AppleScript reachable` : 'running · AppleScript reachable');
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
row('⚠', 'iTerm', appl.version ? `AppleScript blocked: ${appl.version}` : 'AppleScript blocked');
|
|
260
|
+
hint('System Settings → Privacy & Security → Automation → allow your terminal to control iTerm');
|
|
261
|
+
}
|
|
279
262
|
}
|
|
280
263
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
264
|
+
section('CLAUDE CODE');
|
|
265
|
+
const statusSource = deps.nonbotMcp ?? (deps.checkBinary ? null : () => nonbotMcpStatuses({ fetchImpl: deps.fetchImpl }));
|
|
266
|
+
const statuses = statusSource ? await statusSource().catch(() => []) : null;
|
|
267
|
+
const nonbotMcp = statuses ? { installed: statuses.length > 0 } : checkBinary('mcp:nonbot');
|
|
268
|
+
if (statuses && statuses.length > 0) {
|
|
269
|
+
const r = mcpRow(statuses[0]);
|
|
270
|
+
row(r.sigil, 'nonbot MCP', r.detail);
|
|
271
|
+
if (r.hint)
|
|
272
|
+
hint(r.hint);
|
|
273
|
+
}
|
|
274
|
+
else if (nonbotMcp.installed) {
|
|
275
|
+
const how = nonbotMcp.version ?? 'configured (@nonbot/mcp)';
|
|
276
|
+
row(/pinned/.test(how) ? '⚠' : '✓', 'nonbot MCP', how);
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
row('⚠', 'nonbot MCP', 'not set up — runs cannot report back without it');
|
|
280
|
+
hint(`add it: ${NONBOT_MCP_ADD_HINT}`);
|
|
281
|
+
}
|
|
282
|
+
for (const name of LEGACY_MCPS) {
|
|
283
|
+
const legacy = checkBinary(`mcp:${name}`);
|
|
284
|
+
if (legacy.installed) {
|
|
285
|
+
row('ℹ', name, legacy.version ? `retired — safe to delete from ${legacy.version}` : 'retired — safe to remove');
|
|
288
286
|
}
|
|
289
287
|
}
|
|
290
288
|
for (const name of EXPECTED_SKILLS) {
|
|
291
289
|
const r = checkBinary(`skill:${name}`);
|
|
292
|
-
if (r.installed)
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
log(statusRow('⚠', `Skill ${name}`, 'not installed under ~/.claude/skills/') + '\n');
|
|
297
|
-
}
|
|
290
|
+
if (r.installed)
|
|
291
|
+
row('✓', `Skill ${name}`);
|
|
292
|
+
else
|
|
293
|
+
row('⚠', `Skill ${name}`, 'not installed under ~/.claude/skills/');
|
|
298
294
|
}
|
|
299
|
-
|
|
295
|
+
section('COMMANDS');
|
|
296
|
+
log(commandRow('nonbot daemon', 'start listening for runs'));
|
|
297
|
+
log(commandRow('Ctrl-C', 'stop the daemon (in its window)'));
|
|
298
|
+
log(commandRow('nonbot daemon stop', 'stop it from any other terminal'));
|
|
299
|
+
log(commandRow('nonbot logout', 'sign out on this machine and stop the daemon'));
|
|
300
300
|
return exitCode;
|
|
301
301
|
}
|
|
302
|
+
const LABEL_WIDTH = 14;
|
|
303
|
+
const HINT_INDENT = 3 + 1 + 2 + LABEL_WIDTH + 1;
|
|
304
|
+
function paintDim(s) {
|
|
305
|
+
return isTTY(process.stdout) ? `${ANSI.dim}${s}${ANSI.reset}` : s;
|
|
306
|
+
}
|
|
307
|
+
function paintSection(title) {
|
|
308
|
+
return isTTY(process.stdout) ? ` ${ANSI.bold}${title}${ANSI.reset}` : ` ${title}`;
|
|
309
|
+
}
|
|
310
|
+
function commandRow(cmd, what) {
|
|
311
|
+
const padded = cmd.padEnd(LABEL_WIDTH + 6, ' ');
|
|
312
|
+
if (!isTTY(process.stdout))
|
|
313
|
+
return ` ${padded}${what}\n`;
|
|
314
|
+
return ` ${ANSI.cyan}${padded}${ANSI.reset}${ANSI.dim}${what}${ANSI.reset}\n`;
|
|
315
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { spawnSync as nodeSpawnSync } from 'node:child_process';
|
|
2
|
+
import { loadAuth, clearAuth, getActiveProfile } from '../lib/auth.js';
|
|
3
|
+
import { readPidFile, stopDaemonLocally } from '../lib/daemon-lifecycle.js';
|
|
4
|
+
import { header, statusRow } from '../lib/output.js';
|
|
5
|
+
function defaultDaemonRunning() {
|
|
6
|
+
const pid = readPidFile();
|
|
7
|
+
if (pid !== null) {
|
|
8
|
+
try {
|
|
9
|
+
process.kill(pid, 0);
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const r = nodeSpawnSync('tmux', ['has-session', '-t', 'nonbot'], {
|
|
17
|
+
encoding: 'utf-8',
|
|
18
|
+
timeout: 2000,
|
|
19
|
+
windowsHide: true,
|
|
20
|
+
});
|
|
21
|
+
return r.status === 0;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export async function runLogoutCommand(args = [], deps = {}) {
|
|
28
|
+
void args;
|
|
29
|
+
const log = deps.log ?? ((s) => process.stdout.write(s));
|
|
30
|
+
const errLog = deps.errLog ?? ((s) => process.stderr.write(s));
|
|
31
|
+
const loader = deps.loadAuth ?? loadAuth;
|
|
32
|
+
const clear = deps.clearAuth ?? clearAuth;
|
|
33
|
+
const daemonRunning = deps.daemonRunning ?? defaultDaemonRunning;
|
|
34
|
+
const stopDaemon = deps.stopDaemon ?? ((l, e) => stopDaemonLocally({ log: l, errLog: e }));
|
|
35
|
+
const profile = getActiveProfile();
|
|
36
|
+
log(header('non.bot logout', profile === 'default' ? '' : `profile ${profile}`) + '\n\n');
|
|
37
|
+
const auth = await loader();
|
|
38
|
+
const running = daemonRunning();
|
|
39
|
+
if (!auth && !running) {
|
|
40
|
+
log(statusRow('✓', 'Already signed out', 'nothing saved on this machine, no daemon running') + '\n');
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
let exitCode = 0;
|
|
44
|
+
if (auth) {
|
|
45
|
+
try {
|
|
46
|
+
await clear();
|
|
47
|
+
log(statusRow('✓', 'Signed out', `${auth.email} · token removed from this machine`) + '\n');
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
errLog(statusRow('✗', 'Sign-out failed', `could not remove the saved token: ${e.message}`, { stream: process.stderr }) + '\n');
|
|
51
|
+
exitCode = 1;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
log(statusRow('✓', 'Signed out', 'no token was saved on this machine') + '\n');
|
|
56
|
+
}
|
|
57
|
+
if (running) {
|
|
58
|
+
const said = [];
|
|
59
|
+
const code = await stopDaemon((s) => said.push(s), (s) => said.push(s));
|
|
60
|
+
if (code === 0) {
|
|
61
|
+
log(statusRow('✓', 'Daemon stopped', '') + '\n');
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
const why = said.join(' ').replace(/\s+/g, ' ').replace(/^[✓✗⚠]\s*/, '').trim();
|
|
65
|
+
errLog(statusRow('⚠', 'Daemon still running', why || 'stop it with Ctrl-C in its window', { stream: process.stderr }) + '\n');
|
|
66
|
+
exitCode = 1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
log(statusRow('✓', 'Daemon', 'was not running') + '\n');
|
|
71
|
+
}
|
|
72
|
+
if (auth) {
|
|
73
|
+
log('\n');
|
|
74
|
+
log(statusRow('ℹ', 'Token', `still exists on non.bot — delete it for good at ${auth.baseUrl}/settings/connections`) + '\n');
|
|
75
|
+
log(statusRow('ℹ', 'Sign back in', 'nonbot login') + '\n');
|
|
76
|
+
}
|
|
77
|
+
return exitCode;
|
|
78
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { spawnSync as nodeSpawnSync } from 'node:child_process';
|
|
2
|
+
import { realpathSync } from 'node:fs';
|
|
3
|
+
import { dirname, sep } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { VERSION } from '../version.js';
|
|
6
|
+
import { fetchLatestVersion, compareVersions } from '../lib/update-check.js';
|
|
7
|
+
import { readPidFile } from '../lib/daemon-lifecycle.js';
|
|
8
|
+
import { header, statusRow } from '../lib/output.js';
|
|
9
|
+
import { nonbotMcpStatuses, mcpRow, globalMcpIsLinked } from '../lib/mcp-status.js';
|
|
10
|
+
export const UPDATE_INSTALL_ARGS = ['install', '-g', '@nonbot/cli@latest'];
|
|
11
|
+
function defaultEntryPath() {
|
|
12
|
+
const here = fileURLToPath(import.meta.url);
|
|
13
|
+
try {
|
|
14
|
+
return realpathSync(here);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return here;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function isLinkedCheckout(entry) {
|
|
21
|
+
const marker = `${sep}node_modules${sep}@nonbot${sep}cli${sep}`;
|
|
22
|
+
return !entry.split('/').join(sep).includes(marker);
|
|
23
|
+
}
|
|
24
|
+
function checkoutRoot(entry) {
|
|
25
|
+
let d = dirname(entry);
|
|
26
|
+
while (d !== dirname(d)) {
|
|
27
|
+
if (d.endsWith(`${sep}dist`))
|
|
28
|
+
return dirname(d);
|
|
29
|
+
d = dirname(d);
|
|
30
|
+
}
|
|
31
|
+
return dirname(entry);
|
|
32
|
+
}
|
|
33
|
+
function defaultRunNpm(args) {
|
|
34
|
+
const r = nodeSpawnSync('npm', [...args], {
|
|
35
|
+
stdio: 'inherit',
|
|
36
|
+
shell: process.platform === 'win32',
|
|
37
|
+
});
|
|
38
|
+
if (r.error)
|
|
39
|
+
return 127;
|
|
40
|
+
return r.status ?? 1;
|
|
41
|
+
}
|
|
42
|
+
function defaultDaemonRunning() {
|
|
43
|
+
const pid = readPidFile();
|
|
44
|
+
if (pid === null)
|
|
45
|
+
return false;
|
|
46
|
+
try {
|
|
47
|
+
process.kill(pid, 0);
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export async function runUpdateCommand(args = [], deps = {}) {
|
|
55
|
+
const log = deps.log ?? ((s) => process.stdout.write(s));
|
|
56
|
+
const errLog = deps.errLog ?? ((s) => process.stderr.write(s));
|
|
57
|
+
const positional = args.filter((a) => !a.startsWith('-'));
|
|
58
|
+
if (positional[0] === 'mcp')
|
|
59
|
+
return runUpdateMcpCommand(args.slice(1), deps);
|
|
60
|
+
if (positional.length > 0) {
|
|
61
|
+
errLog(`nonbot update: unknown target "${positional[0]}". Try: nonbot update (this CLI) · nonbot update mcp (the non.bot MCP)\n`);
|
|
62
|
+
return 1;
|
|
63
|
+
}
|
|
64
|
+
const current = deps.current ?? VERSION;
|
|
65
|
+
const fetchLatest = deps.fetchLatest ?? (() => fetchLatestVersion());
|
|
66
|
+
const entry = (deps.entryPath ?? defaultEntryPath)();
|
|
67
|
+
const runNpm = deps.runNpm ?? defaultRunNpm;
|
|
68
|
+
const daemonRunning = deps.daemonRunning ?? defaultDaemonRunning;
|
|
69
|
+
const force = args.includes('--force');
|
|
70
|
+
log(header('non.bot update', `v${current}`) + '\n\n');
|
|
71
|
+
const latest = await fetchLatest();
|
|
72
|
+
if (!latest) {
|
|
73
|
+
errLog(statusRow('⚠', 'Registry', 'could not read the latest version from npm — check your connection', { stream: process.stderr }) + '\n');
|
|
74
|
+
errLog(` or install it directly: npm ${UPDATE_INSTALL_ARGS.join(' ')}\n`);
|
|
75
|
+
return 1;
|
|
76
|
+
}
|
|
77
|
+
if (compareVersions(latest, current) <= 0) {
|
|
78
|
+
log(statusRow('✓', 'Up to date', `v${current} is the latest`) + '\n');
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
log(statusRow('ℹ', 'Update', `v${current} → v${latest}`) + '\n');
|
|
82
|
+
if (isLinkedCheckout(entry) && !force) {
|
|
83
|
+
const root = checkoutRoot(entry);
|
|
84
|
+
log(statusRow('⚠', 'Linked copy', `this nonbot runs from a source checkout: ${root}`) + '\n');
|
|
85
|
+
log(` update the checkout: cd ${root} && git pull && npm run build\n`);
|
|
86
|
+
log(` or replace the link with the published v${latest}: nonbot update --force\n`);
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
log(`\n npm ${UPDATE_INSTALL_ARGS.join(' ')}\n\n`);
|
|
90
|
+
const code = runNpm(UPDATE_INSTALL_ARGS);
|
|
91
|
+
log('\n');
|
|
92
|
+
if (code !== 0) {
|
|
93
|
+
errLog(statusRow('✗', 'Update failed', `npm exited ${code} — see its output above`, { stream: process.stderr }) + '\n');
|
|
94
|
+
return 1;
|
|
95
|
+
}
|
|
96
|
+
log(statusRow('✓', 'Updated', `v${latest} installed`) + '\n');
|
|
97
|
+
if (daemonRunning()) {
|
|
98
|
+
log(statusRow('ℹ', 'Daemon', 'still running the old version — restart it to use the new one:') + '\n');
|
|
99
|
+
log(' Ctrl-C in its window (or: nonbot daemon stop), then: nonbot daemon\n');
|
|
100
|
+
}
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
export const MCP_INSTALL_ARGS = ['install', '-g', '@nonbot/mcp@latest'];
|
|
104
|
+
export async function runUpdateMcpCommand(args = [], deps = {}) {
|
|
105
|
+
const log = deps.log ?? ((s) => process.stdout.write(s));
|
|
106
|
+
const errLog = deps.errLog ?? ((s) => process.stderr.write(s));
|
|
107
|
+
const statusesOf = deps.mcpStatuses ?? (() => nonbotMcpStatuses());
|
|
108
|
+
const runNpm = deps.runNpm ?? defaultRunNpm;
|
|
109
|
+
const linked = deps.globalMcpLinked ?? (() => globalMcpIsLinked());
|
|
110
|
+
const force = args.includes('--force');
|
|
111
|
+
const W = 14;
|
|
112
|
+
const row = (sig, label, detail = '') => log(statusRow(sig, label, detail, { labelWidth: W }) + '\n');
|
|
113
|
+
const hint = (text) => log(' '.repeat(3 + 1 + 2 + W + 1) + text + '\n');
|
|
114
|
+
const statuses = await statusesOf();
|
|
115
|
+
const latest = statuses.find((s) => s.latest)?.latest ?? null;
|
|
116
|
+
log(header('non.bot update mcp', latest ? `v${latest} on npm` : 'npm not reachable') + '\n\n');
|
|
117
|
+
if (statuses.length === 0) {
|
|
118
|
+
row('⚠', 'nonbot MCP', 'not set up in Claude Code');
|
|
119
|
+
hint('add it: claude mcp add nonbot -- npx -y @nonbot/mcp');
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
let changed = false;
|
|
123
|
+
let failed = false;
|
|
124
|
+
let globalDone = false;
|
|
125
|
+
for (const s of statuses) {
|
|
126
|
+
const r = mcpRow(s);
|
|
127
|
+
row(r.sigil, 'nonbot MCP', r.detail);
|
|
128
|
+
hint(`in ${s.entry.source}`);
|
|
129
|
+
const behind = !!(s.installed && s.latest && compareVersions(s.latest, s.installed) > 0);
|
|
130
|
+
if (s.entry.kind === 'global' && (behind || !s.installed) && !globalDone) {
|
|
131
|
+
globalDone = true;
|
|
132
|
+
if (linked() && !force) {
|
|
133
|
+
hint('the global @nonbot/mcp is linked to a source checkout — left alone (nonbot update mcp --force replaces it)');
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
log(`\n npm ${MCP_INSTALL_ARGS.join(' ')}\n\n`);
|
|
137
|
+
const code = runNpm(MCP_INSTALL_ARGS);
|
|
138
|
+
log('\n');
|
|
139
|
+
if (code === 0) {
|
|
140
|
+
row('✓', 'Updated', latest ? `@nonbot/mcp v${latest} installed` : '@nonbot/mcp installed');
|
|
141
|
+
changed = true;
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
errLog(statusRow('✗', 'Update failed', `npm exited ${code} — see its output above`, { labelWidth: W, stream: process.stderr }) + '\n');
|
|
145
|
+
failed = true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else if (r.hint) {
|
|
149
|
+
hint(r.hint);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (changed) {
|
|
153
|
+
log('\n');
|
|
154
|
+
row('ℹ', 'Load it', 'restart Claude Code, or run /mcp → Reconnect');
|
|
155
|
+
}
|
|
156
|
+
return failed ? 1 : 0;
|
|
157
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -6,95 +6,159 @@ import { runDaemonCommand } from './commands/daemon.js';
|
|
|
6
6
|
import { runLoginCommand } from './commands/login.js';
|
|
7
7
|
import { runRunCommand } from './commands/run.js';
|
|
8
8
|
import { runDoctorCommand } from './commands/doctor.js';
|
|
9
|
+
import { runLogoutCommand } from './commands/logout.js';
|
|
10
|
+
import { runUpdateCommand } from './commands/update.js';
|
|
9
11
|
import { runLogsCommand } from './commands/logs.js';
|
|
10
12
|
import { runProfilesCommand } from './commands/profiles.js';
|
|
11
13
|
import { runChoirCommand } from './commands/choir.js';
|
|
12
14
|
import { runRunMcpCommand } from './commands/run-mcp.js';
|
|
13
15
|
import { runRunPromptHookCommand } from './commands/run-prompt-hook.js';
|
|
14
16
|
import { setActiveProfile } from './lib/auth.js';
|
|
15
|
-
import { header, kvRow, ANSI, isTTY } from './lib/output.js';
|
|
17
|
+
import { header, kvRow, ANSI, isTTY, wordmarkArt, WORDMARK_WIDTH } from './lib/output.js';
|
|
16
18
|
const COMMANDS = [
|
|
17
19
|
{
|
|
18
20
|
name: 'login',
|
|
19
|
-
description: '
|
|
21
|
+
description: 'Sign in with a token from non.bot Settings → Connections',
|
|
22
|
+
group: 'start',
|
|
20
23
|
run: (args) => runLoginCommand(args),
|
|
21
24
|
},
|
|
25
|
+
{
|
|
26
|
+
name: 'logout',
|
|
27
|
+
description: 'Sign out on this machine and stop the daemon',
|
|
28
|
+
group: 'start',
|
|
29
|
+
run: (args) => runLogoutCommand(args),
|
|
30
|
+
},
|
|
22
31
|
{
|
|
23
32
|
name: 'daemon',
|
|
24
|
-
description: '
|
|
33
|
+
description: 'Listen for runs from non.bot; each opens a terminal here',
|
|
34
|
+
group: 'start',
|
|
25
35
|
run: (args) => runDaemonCommand(args),
|
|
26
36
|
},
|
|
37
|
+
{
|
|
38
|
+
name: 'update',
|
|
39
|
+
description: 'Install the latest version · update mcp: the non.bot MCP',
|
|
40
|
+
usage: 'update [mcp]',
|
|
41
|
+
group: 'start',
|
|
42
|
+
run: (args) => runUpdateCommand(args),
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'status',
|
|
46
|
+
description: 'Who is signed in and whether the daemon is connected',
|
|
47
|
+
group: 'check',
|
|
48
|
+
run: (args) => runStatusCommand(args),
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'doctor',
|
|
52
|
+
description: 'Health check: account, agents, terminal, Claude Code',
|
|
53
|
+
group: 'check',
|
|
54
|
+
run: (args) => runDoctorCommand(args),
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'logs',
|
|
58
|
+
description: 'Runs launched on this machine (--limit <n>, --json, -f)',
|
|
59
|
+
group: 'check',
|
|
60
|
+
run: (args) => runLogsCommand(args),
|
|
61
|
+
},
|
|
27
62
|
{
|
|
28
63
|
name: 'run',
|
|
29
|
-
|
|
64
|
+
usage: 'run <id>',
|
|
65
|
+
description: 'Launch one run by id, then exit',
|
|
66
|
+
group: 'more',
|
|
30
67
|
run: (args) => runRunCommand(args),
|
|
31
68
|
},
|
|
32
69
|
{
|
|
33
70
|
name: 'choir',
|
|
34
|
-
description: 'Coordinated-run engine
|
|
71
|
+
description: 'Coordinated-run engine — one agent per git worktree',
|
|
72
|
+
usage: 'choir <session>',
|
|
73
|
+
group: 'more',
|
|
35
74
|
run: (args) => runChoirCommand(args),
|
|
36
75
|
},
|
|
76
|
+
{
|
|
77
|
+
name: 'profiles',
|
|
78
|
+
description: 'List signed-in accounts (pick one with --profile <name>)',
|
|
79
|
+
group: 'more',
|
|
80
|
+
run: (args) => runProfilesCommand(args),
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'test',
|
|
84
|
+
description: 'Print a local diagnostic banner (no server needed)',
|
|
85
|
+
group: 'more',
|
|
86
|
+
run: () => runTestCommand(),
|
|
87
|
+
},
|
|
37
88
|
{
|
|
38
89
|
name: 'run-mcp',
|
|
39
90
|
description: 'Per-pane fleet MCP server over stdio. Launched by .mcp.json — not run by hand.',
|
|
91
|
+
group: 'hidden',
|
|
40
92
|
run: () => runRunMcpCommand(),
|
|
41
93
|
},
|
|
42
94
|
{
|
|
43
95
|
name: 'choir-mcp',
|
|
44
96
|
description: 'Deprecated alias for run-mcp.',
|
|
97
|
+
group: 'hidden',
|
|
45
98
|
run: () => runRunMcpCommand(),
|
|
46
99
|
},
|
|
47
100
|
{
|
|
48
101
|
name: 'run-prompt-hook',
|
|
49
102
|
description: "Claude Code Notification-hook handler — surfaces a Run's mid-run prompt. Not run by hand.",
|
|
103
|
+
group: 'hidden',
|
|
50
104
|
run: () => runRunPromptHookCommand(),
|
|
51
105
|
},
|
|
52
|
-
{
|
|
53
|
-
name: 'status',
|
|
54
|
-
description: 'Report login state + last daemon heartbeat.',
|
|
55
|
-
run: (args) => runStatusCommand(args),
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
name: 'doctor',
|
|
59
|
-
description: 'Health check — auth, server, daemon, provider CLIs, terminal.',
|
|
60
|
-
run: (args) => runDoctorCommand(args),
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
name: 'logs',
|
|
64
|
-
description: 'Local activation history. Flags: --limit <n>, --json, --follow/-f.',
|
|
65
|
-
run: (args) => runLogsCommand(args),
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
name: 'profiles',
|
|
69
|
-
description: 'List multi-account auth profiles; marks the active one.',
|
|
70
|
-
run: (args) => runProfilesCommand(args),
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
name: 'test',
|
|
74
|
-
description: 'Print the diagnostic banner locally — no server roundtrip.',
|
|
75
|
-
run: () => runTestCommand(),
|
|
76
|
-
},
|
|
77
106
|
];
|
|
78
107
|
function printVersion() {
|
|
79
108
|
process.stdout.write(VERSION + '\n');
|
|
80
109
|
}
|
|
81
110
|
function printHelp() {
|
|
82
111
|
const tty = isTTY(process.stdout);
|
|
112
|
+
const head = (t) => (tty ? ` ${ANSI.bold}${t}${ANSI.reset}` : ` ${t}`);
|
|
113
|
+
const KEY = 18;
|
|
83
114
|
const lines = [];
|
|
84
|
-
lines.push(header('nonbot', `non.bot CLI
|
|
115
|
+
lines.push(header('nonbot', `non.bot CLI · v${VERSION}`));
|
|
116
|
+
lines.push('');
|
|
117
|
+
lines.push(kvRow('Usage', 'nonbot <command> [args]', { keyWidth: KEY }));
|
|
118
|
+
const section = (title, group) => {
|
|
119
|
+
lines.push('');
|
|
120
|
+
lines.push(head(title));
|
|
121
|
+
for (const cmd of COMMANDS.filter((c) => c.group === group)) {
|
|
122
|
+
lines.push(kvRow(cmd.usage ?? cmd.name, cmd.description, { keyWidth: KEY }));
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
section('Get started', 'start');
|
|
126
|
+
lines.push('');
|
|
127
|
+
lines.push(head('Stop the daemon'));
|
|
128
|
+
lines.push(kvRow('Ctrl-C', 'in the daemon\'s window', { keyWidth: KEY }));
|
|
129
|
+
lines.push(kvRow('daemon stop', 'from another terminal (--machine <id> / --all)', { keyWidth: KEY }));
|
|
130
|
+
lines.push(kvRow('daemon attach', 'bring back a closed daemon window', { keyWidth: KEY }));
|
|
131
|
+
section('Check', 'check');
|
|
132
|
+
section('More', 'more');
|
|
85
133
|
lines.push('');
|
|
86
|
-
lines.push(
|
|
134
|
+
lines.push(head('Flags'));
|
|
135
|
+
lines.push(kvRow('--profile <name>', 'use a named account (works with any command)', { keyWidth: KEY }));
|
|
136
|
+
lines.push(kvRow('-v, --version', 'print the version', { keyWidth: KEY }));
|
|
137
|
+
lines.push(kvRow('-h, --help', 'print this help', { keyWidth: KEY }));
|
|
87
138
|
lines.push('');
|
|
88
|
-
lines.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
139
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
140
|
+
}
|
|
141
|
+
function printQuickStart() {
|
|
142
|
+
const KEY = 8;
|
|
143
|
+
const lines = [];
|
|
144
|
+
const cols = process.stdout.columns ?? 0;
|
|
145
|
+
if (cols === 0 || cols >= WORDMARK_WIDTH + 4) {
|
|
146
|
+
lines.push('');
|
|
147
|
+
lines.push(wordmarkArt({ indent: ' ' }));
|
|
148
|
+
lines.push('');
|
|
149
|
+
lines.push(kvRow('', `the non.bot CLI · v${VERSION}`, { keyWidth: KEY }));
|
|
92
150
|
}
|
|
151
|
+
else {
|
|
152
|
+
lines.push(header('non.bot CLI', `v${VERSION}`));
|
|
153
|
+
}
|
|
154
|
+
lines.push('');
|
|
155
|
+
lines.push(kvRow('login', 'sign in', { keyWidth: KEY }));
|
|
156
|
+
lines.push(kvRow('daemon', 'listen for runs — Ctrl-C stops it', { keyWidth: KEY }));
|
|
157
|
+
lines.push(kvRow('doctor', 'check that everything is set up', { keyWidth: KEY }));
|
|
158
|
+
lines.push(kvRow('logout', 'sign out and stop the daemon', { keyWidth: KEY }));
|
|
159
|
+
lines.push(kvRow('update', 'install the latest version', { keyWidth: KEY }));
|
|
93
160
|
lines.push('');
|
|
94
|
-
lines.push(
|
|
95
|
-
lines.push(kvRow('--profile <name>', 'Use a named auth profile (multi-account). Default: "default".', { keyWidth: 18 }));
|
|
96
|
-
lines.push(kvRow('-v, --version', 'Print CLI version and exit.', { keyWidth: 18 }));
|
|
97
|
-
lines.push(kvRow('-h, --help', 'Print this help and exit.', { keyWidth: 18 }));
|
|
161
|
+
lines.push(kvRow('', 'type nonbot <command> · nonbot --help lists everything', { keyWidth: KEY }));
|
|
98
162
|
lines.push('');
|
|
99
163
|
process.stdout.write(lines.join('\n') + '\n');
|
|
100
164
|
}
|
|
@@ -131,7 +195,7 @@ async function main(argv) {
|
|
|
131
195
|
}
|
|
132
196
|
}
|
|
133
197
|
if (args.length === 0) {
|
|
134
|
-
|
|
198
|
+
printQuickStart();
|
|
135
199
|
return 0;
|
|
136
200
|
}
|
|
137
201
|
const first = args[0];
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { basename, dirname, join, resolve, isAbsolute } from 'node:path';
|
|
4
|
+
function defaultReadFile(path) {
|
|
5
|
+
try {
|
|
6
|
+
return existsSync(path) ? readFileSync(path, 'utf-8') : null;
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function tilde(path, home) {
|
|
13
|
+
return path.startsWith(home) ? '~' + path.slice(home.length) : path;
|
|
14
|
+
}
|
|
15
|
+
export function classifyServer(name, server, baseDir) {
|
|
16
|
+
const args = Array.isArray(server.args) ? server.args.filter((a) => typeof a === 'string') : [];
|
|
17
|
+
const command = typeof server.command === 'string' ? server.command : '';
|
|
18
|
+
const url = typeof server.url === 'string' ? server.url : '';
|
|
19
|
+
if (/(^|\/\/|\.)mcp\.non\.bot\b/.test(url) || args.some((a) => /mcp\.non\.bot/.test(a))) {
|
|
20
|
+
return { kind: 'remote' };
|
|
21
|
+
}
|
|
22
|
+
const spec = args.find((a) => /^@nonbot\/mcp(@[^\s]+)?$/.test(a));
|
|
23
|
+
if (spec) {
|
|
24
|
+
const at = spec.indexOf('@', 1);
|
|
25
|
+
const version = at > 0 ? spec.slice(at + 1) : undefined;
|
|
26
|
+
return { kind: 'npx', pinned: version && version !== 'latest' ? version : undefined };
|
|
27
|
+
}
|
|
28
|
+
if (basename(command) === 'nonbot-mcp' || basename(command) === 'nonbot-mcp.cmd') {
|
|
29
|
+
return { kind: 'global' };
|
|
30
|
+
}
|
|
31
|
+
const script = [command, ...args].find((a) => /nonbot-mcp[\\/]+dist[\\/]+stdio\.js$/.test(a));
|
|
32
|
+
if (script) {
|
|
33
|
+
const abs = isAbsolute(script) ? script : resolve(baseDir, script);
|
|
34
|
+
return { kind: 'checkout', checkoutDir: dirname(dirname(abs)) };
|
|
35
|
+
}
|
|
36
|
+
if (name === 'nonbot')
|
|
37
|
+
return { kind: 'unknown' };
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
function serversIn(json) {
|
|
41
|
+
const servers = json?.mcpServers;
|
|
42
|
+
if (!servers || typeof servers !== 'object')
|
|
43
|
+
return [];
|
|
44
|
+
return Object.entries(servers).filter(([, v]) => v !== null && typeof v === 'object');
|
|
45
|
+
}
|
|
46
|
+
function parse(text) {
|
|
47
|
+
if (!text)
|
|
48
|
+
return null;
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(text);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function scanMcpConfigs(deps = {}) {
|
|
57
|
+
const home = deps.home ?? homedir();
|
|
58
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
59
|
+
const platform = deps.platform ?? process.platform;
|
|
60
|
+
const read = deps.readFile ?? defaultReadFile;
|
|
61
|
+
const names = new Set();
|
|
62
|
+
const where = new Map();
|
|
63
|
+
const nonbot = [];
|
|
64
|
+
const seenFiles = new Set();
|
|
65
|
+
const take = (source, json, baseDir) => {
|
|
66
|
+
for (const [name, server] of serversIn(json)) {
|
|
67
|
+
names.add(name);
|
|
68
|
+
if (!where.has(name))
|
|
69
|
+
where.set(name, source);
|
|
70
|
+
const c = classifyServer(name, server, baseDir);
|
|
71
|
+
if (c)
|
|
72
|
+
nonbot.push({ source, name, ...c });
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const claudeJsonPath = join(home, '.claude.json');
|
|
76
|
+
const claudeJson = parse(read(claudeJsonPath));
|
|
77
|
+
const projectDirs = [];
|
|
78
|
+
if (claudeJson) {
|
|
79
|
+
take('~/.claude.json', claudeJson, home);
|
|
80
|
+
if (claudeJson.projects && typeof claudeJson.projects === 'object') {
|
|
81
|
+
for (const [dir, proj] of Object.entries(claudeJson.projects)) {
|
|
82
|
+
take(`~/.claude.json (${tilde(dir, home)})`, proj, dir);
|
|
83
|
+
projectDirs.push(dir);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const mcpJsonDirs = [];
|
|
88
|
+
for (let d = resolve(cwd);; d = dirname(d)) {
|
|
89
|
+
mcpJsonDirs.push(d);
|
|
90
|
+
if (d === dirname(d) || d === home)
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
mcpJsonDirs.push(...projectDirs);
|
|
94
|
+
for (const dir of mcpJsonDirs) {
|
|
95
|
+
const file = join(dir, '.mcp.json');
|
|
96
|
+
if (seenFiles.has(file))
|
|
97
|
+
continue;
|
|
98
|
+
seenFiles.add(file);
|
|
99
|
+
const json = parse(read(file));
|
|
100
|
+
if (json)
|
|
101
|
+
take(tilde(file, home), json, dir);
|
|
102
|
+
}
|
|
103
|
+
if (platform === 'darwin') {
|
|
104
|
+
const desktop = join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
|
|
105
|
+
const json = parse(read(desktop));
|
|
106
|
+
if (json)
|
|
107
|
+
take('Claude Desktop', json, home);
|
|
108
|
+
}
|
|
109
|
+
return { names, where, nonbot };
|
|
110
|
+
}
|
|
111
|
+
export function describeNonbotMcp(e, home = homedir()) {
|
|
112
|
+
switch (e.kind) {
|
|
113
|
+
case 'remote':
|
|
114
|
+
return 'remote (mcp.non.bot) · always current';
|
|
115
|
+
case 'npx':
|
|
116
|
+
return e.pinned ? `npx, pinned to ${e.pinned} · gets no updates` : 'npx @nonbot/mcp · updates itself';
|
|
117
|
+
case 'global':
|
|
118
|
+
return 'global install · update: npm i -g @nonbot/mcp';
|
|
119
|
+
case 'checkout':
|
|
120
|
+
return `source checkout ${e.checkoutDir ? tilde(e.checkoutDir, home) : ''}`.trim();
|
|
121
|
+
default:
|
|
122
|
+
return 'configured';
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, lstatSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fetchLatestVersion, compareVersions, parseSemver } from './update-check.js';
|
|
5
|
+
import { scanMcpConfigs, describeNonbotMcp } from './mcp-config.js';
|
|
6
|
+
export const MCP_LATEST_URL = 'https://registry.npmjs.org/@nonbot/mcp/latest';
|
|
7
|
+
export const MCP_REMOTE_URL = 'https://mcp.non.bot/';
|
|
8
|
+
function readVersion(pkgJsonPath) {
|
|
9
|
+
try {
|
|
10
|
+
if (!existsSync(pkgJsonPath))
|
|
11
|
+
return null;
|
|
12
|
+
const v = JSON.parse(readFileSync(pkgJsonPath, 'utf-8')).version;
|
|
13
|
+
return parseSemver(v) ? String(v) : null;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function defaultGlobalRoot(execPath = process.execPath, platform = process.platform) {
|
|
20
|
+
return platform === 'win32'
|
|
21
|
+
? join(dirname(execPath), 'node_modules')
|
|
22
|
+
: join(dirname(dirname(execPath)), 'lib', 'node_modules');
|
|
23
|
+
}
|
|
24
|
+
export function globalMcpIsLinked(globalRoot = defaultGlobalRoot()) {
|
|
25
|
+
try {
|
|
26
|
+
return lstatSync(join(globalRoot, '@nonbot', 'mcp')).isSymbolicLink();
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function npxCachedMcpVersion(env = process.env, home = homedir()) {
|
|
33
|
+
const root = join(env.npm_config_cache || join(home, '.npm'), '_npx');
|
|
34
|
+
let best = null;
|
|
35
|
+
let dirs = [];
|
|
36
|
+
try {
|
|
37
|
+
dirs = readdirSync(root);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
for (const hash of dirs) {
|
|
43
|
+
const v = readVersion(join(root, hash, 'node_modules', '@nonbot', 'mcp', 'package.json'));
|
|
44
|
+
if (v && (best === null || compareVersions(v, best) > 0))
|
|
45
|
+
best = v;
|
|
46
|
+
}
|
|
47
|
+
return best;
|
|
48
|
+
}
|
|
49
|
+
export async function remoteMcpVersion(fetchImpl = fetch) {
|
|
50
|
+
try {
|
|
51
|
+
const res = await fetchImpl(MCP_REMOTE_URL, {
|
|
52
|
+
headers: { Accept: 'text/html' },
|
|
53
|
+
signal: AbortSignal.timeout(3_000),
|
|
54
|
+
});
|
|
55
|
+
if (!res.ok)
|
|
56
|
+
return null;
|
|
57
|
+
const m = /version\s+(\d{1,6}\.\d{1,6}\.\d{1,6})\b/.exec(await res.text());
|
|
58
|
+
return m ? m[1] : null;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export async function installedMcpVersion(entry, deps = {}) {
|
|
65
|
+
switch (entry.kind) {
|
|
66
|
+
case 'checkout':
|
|
67
|
+
return entry.checkoutDir ? readVersion(join(entry.checkoutDir, 'package.json')) : null;
|
|
68
|
+
case 'npx':
|
|
69
|
+
return entry.pinned && parseSemver(entry.pinned) ? entry.pinned : npxCachedMcpVersion(deps.env, deps.home);
|
|
70
|
+
case 'global':
|
|
71
|
+
return readVersion(join(deps.globalRoot ?? defaultGlobalRoot(), '@nonbot', 'mcp', 'package.json'));
|
|
72
|
+
case 'remote':
|
|
73
|
+
return remoteMcpVersion(deps.fetchImpl);
|
|
74
|
+
default:
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export async function nonbotMcpStatuses(deps = {}) {
|
|
79
|
+
const entries = scanMcpConfigs(deps).nonbot;
|
|
80
|
+
if (entries.length === 0)
|
|
81
|
+
return [];
|
|
82
|
+
const [latest, installed] = await Promise.all([
|
|
83
|
+
fetchLatestVersion({ url: MCP_LATEST_URL, fetchImpl: deps.fetchImpl }),
|
|
84
|
+
Promise.all(entries.map((e) => installedMcpVersion(e, deps))),
|
|
85
|
+
]);
|
|
86
|
+
return entries.map((entry, i) => ({ entry, installed: installed[i] ?? null, latest }));
|
|
87
|
+
}
|
|
88
|
+
export function mcpRow(s, home = homedir()) {
|
|
89
|
+
const how = describeNonbotMcp(s.entry, home);
|
|
90
|
+
const v = s.installed ? `v${s.installed} · ` : '';
|
|
91
|
+
const behind = !!(s.installed && s.latest && compareVersions(s.latest, s.installed) > 0);
|
|
92
|
+
const e = s.entry;
|
|
93
|
+
if (e.kind === 'npx' && e.pinned) {
|
|
94
|
+
return {
|
|
95
|
+
sigil: '⚠',
|
|
96
|
+
detail: `${v}npx, pinned · gets no updates`,
|
|
97
|
+
hint: 'unpin it: claude mcp remove nonbot && claude mcp add nonbot -- npx -y @nonbot/mcp',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (!behind) {
|
|
101
|
+
if (e.kind === 'npx' && !s.installed) {
|
|
102
|
+
return { sigil: '✓', detail: `npx @nonbot/mcp · fetches ${s.latest ? `v${s.latest}` : 'the latest'} on first start` };
|
|
103
|
+
}
|
|
104
|
+
return { sigil: '✓', detail: `${v}${how}` };
|
|
105
|
+
}
|
|
106
|
+
const out = `v${s.installed} · ${shortKind(e.kind)} · v${s.latest} is out`;
|
|
107
|
+
switch (e.kind) {
|
|
108
|
+
case 'npx':
|
|
109
|
+
return { sigil: 'ℹ', detail: out, hint: 'restart Claude Code (or /mcp → Reconnect) and npx fetches it' };
|
|
110
|
+
case 'global':
|
|
111
|
+
return { sigil: '⚠', detail: out, hint: 'update it: nonbot update mcp' };
|
|
112
|
+
case 'checkout':
|
|
113
|
+
return {
|
|
114
|
+
sigil: 'ℹ',
|
|
115
|
+
detail: out,
|
|
116
|
+
hint: `update the checkout: cd ${e.checkoutDir ? tildePath(e.checkoutDir, home) : '<checkout>'} && git pull && npm run build`,
|
|
117
|
+
};
|
|
118
|
+
default:
|
|
119
|
+
return { sigil: 'ℹ', detail: out };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function shortKind(kind) {
|
|
123
|
+
return kind === 'npx' ? 'npx @nonbot/mcp' : kind === 'checkout' ? 'source checkout' : kind === 'global' ? 'global install' : kind;
|
|
124
|
+
}
|
|
125
|
+
function tildePath(p, home) {
|
|
126
|
+
return p.startsWith(home) ? '~' + p.slice(home.length) : p;
|
|
127
|
+
}
|
package/dist/lib/output.js
CHANGED
|
@@ -144,11 +144,40 @@ const WORDMARK_LINES = [
|
|
|
144
144
|
export const WORDMARK_WIDTH = 63;
|
|
145
145
|
export const WORDMARK_HEIGHT = 6;
|
|
146
146
|
export const WORDMARK_DOT_COLS = [31, 32, 33];
|
|
147
|
+
export const WORDMARK_GRADIENT_STOPS = [
|
|
148
|
+
[0x5e, 0xd8, 0xf0],
|
|
149
|
+
[0x6c, 0x8c, 0xff],
|
|
150
|
+
[0x9b, 0x6c, 0xf0],
|
|
151
|
+
[0xe0, 0x7a, 0xd8],
|
|
152
|
+
];
|
|
153
|
+
const WORDMARK_SHEEN = [0.24, 0.15, 0.07, 0, 0, 0];
|
|
154
|
+
const WORDMARK_SHADOW = 0.55;
|
|
155
|
+
function lerp(a, b, t) {
|
|
156
|
+
return a + (b - a) * t;
|
|
157
|
+
}
|
|
158
|
+
export function wordmarkCellColor(col, row, shadow = false) {
|
|
159
|
+
const span = WORDMARK_WIDTH - 1 + (WORDMARK_HEIGHT - 1) * 1.5;
|
|
160
|
+
const t = Math.min(1, Math.max(0, (col + row * 1.5) / span));
|
|
161
|
+
const stops = WORDMARK_GRADIENT_STOPS;
|
|
162
|
+
const seg = Math.min(stops.length - 2, Math.floor(t * (stops.length - 1)));
|
|
163
|
+
const local = t * (stops.length - 1) - seg;
|
|
164
|
+
const a = stops[seg];
|
|
165
|
+
const b = stops[seg + 1];
|
|
166
|
+
let rgb = [lerp(a[0], b[0], local), lerp(a[1], b[1], local), lerp(a[2], b[2], local)];
|
|
167
|
+
if (shadow) {
|
|
168
|
+
rgb = [rgb[0] * WORDMARK_SHADOW, rgb[1] * WORDMARK_SHADOW, rgb[2] * WORDMARK_SHADOW];
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
const sheen = WORDMARK_SHEEN[row] ?? 0;
|
|
172
|
+
rgb = [lerp(rgb[0], 255, sheen), lerp(rgb[1], 255, sheen), lerp(rgb[2], 255, sheen)];
|
|
173
|
+
}
|
|
174
|
+
return [Math.round(rgb[0]), Math.round(rgb[1]), Math.round(rgb[2])];
|
|
175
|
+
}
|
|
147
176
|
function hexFg(hex) {
|
|
148
177
|
const n = parseInt(hex.slice(1), 16);
|
|
149
178
|
return `\x1b[38;2;${(n >> 16) & 255};${(n >> 8) & 255};${n & 255}m`;
|
|
150
179
|
}
|
|
151
|
-
function paintWordmarkLine(line, stream = process.stdout) {
|
|
180
|
+
function paintWordmarkLine(line, row, stream = process.stdout) {
|
|
152
181
|
if (!isTTY(stream))
|
|
153
182
|
return line;
|
|
154
183
|
const chars = Array.from(line);
|
|
@@ -159,11 +188,16 @@ function paintWordmarkLine(line, stream = process.stdout) {
|
|
|
159
188
|
out += ' ';
|
|
160
189
|
continue;
|
|
161
190
|
}
|
|
162
|
-
const
|
|
163
|
-
out += BRAND.bold
|
|
191
|
+
const [r, g, b] = wordmarkCellColor(i, row, ch !== '█');
|
|
192
|
+
out += `${BRAND.bold}\x1b[38;2;${r};${g};${b}m${ch}${BRAND.reset}`;
|
|
164
193
|
}
|
|
165
194
|
return out;
|
|
166
195
|
}
|
|
196
|
+
export function wordmarkArt(opts = {}) {
|
|
197
|
+
const stream = opts.stream ?? process.stdout;
|
|
198
|
+
const indent = opts.indent ?? '';
|
|
199
|
+
return WORDMARK_LINES.map((line, row) => indent + paintWordmarkLine(line, row, stream)).join('\n');
|
|
200
|
+
}
|
|
167
201
|
export function terminalWidth(width) {
|
|
168
202
|
const cols = width ?? (process.stdout && process.stdout.columns) ?? 80;
|
|
169
203
|
const min = WORDMARK_WIDTH + 6;
|
|
@@ -180,7 +214,7 @@ function trackedCaps(s) {
|
|
|
180
214
|
export function wordmark(version, opts = {}) {
|
|
181
215
|
const stream = opts.stream ?? process.stdout;
|
|
182
216
|
const state = opts.state;
|
|
183
|
-
const headLines = WORDMARK_LINES.map((line) => paintWordmarkLine(line, stream));
|
|
217
|
+
const headLines = WORDMARK_LINES.map((line, row) => paintWordmarkLine(line, row, stream));
|
|
184
218
|
const parts = [c.cyan(trackedCaps('DAEMON'), stream)];
|
|
185
219
|
if (version) {
|
|
186
220
|
parts.push(c.muted('·', stream));
|
|
@@ -207,7 +241,7 @@ export function renderWordmarkWithBadge(opts = {}) {
|
|
|
207
241
|
red: (s) => c.red(s, stream),
|
|
208
242
|
muted: (s) => c.muted(s, stream),
|
|
209
243
|
};
|
|
210
|
-
const lines = WORDMARK_LINES.map((wm) => paintWordmarkLine(wm, stream));
|
|
244
|
+
const lines = WORDMARK_LINES.map((wm, row) => paintWordmarkLine(wm, row, stream));
|
|
211
245
|
lines.push('');
|
|
212
246
|
lines.push(` ${dotColored[color]('●')} ${dotColored[color](label)}`);
|
|
213
247
|
for (const s of sublines) {
|
|
@@ -360,7 +394,7 @@ export function compactMasthead(version, opts = {}) {
|
|
|
360
394
|
const badgeColor = opts.badgeColor ?? 'green';
|
|
361
395
|
const badgeLabel = opts.badgeLabel ?? 'CONNECTED';
|
|
362
396
|
const dotGlyph = asciiOnly() ? '*' : '●';
|
|
363
|
-
const brand =
|
|
397
|
+
const brand = c.bold(c.cream('non.bot', stream), stream);
|
|
364
398
|
const badge = colorize(`${dotGlyph} ${badgeLabel}`, badgeColor, stream);
|
|
365
399
|
const sep = c.muted('·', stream);
|
|
366
400
|
const tail = [c.muted(`daemon ${version}`, stream), sep, c.muted(state, stream)].join(' ');
|
package/dist/lib/update-check.js
CHANGED
|
@@ -35,7 +35,7 @@ export function isNewerVersion(latest, current) {
|
|
|
35
35
|
return compareVersions(latest, current) > 0;
|
|
36
36
|
}
|
|
37
37
|
export function updateAvailableLine(current, latest) {
|
|
38
|
-
return `Update available: @nonbot/cli ${current} → ${latest} (
|
|
38
|
+
return `Update available: @nonbot/cli ${current} → ${latest} (run: nonbot update)`;
|
|
39
39
|
}
|
|
40
40
|
export async function fetchLatestVersion(opts = {}) {
|
|
41
41
|
const fetchImpl = opts.fetchImpl ?? fetch;
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.10.
|
|
1
|
+
export const VERSION = '0.10.15';
|
package/package.json
CHANGED