@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
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { loadAuth } from '../lib/auth.js';
|
|
6
|
+
import { resolveTerminal } from '../lib/terminal.js';
|
|
7
|
+
import { VERSION } from '../version.js';
|
|
8
|
+
import { header, statusRow, errorBlock } from '../lib/output.js';
|
|
9
|
+
const HEARTBEAT_FRESH_MS = 15_000;
|
|
10
|
+
const PROVIDER_CLIS = ['claude', 'codex', 'gemini'];
|
|
11
|
+
const EXPECTED_MCPS = ['portfolio-mcp', 'jobshop-mcp', 'context-graph'];
|
|
12
|
+
const EXPECTED_SKILLS = [];
|
|
13
|
+
function readConfiguredMcpNames() {
|
|
14
|
+
const out = new Set();
|
|
15
|
+
try {
|
|
16
|
+
const path = join(homedir(), '.claude.json');
|
|
17
|
+
if (!existsSync(path))
|
|
18
|
+
return out;
|
|
19
|
+
const json = JSON.parse(readFileSync(path, 'utf-8'));
|
|
20
|
+
const projects = json?.projects && typeof json.projects === 'object' ? json.projects : null;
|
|
21
|
+
if (projects) {
|
|
22
|
+
for (const proj of Object.values(projects)) {
|
|
23
|
+
const servers = proj?.mcpServers;
|
|
24
|
+
if (servers && typeof servers === 'object') {
|
|
25
|
+
for (const name of Object.keys(servers))
|
|
26
|
+
out.add(name);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const top = json?.mcpServers;
|
|
31
|
+
if (top && typeof top === 'object') {
|
|
32
|
+
for (const name of Object.keys(top))
|
|
33
|
+
out.add(name);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
function isSkillInstalled(skillName) {
|
|
41
|
+
const base = join(homedir(), '.claude');
|
|
42
|
+
const candidates = [];
|
|
43
|
+
const skillsDir = join(base, 'skills');
|
|
44
|
+
if (existsSync(skillsDir))
|
|
45
|
+
candidates.push(skillsDir);
|
|
46
|
+
const pluginsDir = join(base, 'plugins');
|
|
47
|
+
if (existsSync(pluginsDir)) {
|
|
48
|
+
try {
|
|
49
|
+
for (const entry of readdirSync(pluginsDir)) {
|
|
50
|
+
const inner = join(pluginsDir, entry, 'skills');
|
|
51
|
+
if (existsSync(inner))
|
|
52
|
+
candidates.push(inner);
|
|
53
|
+
try {
|
|
54
|
+
const sub = join(pluginsDir, entry);
|
|
55
|
+
if (statSync(sub).isDirectory()) {
|
|
56
|
+
for (const child of readdirSync(sub)) {
|
|
57
|
+
const inner2 = join(sub, child, 'skills');
|
|
58
|
+
if (existsSync(inner2))
|
|
59
|
+
candidates.push(inner2);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
for (const dir of candidates) {
|
|
71
|
+
try {
|
|
72
|
+
for (const entry of readdirSync(dir)) {
|
|
73
|
+
if (entry === skillName)
|
|
74
|
+
return true;
|
|
75
|
+
const skillMd = join(dir, entry, 'SKILL.md');
|
|
76
|
+
if (existsSync(skillMd)) {
|
|
77
|
+
try {
|
|
78
|
+
const head = readFileSync(skillMd, 'utf-8').slice(0, 1024);
|
|
79
|
+
const m = head.match(/^name:\s*([^\s\n]+)/m);
|
|
80
|
+
if (m && m[1] === skillName)
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
function probe(cmd, args, interpret = (out) => ({
|
|
94
|
+
installed: true,
|
|
95
|
+
version: out || undefined,
|
|
96
|
+
})) {
|
|
97
|
+
try {
|
|
98
|
+
const res = spawnSync(cmd, args, { encoding: 'utf-8' });
|
|
99
|
+
if (res.error)
|
|
100
|
+
return { installed: false };
|
|
101
|
+
if (res.status === 0)
|
|
102
|
+
return interpret((res.stdout ?? '').trim());
|
|
103
|
+
const stderr = (res.stderr ?? '').trim();
|
|
104
|
+
return { installed: false, version: stderr ? stderr.slice(0, 200) : undefined };
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
return { installed: false, version: e.message };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function defaultCheckBinary(bin) {
|
|
111
|
+
if (bin.startsWith('mcp:')) {
|
|
112
|
+
const name = bin.slice('mcp:'.length);
|
|
113
|
+
return { installed: readConfiguredMcpNames().has(name) };
|
|
114
|
+
}
|
|
115
|
+
if (bin.startsWith('skill:')) {
|
|
116
|
+
const name = bin.slice('skill:'.length);
|
|
117
|
+
return { installed: isSkillInstalled(name) };
|
|
118
|
+
}
|
|
119
|
+
if (bin === 'iterm-running') {
|
|
120
|
+
return probe('osascript', ['-e', 'tell application "System Events" to ((name of every process) contains "iTerm2")'], (out) => ({ installed: out === 'true' }));
|
|
121
|
+
}
|
|
122
|
+
if (bin === 'iterm-applescript') {
|
|
123
|
+
return probe('osascript', ['-e', 'tell application "iTerm" to get version']);
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
const res = spawnSync(bin, ['--version'], { encoding: 'utf-8' });
|
|
127
|
+
if (res.error)
|
|
128
|
+
return { installed: false };
|
|
129
|
+
if (res.status === 0)
|
|
130
|
+
return { installed: true, version: (res.stdout ?? '').trim() || undefined };
|
|
131
|
+
return { installed: true };
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return { installed: false };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
export async function runDoctorCommand(args = [], deps = {}) {
|
|
138
|
+
void args;
|
|
139
|
+
const log = deps.log ?? ((s) => process.stdout.write(s));
|
|
140
|
+
const errLog = deps.errLog ?? ((s) => process.stderr.write(s));
|
|
141
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
142
|
+
const loader = deps.loadAuth ?? loadAuth;
|
|
143
|
+
const now = deps.now ?? (() => Date.now());
|
|
144
|
+
const checkBinary = deps.checkBinary ?? defaultCheckBinary;
|
|
145
|
+
let exitCode = 0;
|
|
146
|
+
log(header('non.bot doctor', `v${VERSION} · ${process.platform}/${process.arch}`) + '\n');
|
|
147
|
+
log('\n');
|
|
148
|
+
const auth = await loader();
|
|
149
|
+
if (!auth) {
|
|
150
|
+
log(errorBlock('Not logged in', 'Run `nonbot login` to authenticate.'));
|
|
151
|
+
return 1;
|
|
152
|
+
}
|
|
153
|
+
log(statusRow('✓', 'Auth', `logged in as ${auth.email}`) + '\n');
|
|
154
|
+
let serverOk = false;
|
|
155
|
+
try {
|
|
156
|
+
const res = await fetchImpl(`${auth.baseUrl}/api/auth/me`, {
|
|
157
|
+
headers: {
|
|
158
|
+
Authorization: `Bearer ${auth.pat}`,
|
|
159
|
+
'X-Requested-With': 'ConradPM-Native',
|
|
160
|
+
'X-CLI-Version': VERSION,
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
if (res.ok) {
|
|
164
|
+
serverOk = true;
|
|
165
|
+
log(statusRow('✓', 'Server reachable', `${auth.baseUrl} · PAT valid`) + '\n');
|
|
166
|
+
}
|
|
167
|
+
else if (res.status === 401) {
|
|
168
|
+
errLog(statusRow('✗', 'Server', `PAT rejected (HTTP 401) — re-run: nonbot login`, { stream: process.stderr }) + '\n');
|
|
169
|
+
exitCode = 1;
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
log(statusRow('⚠', 'Server', `returned HTTP ${res.status} for /api/auth/me`) + '\n');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
log(statusRow('⚠', 'Server', `Could not reach ${auth.baseUrl}: ${e.message}`) + '\n');
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
const res = await fetchImpl(`${auth.baseUrl}/api/cli/daemon-status`, {
|
|
180
|
+
headers: {
|
|
181
|
+
Authorization: `Bearer ${auth.pat}`,
|
|
182
|
+
'X-Requested-With': 'ConradPM-Native',
|
|
183
|
+
'X-CLI-Version': VERSION,
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
if (res.ok) {
|
|
187
|
+
const body = (await res.json());
|
|
188
|
+
const last = body.lastHeartbeatAt;
|
|
189
|
+
if (typeof last === 'number' && now() - last <= HEARTBEAT_FRESH_MS) {
|
|
190
|
+
const ageSec = Math.round((now() - last) / 1000);
|
|
191
|
+
log(statusRow('✓', 'Daemon running', `last heartbeat ${ageSec}s ago`) + '\n');
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
log(statusRow('✗', 'Daemon not running', 'no recent heartbeat — start with: nonbot daemon') + '\n');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
else if (res.status === 401) {
|
|
198
|
+
log(statusRow('⚠', 'Daemon status unknown', '(auth rejected)') + '\n');
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
log(statusRow('⚠', 'Daemon status unknown', `(HTTP ${res.status})`) + '\n');
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch (e) {
|
|
205
|
+
log(statusRow('⚠', 'Daemon status unknown', e.message) + '\n');
|
|
206
|
+
}
|
|
207
|
+
for (const bin of PROVIDER_CLIS) {
|
|
208
|
+
const r = checkBinary(bin);
|
|
209
|
+
if (r.installed) {
|
|
210
|
+
log(statusRow('✓', bin, r.version ? `(${r.version})` : '') + '\n');
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
log(statusRow('⚠', bin, 'not on PATH') + '\n');
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const profile = resolveTerminal(undefined);
|
|
217
|
+
const { cmd } = profile.launch('/tmp/nonbot-doctor-probe.sh');
|
|
218
|
+
const launcher = checkBinary(cmd);
|
|
219
|
+
if (launcher.installed) {
|
|
220
|
+
log(statusRow('✓', 'Terminal', `${profile.displayName} (launches via "${cmd}")`) + '\n');
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
log(statusRow('⚠', 'Terminal', `${profile.displayName} — launcher "${cmd}" not found on PATH`) + '\n');
|
|
224
|
+
}
|
|
225
|
+
if (process.platform === 'darwin') {
|
|
226
|
+
const running = checkBinary('iterm-running');
|
|
227
|
+
if (running.installed) {
|
|
228
|
+
log(statusRow('✓', 'iTerm running', '') + '\n');
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
log(statusRow('⚠', 'iTerm not running', 'to start iTerm: open -a iTerm') + '\n');
|
|
232
|
+
}
|
|
233
|
+
const appl = checkBinary('iterm-applescript');
|
|
234
|
+
if (appl.installed) {
|
|
235
|
+
log(statusRow('✓', 'iTerm AppleScript reachable', appl.version ?? '') + '\n');
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
log(statusRow('⚠', 'iTerm AppleScript', appl.version ? `probe failed: ${appl.version}` : 'probe failed') + '\n');
|
|
239
|
+
log(' ' + 'System Settings → Privacy & Security → Automation → allow your terminal to control iTerm.' + '\n');
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
for (const name of EXPECTED_MCPS) {
|
|
243
|
+
const r = checkBinary(`mcp:${name}`);
|
|
244
|
+
if (r.installed) {
|
|
245
|
+
log(statusRow('✓', `MCP ${name}`, '') + '\n');
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
log(statusRow('⚠', `MCP ${name}`, 'not configured in ~/.claude.json') + '\n');
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
for (const name of EXPECTED_SKILLS) {
|
|
252
|
+
const r = checkBinary(`skill:${name}`);
|
|
253
|
+
if (r.installed) {
|
|
254
|
+
log(statusRow('✓', `Skill ${name}`, '') + '\n');
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
log(statusRow('⚠', `Skill ${name}`, 'not installed under ~/.claude/skills/') + '\n');
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
void serverOk;
|
|
261
|
+
return exitCode;
|
|
262
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import readline from 'node:readline';
|
|
2
|
+
import { saveAuth } from '../lib/auth.js';
|
|
3
|
+
import { VERSION } from '../version.js';
|
|
4
|
+
import { c, statusRow, wordmark } from '../lib/output.js';
|
|
5
|
+
const DEFAULT_BASE_URL = 'https://non.bot';
|
|
6
|
+
export function parseLoginArgs(args) {
|
|
7
|
+
const opts = {};
|
|
8
|
+
for (let i = 0; i < args.length; i++) {
|
|
9
|
+
const a = args[i];
|
|
10
|
+
if (a === '--token' && i + 1 < args.length) {
|
|
11
|
+
opts.token = args[++i];
|
|
12
|
+
}
|
|
13
|
+
else if (a === '--base-url' && i + 1 < args.length) {
|
|
14
|
+
opts.baseUrl = args[++i];
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return opts;
|
|
18
|
+
}
|
|
19
|
+
async function promptForPat() {
|
|
20
|
+
process.stdout.write('First, generate a Personal Access Token at:\n');
|
|
21
|
+
process.stdout.write(' https://non.bot/settings/connections#api-tokens\n\n');
|
|
22
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
23
|
+
try {
|
|
24
|
+
const rlAny = rl;
|
|
25
|
+
const originalWrite = rlAny._writeToOutput?.bind(rl);
|
|
26
|
+
if (originalWrite) {
|
|
27
|
+
let inPrompt = true;
|
|
28
|
+
rlAny._writeToOutput = (s) => {
|
|
29
|
+
if (inPrompt && s.includes('Paste your PAT')) {
|
|
30
|
+
inPrompt = false;
|
|
31
|
+
originalWrite(s);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (s === '\r\n' || s === '\n') {
|
|
35
|
+
originalWrite(s);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
originalWrite('*'.repeat(s.length));
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return await new Promise((resolve) => {
|
|
42
|
+
rl.question("Paste your PAT (starts with 'pat_'): ", (answer) => {
|
|
43
|
+
resolve(answer.trim());
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
rl.close();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export async function runLoginCommand(args = [], deps = {}) {
|
|
52
|
+
const opts = parseLoginArgs(args);
|
|
53
|
+
const baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
|
|
54
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
55
|
+
const log = deps.log ?? ((s) => process.stdout.write(s));
|
|
56
|
+
const errLog = deps.errLog ?? ((s) => process.stderr.write(s));
|
|
57
|
+
let pat = opts.token?.trim() ?? '';
|
|
58
|
+
if (!pat) {
|
|
59
|
+
const reader = deps.readPat ?? promptForPat;
|
|
60
|
+
pat = (await reader()).trim();
|
|
61
|
+
}
|
|
62
|
+
if (!pat) {
|
|
63
|
+
errLog(statusRow('✗', 'No PAT provided', '', { stream: process.stderr }) + '\n');
|
|
64
|
+
return 1;
|
|
65
|
+
}
|
|
66
|
+
if (!pat.startsWith('pat_')) {
|
|
67
|
+
errLog(statusRow('✗', "Token doesn't look like a PAT", "should start with 'pat_'", { stream: process.stderr }) + '\n');
|
|
68
|
+
return 1;
|
|
69
|
+
}
|
|
70
|
+
let res;
|
|
71
|
+
try {
|
|
72
|
+
res = await fetchImpl(`${baseUrl}/api/auth/me`, {
|
|
73
|
+
headers: {
|
|
74
|
+
Authorization: `Bearer ${pat}`,
|
|
75
|
+
'X-Requested-With': 'ConradPM-Native',
|
|
76
|
+
'X-CLI-Version': VERSION,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
errLog(statusRow('✗', 'Network error', `${baseUrl}: ${e.message}`, { stream: process.stderr }) + '\n');
|
|
82
|
+
return 1;
|
|
83
|
+
}
|
|
84
|
+
if (!res.ok) {
|
|
85
|
+
errLog(statusRow('✗', `Login failed (HTTP ${res.status})`, 'Check the PAT and try again', { stream: process.stderr }) + '\n');
|
|
86
|
+
return 1;
|
|
87
|
+
}
|
|
88
|
+
let body;
|
|
89
|
+
try {
|
|
90
|
+
body = (await res.json());
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
errLog(statusRow('✗', 'Login failed', 'server returned non-JSON', { stream: process.stderr }) + '\n');
|
|
94
|
+
return 1;
|
|
95
|
+
}
|
|
96
|
+
if (!body.authenticated || !body.user?.email || !body.user?.id) {
|
|
97
|
+
errLog(statusRow('✗', 'Login failed', 'server says token is not valid', { stream: process.stderr }) + '\n');
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
100
|
+
const auth = {
|
|
101
|
+
pat,
|
|
102
|
+
baseUrl,
|
|
103
|
+
email: body.user.email,
|
|
104
|
+
userId: body.user.id,
|
|
105
|
+
savedAt: Date.now(),
|
|
106
|
+
};
|
|
107
|
+
await saveAuth(auth);
|
|
108
|
+
log('\n' + wordmark(`v${VERSION}`, { state: 'READY' }) + '\n\n');
|
|
109
|
+
log(statusRow('✓', 'Logged in', `as ${c.cyan(auth.email)}`) + '\n');
|
|
110
|
+
log(statusRow('✓', 'Saved auth', '~/.config/nonbot/auth.json (mode 600)') + '\n');
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { readActivityLog as readActivityLogDefault, } from '../lib/activity-log.js';
|
|
2
|
+
import { header, statusRow, eventRow } from '../lib/output.js';
|
|
3
|
+
const FOLLOW_INTERVAL_MS = 1000;
|
|
4
|
+
function parseArgs(args) {
|
|
5
|
+
let limit = 20;
|
|
6
|
+
let json = false;
|
|
7
|
+
let follow = false;
|
|
8
|
+
for (let i = 0; i < args.length; i++) {
|
|
9
|
+
const a = args[i];
|
|
10
|
+
if (a === '--json') {
|
|
11
|
+
json = true;
|
|
12
|
+
}
|
|
13
|
+
else if (a === '--follow' || a === '-f') {
|
|
14
|
+
follow = true;
|
|
15
|
+
}
|
|
16
|
+
else if (a === '--limit') {
|
|
17
|
+
const v = args[i + 1];
|
|
18
|
+
const n = Number(v);
|
|
19
|
+
if (!v || !Number.isFinite(n) || n <= 0) {
|
|
20
|
+
return { limit, json, follow, error: '--limit needs a positive number' };
|
|
21
|
+
}
|
|
22
|
+
limit = Math.floor(n);
|
|
23
|
+
i++;
|
|
24
|
+
}
|
|
25
|
+
else if (a.startsWith('--limit=')) {
|
|
26
|
+
const n = Number(a.slice('--limit='.length));
|
|
27
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
28
|
+
return { limit, json, follow, error: '--limit needs a positive number' };
|
|
29
|
+
}
|
|
30
|
+
limit = Math.floor(n);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
return { limit, json, follow, error: `unknown flag "${a}"` };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { limit, json, follow };
|
|
37
|
+
}
|
|
38
|
+
function relativeTime(ts, now) {
|
|
39
|
+
const deltaSec = Math.max(0, Math.round((now - ts) / 1000));
|
|
40
|
+
if (deltaSec < 60)
|
|
41
|
+
return `${deltaSec}s ago`;
|
|
42
|
+
const min = Math.round(deltaSec / 60);
|
|
43
|
+
if (min < 60)
|
|
44
|
+
return `${min}m ago`;
|
|
45
|
+
const hr = Math.round(min / 60);
|
|
46
|
+
if (hr < 24)
|
|
47
|
+
return `${hr}h ago`;
|
|
48
|
+
const days = Math.round(hr / 24);
|
|
49
|
+
return `${days}d ago`;
|
|
50
|
+
}
|
|
51
|
+
function formatEntry(e, now) {
|
|
52
|
+
const status = e.status === 'launched' ? '✓' : '✗';
|
|
53
|
+
const when = relativeTime(e.ts, now);
|
|
54
|
+
const target = `${e.mode}/${e.target}`;
|
|
55
|
+
const parts = [e.id, e.kind, target, when];
|
|
56
|
+
if (e.repoPath)
|
|
57
|
+
parts.push(e.repoPath);
|
|
58
|
+
if (e.status === 'failed' && e.reason)
|
|
59
|
+
parts.push(`reason: ${e.reason}`);
|
|
60
|
+
return eventRow(parts, { status }) + '\n';
|
|
61
|
+
}
|
|
62
|
+
export async function runLogsCommand(args = [], deps = {}) {
|
|
63
|
+
const log = deps.log ?? ((s) => process.stdout.write(s));
|
|
64
|
+
const errLog = deps.errLog ?? ((s) => process.stderr.write(s));
|
|
65
|
+
const readActivityLog = deps.readActivityLog ?? readActivityLogDefault;
|
|
66
|
+
const now = deps.now ?? (() => Date.now());
|
|
67
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
68
|
+
const opts = parseArgs(args);
|
|
69
|
+
if (opts.error) {
|
|
70
|
+
errLog(statusRow('✗', 'nonbot logs', opts.error, { stream: process.stderr }) + '\n');
|
|
71
|
+
errLog(' Usage: nonbot logs [--limit <n>] [--json] [--follow|-f]\n');
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
const entries = await readActivityLog(opts.limit);
|
|
75
|
+
if (opts.json) {
|
|
76
|
+
for (const e of entries)
|
|
77
|
+
log(JSON.stringify(e) + '\n');
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
log(header('nonbot logs', `last ${entries.length} ${entries.length === 1 ? 'entry' : 'entries'}`) + '\n');
|
|
81
|
+
if (entries.length === 0) {
|
|
82
|
+
log(statusRow('ℹ', 'No activation history yet', '') + '\n');
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
for (const e of entries)
|
|
86
|
+
log(formatEntry(e, now()));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (!opts.follow)
|
|
90
|
+
return 0;
|
|
91
|
+
let highWater = entries.length > 0 ? entries[entries.length - 1].ts : 0;
|
|
92
|
+
let running = true;
|
|
93
|
+
const sigHandler = () => {
|
|
94
|
+
running = false;
|
|
95
|
+
log('\n' + statusRow('✓', 'stopped following', 'Ctrl-C received') + '\n');
|
|
96
|
+
};
|
|
97
|
+
process.on('SIGINT', sigHandler);
|
|
98
|
+
log(statusRow('ℹ', 'Following activation log', 'Ctrl-C to stop') + '\n');
|
|
99
|
+
while (running) {
|
|
100
|
+
await sleep(FOLLOW_INTERVAL_MS);
|
|
101
|
+
if (!running)
|
|
102
|
+
break;
|
|
103
|
+
const recent = await readActivityLog(200);
|
|
104
|
+
for (const e of recent) {
|
|
105
|
+
if (e.ts > highWater) {
|
|
106
|
+
log(formatEntry(e, now()));
|
|
107
|
+
highWater = e.ts;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
process.off('SIGINT', sigHandler);
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { listProfiles as listProfilesDefault, getActiveProfile } from '../lib/auth.js';
|
|
2
|
+
export async function runProfilesCommand(args = [], deps = {}) {
|
|
3
|
+
void args;
|
|
4
|
+
const log = deps.log ?? ((s) => process.stdout.write(s));
|
|
5
|
+
const errLog = deps.errLog ?? ((s) => process.stderr.write(s));
|
|
6
|
+
const listProfiles = deps.listProfiles ?? listProfilesDefault;
|
|
7
|
+
const active = getActiveProfile();
|
|
8
|
+
const profiles = await listProfiles();
|
|
9
|
+
if (profiles.length === 0) {
|
|
10
|
+
log('ℹ No profiles found. Run: nonbot login\n');
|
|
11
|
+
log(`ℹ Active profile: ${active} (no auth saved yet)\n`);
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
log('Profiles:\n');
|
|
15
|
+
for (const name of profiles) {
|
|
16
|
+
const marker = name === active ? '●' : ' ';
|
|
17
|
+
log(` ${marker} ${name}\n`);
|
|
18
|
+
}
|
|
19
|
+
if (!profiles.includes(active)) {
|
|
20
|
+
log(`\nℹ Active profile "${active}" has no auth saved. Run: nonbot login --profile ${active}\n`);
|
|
21
|
+
}
|
|
22
|
+
void errLog;
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { loadAuth } from '../lib/auth.js';
|
|
2
|
+
import { fireActivation, makeHeadlessSpawner, } from '../lib/activations.js';
|
|
3
|
+
import { VERSION } from '../version.js';
|
|
4
|
+
export async function runRunCommand(args = [], deps = {}) {
|
|
5
|
+
const log = deps.log ?? ((s) => process.stdout.write(s));
|
|
6
|
+
const errLog = deps.errLog ?? ((s) => process.stderr.write(s));
|
|
7
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
8
|
+
const loader = deps.loadAuth ?? loadAuth;
|
|
9
|
+
const headless = args.includes('--headless') || args.includes('--no-terminal');
|
|
10
|
+
if (headless) {
|
|
11
|
+
deps.headless = true;
|
|
12
|
+
if (!deps.spawnTerminal) {
|
|
13
|
+
deps.spawnTerminal = makeHeadlessSpawner({ wait: true, log, errLog });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const id = args.find((a) => !a.startsWith('-'));
|
|
17
|
+
if (!id) {
|
|
18
|
+
errLog('Usage: nonbot run <activation-id> [--headless]\n');
|
|
19
|
+
return 1;
|
|
20
|
+
}
|
|
21
|
+
const auth = await loader();
|
|
22
|
+
if (!auth) {
|
|
23
|
+
errLog('✗ Not logged in. Run: nonbot login\n');
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
26
|
+
let res;
|
|
27
|
+
try {
|
|
28
|
+
res = await fetchImpl(`${auth.baseUrl}/api/cli/activations/pending`, {
|
|
29
|
+
headers: {
|
|
30
|
+
Authorization: `Bearer ${auth.pat}`,
|
|
31
|
+
'X-Requested-With': 'ConradPM-Native',
|
|
32
|
+
'X-CLI-Version': VERSION,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
errLog(`✗ Network error: ${e.message}\n`);
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
if (res.status === 401) {
|
|
41
|
+
errLog('✗ Auth failed (401). Re-run: nonbot login\n');
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
44
|
+
if (!res.ok) {
|
|
45
|
+
errLog(`✗ Server returned HTTP ${res.status}.\n`);
|
|
46
|
+
return 1;
|
|
47
|
+
}
|
|
48
|
+
let body;
|
|
49
|
+
try {
|
|
50
|
+
body = (await res.json());
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
errLog('✗ Server returned non-JSON.\n');
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
const match = (body.activations ?? []).find((a) => a?.id === id);
|
|
57
|
+
if (!match) {
|
|
58
|
+
errLog(`✗ No pending activation with id ${id}. It may have already been delivered.\n`);
|
|
59
|
+
return 1;
|
|
60
|
+
}
|
|
61
|
+
await fireActivation(auth, match, deps, log, errLog);
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { loadAuth } from '../lib/auth.js';
|
|
2
|
+
import { readActivityLog as readActivityLogDefault } from '../lib/activity-log.js';
|
|
3
|
+
import { VERSION } from '../version.js';
|
|
4
|
+
import { header, formatKV, errorBlock, statusRow, clockTime } from '../lib/output.js';
|
|
5
|
+
const HEARTBEAT_FRESH_MS = 15_000;
|
|
6
|
+
function relativeTime(deltaMs) {
|
|
7
|
+
const deltaSec = Math.max(0, Math.round(deltaMs / 1000));
|
|
8
|
+
if (deltaSec < 60)
|
|
9
|
+
return `${deltaSec}s ago`;
|
|
10
|
+
const min = Math.round(deltaSec / 60);
|
|
11
|
+
if (min < 60)
|
|
12
|
+
return `${min}m ago`;
|
|
13
|
+
const hr = Math.round(min / 60);
|
|
14
|
+
if (hr < 24)
|
|
15
|
+
return `${hr}h ago`;
|
|
16
|
+
return `${Math.round(hr / 24)}d ago`;
|
|
17
|
+
}
|
|
18
|
+
export async function runStatusCommand(args = [], deps = {}) {
|
|
19
|
+
void args;
|
|
20
|
+
const log = deps.log ?? ((s) => process.stdout.write(s));
|
|
21
|
+
const errLog = deps.errLog ?? ((s) => process.stderr.write(s));
|
|
22
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
23
|
+
const loader = deps.loadAuth ?? loadAuth;
|
|
24
|
+
const now = deps.now ?? (() => Date.now());
|
|
25
|
+
const readActivityLog = deps.readActivityLog ?? readActivityLogDefault;
|
|
26
|
+
const auth = await loader();
|
|
27
|
+
if (!auth) {
|
|
28
|
+
log(errorBlock('Not logged in', 'Run `nonbot login` to authenticate.', { stream: process.stdout }));
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
let lastHeartbeat;
|
|
32
|
+
let serverError;
|
|
33
|
+
try {
|
|
34
|
+
const res = await fetchImpl(`${auth.baseUrl}/api/cli/daemon-status`, {
|
|
35
|
+
headers: {
|
|
36
|
+
Authorization: `Bearer ${auth.pat}`,
|
|
37
|
+
'X-Requested-With': 'ConradPM-Native',
|
|
38
|
+
'X-CLI-Version': VERSION,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
if (res.ok) {
|
|
42
|
+
const body = (await res.json());
|
|
43
|
+
if (typeof body.lastHeartbeatAt === 'number') {
|
|
44
|
+
lastHeartbeat = body.lastHeartbeatAt;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else if (res.status === 401) {
|
|
48
|
+
errLog(errorBlock('Auth failed (HTTP 401)', 'Re-run: nonbot login', { stream: process.stderr }));
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
serverError = `HTTP ${res.status}`;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
serverError = e.message;
|
|
57
|
+
}
|
|
58
|
+
const isFresh = typeof lastHeartbeat === 'number' && now() - lastHeartbeat <= HEARTBEAT_FRESH_MS;
|
|
59
|
+
const hasStale = typeof lastHeartbeat === 'number' && !isFresh;
|
|
60
|
+
const leadStatus = isFresh ? '●' : hasStale ? '⚠' : '✗';
|
|
61
|
+
log(header('non.bot daemon', `v${VERSION}`, { status: leadStatus }) + '\n');
|
|
62
|
+
const hostUrl = new URL(auth.baseUrl).host;
|
|
63
|
+
const rows = [
|
|
64
|
+
['Connection', `${hostUrl} (${auth.baseUrl})`],
|
|
65
|
+
['Account', auth.email],
|
|
66
|
+
];
|
|
67
|
+
if (typeof lastHeartbeat === 'number') {
|
|
68
|
+
rows.push(['Last poll', relativeTime(now() - lastHeartbeat)]);
|
|
69
|
+
}
|
|
70
|
+
else if (serverError) {
|
|
71
|
+
rows.push(['Last poll', `unknown (server: ${serverError})`]);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
rows.push(['Last poll', 'no heartbeat recorded yet']);
|
|
75
|
+
}
|
|
76
|
+
let lastFire;
|
|
77
|
+
try {
|
|
78
|
+
const recent = await readActivityLog(1);
|
|
79
|
+
if (recent.length > 0)
|
|
80
|
+
lastFire = recent[recent.length - 1];
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
}
|
|
84
|
+
if (lastFire) {
|
|
85
|
+
const ts = clockTime(new Date(lastFire.ts));
|
|
86
|
+
const outcome = lastFire.status === 'launched' ? 'launched' : 'failed';
|
|
87
|
+
rows.push([
|
|
88
|
+
'Last fire',
|
|
89
|
+
`${lastFire.id} (${ts}, ${outcome} in ${lastFire.target})`,
|
|
90
|
+
]);
|
|
91
|
+
}
|
|
92
|
+
for (const r of formatKV(rows))
|
|
93
|
+
log(r + '\n');
|
|
94
|
+
if (!isFresh && !hasStale) {
|
|
95
|
+
log('\n');
|
|
96
|
+
log(statusRow('ℹ', 'Start it', 'nonbot daemon') + '\n');
|
|
97
|
+
}
|
|
98
|
+
else if (hasStale && lastHeartbeat) {
|
|
99
|
+
log('\n');
|
|
100
|
+
log(statusRow('⚠', 'Heartbeat stale', `${relativeTime(now() - lastHeartbeat)} — restart with: nonbot daemon`) + '\n');
|
|
101
|
+
}
|
|
102
|
+
return 0;
|
|
103
|
+
}
|