thalixtower-cli 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -0
- package/bin/atc.js +3 -0
- package/dist/client.js +125 -0
- package/dist/hook.js +440 -0
- package/dist/human.js +86 -0
- package/dist/index.js +736 -0
- package/dist/init.js +456 -0
- package/package.json +45 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
/**
|
|
37
|
+
* atc — the Thalix Tower CLI. Thin client of the hosted /v1 API.
|
|
38
|
+
* Config: ATC_API + ATC_TOKEN (frequency token from the dashboard). See
|
|
39
|
+
* docs/AGENT-INTERFACE.md. Exit codes: 0 ok · 1 usage/error · 2 unexpected · 3 conflict.
|
|
40
|
+
*/
|
|
41
|
+
const fs = __importStar(require("node:fs"));
|
|
42
|
+
const child_process = __importStar(require("node:child_process"));
|
|
43
|
+
const client_1 = require("./client");
|
|
44
|
+
const init_1 = require("./init");
|
|
45
|
+
const hook_1 = require("./hook");
|
|
46
|
+
const human_1 = require("./human");
|
|
47
|
+
const VERSION = '0.6.0';
|
|
48
|
+
const COMMANDS = [
|
|
49
|
+
'init', 'checkin', 'brief', 'standing', 'squawk', 'claim', 'clear',
|
|
50
|
+
'note', 'notes', 'checkout', 'config',
|
|
51
|
+
'call', 'traffic',
|
|
52
|
+
'login', 'logout', 'whoami', 'projects', 'tokens',
|
|
53
|
+
'hook', 'audit',
|
|
54
|
+
];
|
|
55
|
+
function parseArgs(argv) {
|
|
56
|
+
const positionals = [];
|
|
57
|
+
const flags = {};
|
|
58
|
+
const tags = [];
|
|
59
|
+
for (let i = 0; i < argv.length; i++) {
|
|
60
|
+
const a = argv[i];
|
|
61
|
+
if (a.startsWith('--')) {
|
|
62
|
+
const key = a.slice(2);
|
|
63
|
+
const next = argv[i + 1];
|
|
64
|
+
const boolFlags = ['pin', 'json', 'yes', 'manual', 'draft', 'activate', 'default', 'none', 'save-token', 'all', 'ack-all'];
|
|
65
|
+
if (boolFlags.includes(key)) {
|
|
66
|
+
flags[key] = true;
|
|
67
|
+
}
|
|
68
|
+
else if (key === 'tag') {
|
|
69
|
+
if (next) {
|
|
70
|
+
tags.push(next);
|
|
71
|
+
i++;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else if (next && !next.startsWith('--')) {
|
|
75
|
+
flags[key] = next;
|
|
76
|
+
i++;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
flags[key] = true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
positionals.push(a);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return { positionals, flags, tags };
|
|
87
|
+
}
|
|
88
|
+
function out(json, human, data) {
|
|
89
|
+
process.stdout.write(json ? JSON.stringify(data, null, 2) + '\n' : human + '\n');
|
|
90
|
+
}
|
|
91
|
+
function fail(msg, code = 1) {
|
|
92
|
+
process.stderr.write(`atc: ${msg}\n`);
|
|
93
|
+
process.exit(code);
|
|
94
|
+
}
|
|
95
|
+
function requireSession() {
|
|
96
|
+
const s = (0, client_1.loadSession)();
|
|
97
|
+
if (!s)
|
|
98
|
+
fail("not checked in — run 'atc checkin' first");
|
|
99
|
+
return s;
|
|
100
|
+
}
|
|
101
|
+
function renderConflicts(conflicts) {
|
|
102
|
+
if (!conflicts || conflicts.length === 0)
|
|
103
|
+
return '✓ no conflicts';
|
|
104
|
+
return (`⚠ CONFLICT (${conflicts.length}):\n` +
|
|
105
|
+
conflicts
|
|
106
|
+
.map((c) => ` ${c.by} holds ${c.theirGlob} (${c.status}, seen ${c.lastSeen}) vs your ${c.glob}`)
|
|
107
|
+
.join('\n'));
|
|
108
|
+
}
|
|
109
|
+
function printHelp() {
|
|
110
|
+
process.stdout.write(`atc ${VERSION} — Thalix Tower CLI\n\n` +
|
|
111
|
+
`Usage: atc <command> [args]\n\n` +
|
|
112
|
+
`Session commands (human login):\n` +
|
|
113
|
+
` login [--api <url>] [--dashboard <url>] [--manual]\n` +
|
|
114
|
+
` logout\n` +
|
|
115
|
+
` whoami\n\n` +
|
|
116
|
+
`Management commands (require atc login):\n` +
|
|
117
|
+
` projects [create "<name>"]\n` +
|
|
118
|
+
` tokens --project <id|name> list tokens\n` +
|
|
119
|
+
` tokens mint --project <p> [--label l] [--orders a,b,c]\n` +
|
|
120
|
+
` tokens revoke <tokenId>\n` +
|
|
121
|
+
` tokens align <tokenId> --orders a,b,c | --default\n` +
|
|
122
|
+
` standing ls --project <p>\n` +
|
|
123
|
+
` standing show <ref> --project <p>\n` +
|
|
124
|
+
` standing create <name> --project <p> (--body "…" | --file f) [--draft]\n` +
|
|
125
|
+
` standing edit <ref> --project <p> (--body "…" | --file f) [--name n] [--activate|--draft]\n` +
|
|
126
|
+
` standing rm <ref> --project <p>\n` +
|
|
127
|
+
` standing assign --project <p> <name1,name2,…> | --none\n` +
|
|
128
|
+
` standing propose <name> (--body "…" | --file f) (agent plane, uses ATC_TOKEN)\n\n` +
|
|
129
|
+
`Agent commands (require ATC_API + ATC_TOKEN):\n` +
|
|
130
|
+
` init [--token <freq-token>] [--save-token] [--yes] wire a project to Thalix Tower\n` +
|
|
131
|
+
` --save-token persists the token to .claude/settings.local.json (gitignored)\n` +
|
|
132
|
+
` checkin [--task "..."] [--as <callsign>] [--cli <name>]\n` +
|
|
133
|
+
` brief\n` +
|
|
134
|
+
` standing print the frequency's standing orders in full\n` +
|
|
135
|
+
` squawk "<status>" [--code working|blocked|review|mayday]\n` +
|
|
136
|
+
` claim "<glob>" (exit 3 if it conflicts)\n` +
|
|
137
|
+
` clear ["<glob>"] (omit glob to release all)\n` +
|
|
138
|
+
` note "<body>" [--tag <t>]... [--pin] [--supersede <id>]\n` +
|
|
139
|
+
` note show <id> full body of one NOTAM (briefs truncate)\n` +
|
|
140
|
+
` notes [--tag <t>] list the decision log\n` +
|
|
141
|
+
` call <callsign> "<message>" send to a callsign (or '*' for everyone)\n` +
|
|
142
|
+
` traffic [--all] [--ack-all] [--ack <id>] read your inbox\n` +
|
|
143
|
+
` checkout\n` +
|
|
144
|
+
` config\n\n` +
|
|
145
|
+
`Tier-3 hook (automatic compliance for Claude Code):\n` +
|
|
146
|
+
` hook install [--yes] wire .claude/settings.json (brief injection, auto-claims, traffic)\n` +
|
|
147
|
+
` hook run <event> hook entrypoint (called by Claude Code; fail-open, never blocks)\n` +
|
|
148
|
+
` audit compliance rate: edited paths claimed-before-edit this session\n\n` +
|
|
149
|
+
`Env: ATC_API, ATC_TOKEN, ATC_DASHBOARD. Add --json for raw output.\n`);
|
|
150
|
+
}
|
|
151
|
+
/** Try to open a URL in the default browser (best-effort, never throws). */
|
|
152
|
+
function tryOpenBrowser(url) {
|
|
153
|
+
const cmds = {
|
|
154
|
+
darwin: ['open', [url]],
|
|
155
|
+
linux: ['xdg-open', [url]],
|
|
156
|
+
win32: ['cmd', ['/c', 'start', url]],
|
|
157
|
+
};
|
|
158
|
+
const entry = cmds[process.platform];
|
|
159
|
+
if (!entry)
|
|
160
|
+
return;
|
|
161
|
+
const [bin, args] = entry;
|
|
162
|
+
try {
|
|
163
|
+
child_process.spawn(bin, args, { detached: true, stdio: 'ignore' }).unref();
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
/* ignore */
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/** Resolve a project ref (id or name) to an id via GET /v1/projects. */
|
|
170
|
+
async function resolveProject(session, ref) {
|
|
171
|
+
if (ref.startsWith('prj_'))
|
|
172
|
+
return ref;
|
|
173
|
+
const r = await (0, human_1.humanApi)(session, 'GET', '/v1/projects');
|
|
174
|
+
if (!r.ok)
|
|
175
|
+
fail(r.body?.error ?? `could not list projects (${r.status})`);
|
|
176
|
+
const projects = r.body.projects ?? [];
|
|
177
|
+
const byId = projects.find((p) => p.id === ref);
|
|
178
|
+
if (byId)
|
|
179
|
+
return byId.id;
|
|
180
|
+
const matches = projects.filter((p) => p.name.toLowerCase() === ref.toLowerCase());
|
|
181
|
+
if (matches.length === 1)
|
|
182
|
+
return matches[0].id;
|
|
183
|
+
if (matches.length === 0)
|
|
184
|
+
fail(`no project named "${ref}"`);
|
|
185
|
+
fail(`ambiguous project name "${ref}" — matches: ${matches.map((p) => `${p.id} (${p.name})`).join(', ')}`);
|
|
186
|
+
}
|
|
187
|
+
/** Read body text from --body "…" or --file f flags. */
|
|
188
|
+
function readBodyFlag(flags) {
|
|
189
|
+
if (typeof flags.body === 'string')
|
|
190
|
+
return flags.body;
|
|
191
|
+
if (typeof flags.file === 'string') {
|
|
192
|
+
try {
|
|
193
|
+
return fs.readFileSync(flags.file, 'utf8');
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
197
|
+
fail(`could not read file "${flags.file}": ${msg}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
async function main(argv) {
|
|
203
|
+
const [cmd, ...rest] = argv;
|
|
204
|
+
if (!cmd || cmd === '--help' || cmd === '-h')
|
|
205
|
+
return printHelp();
|
|
206
|
+
if (cmd === '--version' || cmd === '-v')
|
|
207
|
+
return void process.stdout.write(VERSION + '\n');
|
|
208
|
+
if (!COMMANDS.includes(cmd))
|
|
209
|
+
fail(`unknown command '${cmd}'. Run 'atc --help'.`);
|
|
210
|
+
const { positionals, flags, tags } = parseArgs(rest);
|
|
211
|
+
const json = flags.json === true;
|
|
212
|
+
const cfg = (0, client_1.loadConfig)();
|
|
213
|
+
// Tier-3 hook entrypoints. `hook run` must NEVER fail a session — it handles
|
|
214
|
+
// its own errors and always exits 0. Dispatched before any session gate.
|
|
215
|
+
if (cmd === 'hook') {
|
|
216
|
+
const sub = positionals[0];
|
|
217
|
+
if (sub === 'run')
|
|
218
|
+
return (0, hook_1.runHook)(positionals[1] ?? '');
|
|
219
|
+
if (sub === 'install')
|
|
220
|
+
return void (0, hook_1.installHooks)({ yes: flags.yes === true, json });
|
|
221
|
+
fail('usage: atc hook install | atc hook run <session-start|prompt|pre-edit>');
|
|
222
|
+
}
|
|
223
|
+
if (cmd === 'audit') {
|
|
224
|
+
return (0, hook_1.runAudit)(json);
|
|
225
|
+
}
|
|
226
|
+
if (cmd === 'init') {
|
|
227
|
+
return (0, init_1.runInit)({
|
|
228
|
+
token: typeof flags.token === 'string' ? flags.token : undefined,
|
|
229
|
+
saveToken: flags['save-token'] === true,
|
|
230
|
+
yes: flags.yes === true,
|
|
231
|
+
json,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
if (cmd === 'config') {
|
|
235
|
+
return out(json, `api: ${cfg.api || '(unset)'}\ntoken: ${cfg.token ? '(set)' : '(unset)'}`, {
|
|
236
|
+
api: cfg.api || null,
|
|
237
|
+
token: cfg.token ? 'set' : null,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
// ---- Human-plane session commands ----
|
|
241
|
+
if (cmd === 'login') {
|
|
242
|
+
const apiBase = (typeof flags.api === 'string' ? flags.api : cfg.api || client_1.DEFAULT_API).replace(/\/$/, '');
|
|
243
|
+
const dashboardBase = (typeof flags.dashboard === 'string'
|
|
244
|
+
? flags.dashboard
|
|
245
|
+
: process.env.ATC_DASHBOARD || 'https://tower.thalixinc.ai').replace(/\/$/, '');
|
|
246
|
+
const manual = flags.manual === true;
|
|
247
|
+
// Start the device flow
|
|
248
|
+
const startRes = await (0, client_1.api)({ api: apiBase, token: '' }, '', 'POST', '/v1/cli-auth/start').catch((err) => {
|
|
249
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
250
|
+
fail(`login failed: ${msg}`);
|
|
251
|
+
});
|
|
252
|
+
if (!startRes.ok) {
|
|
253
|
+
fail(startRes.body?.error ?? `login start failed (${startRes.status})`);
|
|
254
|
+
}
|
|
255
|
+
const { authId, pollSecret, expiresInSeconds } = startRes.body;
|
|
256
|
+
const verifyUrl = `${dashboardBase}/cli-auth?code=${authId}`;
|
|
257
|
+
process.stdout.write(`Open this URL to approve the login:\n ${verifyUrl}\n`);
|
|
258
|
+
if (!manual) {
|
|
259
|
+
tryOpenBrowser(verifyUrl);
|
|
260
|
+
}
|
|
261
|
+
// Poll until approved or expired
|
|
262
|
+
const deadline = Date.now() + expiresInSeconds * 1000;
|
|
263
|
+
let approved = false;
|
|
264
|
+
let token = '';
|
|
265
|
+
let orgId = '';
|
|
266
|
+
process.stderr.write('Waiting for browser approval');
|
|
267
|
+
const interval = setInterval(() => process.stderr.write('.'), 2000);
|
|
268
|
+
const cleanup = () => {
|
|
269
|
+
clearInterval(interval);
|
|
270
|
+
process.stderr.write('\n');
|
|
271
|
+
};
|
|
272
|
+
// Handle Ctrl-C gracefully
|
|
273
|
+
process.on('SIGINT', () => {
|
|
274
|
+
cleanup();
|
|
275
|
+
process.stderr.write('atc: login cancelled\n');
|
|
276
|
+
process.exit(1);
|
|
277
|
+
});
|
|
278
|
+
while (Date.now() < deadline && !approved) {
|
|
279
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
280
|
+
try {
|
|
281
|
+
const pollRes = await (0, client_1.api)({ api: apiBase, token: '' }, '', 'GET', `/v1/cli-auth/${encodeURIComponent(authId)}?secret=${encodeURIComponent(pollSecret)}`);
|
|
282
|
+
if (pollRes.ok && pollRes.body?.status === 'approved') {
|
|
283
|
+
approved = true;
|
|
284
|
+
token = pollRes.body.token;
|
|
285
|
+
orgId = pollRes.body.orgId;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
/* transient network error — keep polling */
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
cleanup();
|
|
293
|
+
if (!approved) {
|
|
294
|
+
fail('login timed out — run atc login again');
|
|
295
|
+
}
|
|
296
|
+
// Fetch identity
|
|
297
|
+
let userId;
|
|
298
|
+
try {
|
|
299
|
+
const meRes = await (0, client_1.api)({ api: apiBase, token }, token, 'GET', '/v1/me');
|
|
300
|
+
if (meRes.ok) {
|
|
301
|
+
userId = meRes.body?.userId;
|
|
302
|
+
orgId = meRes.body?.orgId || orgId;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
/* best-effort */
|
|
307
|
+
}
|
|
308
|
+
(0, human_1.saveHuman)({ api: apiBase, token, orgId, userId });
|
|
309
|
+
return out(json, `logged in as ${userId ?? '(unknown)'} (org ${orgId})`, { userId, orgId });
|
|
310
|
+
}
|
|
311
|
+
if (cmd === 'logout') {
|
|
312
|
+
const session = (0, human_1.loadHuman)();
|
|
313
|
+
if (session) {
|
|
314
|
+
try {
|
|
315
|
+
await (0, human_1.humanApi)(session, 'DELETE', '/v1/cli-auth/session');
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
/* best-effort */
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
(0, human_1.clearHuman)();
|
|
322
|
+
return out(json, 'logged out', { ok: true });
|
|
323
|
+
}
|
|
324
|
+
if (cmd === 'whoami') {
|
|
325
|
+
const session = (0, human_1.requireHuman)(fail);
|
|
326
|
+
const r = await (0, human_1.humanApi)(session, 'GET', '/v1/me');
|
|
327
|
+
if (!r.ok)
|
|
328
|
+
fail(r.body?.error ?? `whoami failed (${r.status})`);
|
|
329
|
+
return out(json, `userId: ${r.body.userId}\norgId: ${r.body.orgId}\nrole: ${r.body.orgRole ?? '(unknown)'}`, r.body);
|
|
330
|
+
}
|
|
331
|
+
if (cmd === 'projects') {
|
|
332
|
+
const session = (0, human_1.requireHuman)(fail);
|
|
333
|
+
const sub = positionals[0];
|
|
334
|
+
if (!sub || sub === 'list') {
|
|
335
|
+
const r = await (0, human_1.humanApi)(session, 'GET', '/v1/projects');
|
|
336
|
+
if (!r.ok)
|
|
337
|
+
fail(r.body?.error ?? `projects failed (${r.status})`);
|
|
338
|
+
const rows = (r.body.projects ?? []);
|
|
339
|
+
const human = rows.length
|
|
340
|
+
? rows.map((p) => `${p.id} ${p.name} (${p.status})`).join('\n')
|
|
341
|
+
: '(no projects)';
|
|
342
|
+
return out(json, human, r.body);
|
|
343
|
+
}
|
|
344
|
+
if (sub === 'create') {
|
|
345
|
+
const name = positionals[1] ?? (typeof flags.name === 'string' ? flags.name : undefined);
|
|
346
|
+
if (!name)
|
|
347
|
+
fail('usage: atc projects create "<name>"');
|
|
348
|
+
const r = await (0, human_1.humanApi)(session, 'POST', '/v1/projects', { name });
|
|
349
|
+
if (!r.ok)
|
|
350
|
+
fail(r.body?.error ?? `projects create failed (${r.status})`);
|
|
351
|
+
return out(json, `created project ${r.body.id} — ${r.body.name}`, r.body);
|
|
352
|
+
}
|
|
353
|
+
fail(`unknown projects subcommand "${sub}"`);
|
|
354
|
+
}
|
|
355
|
+
if (cmd === 'tokens') {
|
|
356
|
+
const session = (0, human_1.requireHuman)(fail);
|
|
357
|
+
const sub = positionals[0];
|
|
358
|
+
if (!sub || sub === 'list') {
|
|
359
|
+
const projectRef = typeof flags.project === 'string' ? flags.project : undefined;
|
|
360
|
+
if (!projectRef)
|
|
361
|
+
fail('usage: atc tokens --project <id|name>');
|
|
362
|
+
const pid = await resolveProject(session, projectRef);
|
|
363
|
+
const r = await (0, human_1.humanApi)(session, 'GET', `/v1/projects/${encodeURIComponent(pid)}/tokens`);
|
|
364
|
+
if (!r.ok)
|
|
365
|
+
fail(r.body?.error ?? `tokens list failed (${r.status})`);
|
|
366
|
+
const rows = (r.body.tokens ?? []);
|
|
367
|
+
const human = rows.length
|
|
368
|
+
? rows.map((t) => `${t.tokenId} ${t.label ?? '(no label)'} ${t.revoked ? '[revoked]' : '[active]'} ${t.createdAt}`).join('\n')
|
|
369
|
+
: '(no tokens)';
|
|
370
|
+
return out(json, human, r.body);
|
|
371
|
+
}
|
|
372
|
+
if (sub === 'mint') {
|
|
373
|
+
const projectRef = typeof flags.project === 'string' ? flags.project : undefined;
|
|
374
|
+
if (!projectRef)
|
|
375
|
+
fail('usage: atc tokens mint --project <p> [--label l] [--orders a,b,c]');
|
|
376
|
+
const pid = await resolveProject(session, projectRef);
|
|
377
|
+
const label = typeof flags.label === 'string' ? flags.label : undefined;
|
|
378
|
+
const ordersRaw = typeof flags.orders === 'string' ? flags.orders : undefined;
|
|
379
|
+
const standingOrderIds = ordersRaw ? ordersRaw.split(',').map((s) => s.trim()).filter(Boolean) : undefined;
|
|
380
|
+
const r = await (0, human_1.humanApi)(session, 'POST', `/v1/projects/${encodeURIComponent(pid)}/tokens`, {
|
|
381
|
+
label,
|
|
382
|
+
standingOrderIds,
|
|
383
|
+
});
|
|
384
|
+
if (!r.ok)
|
|
385
|
+
fail(r.body?.error ?? `tokens mint failed (${r.status})`);
|
|
386
|
+
const body = r.body;
|
|
387
|
+
// Token shown ONCE — warning on stderr so --json stdout stays parseable.
|
|
388
|
+
process.stderr.write(`WARNING: copy this token now — it will not be shown again.\n`);
|
|
389
|
+
return out(json, `token: ${body.token}\nid: ${body.tokenId}${body.label ? `\nlabel: ${body.label}` : ''}`, r.body);
|
|
390
|
+
}
|
|
391
|
+
if (sub === 'revoke') {
|
|
392
|
+
const tokenId = positionals[1];
|
|
393
|
+
if (!tokenId)
|
|
394
|
+
fail('usage: atc tokens revoke <tokenId>');
|
|
395
|
+
const r = await (0, human_1.humanApi)(session, 'DELETE', `/v1/tokens/${encodeURIComponent(tokenId)}`);
|
|
396
|
+
if (!r.ok)
|
|
397
|
+
fail(r.body?.error ?? `tokens revoke failed (${r.status})`);
|
|
398
|
+
return out(json, `revoked ${tokenId}`, r.body);
|
|
399
|
+
}
|
|
400
|
+
if (sub === 'align') {
|
|
401
|
+
const tokenId = positionals[1];
|
|
402
|
+
if (!tokenId)
|
|
403
|
+
fail('usage: atc tokens align <tokenId> --orders a,b,c | --default');
|
|
404
|
+
let orders;
|
|
405
|
+
if (flags.default === true) {
|
|
406
|
+
orders = null;
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
const ordersRaw = typeof flags.orders === 'string' ? flags.orders : undefined;
|
|
410
|
+
if (!ordersRaw)
|
|
411
|
+
fail('usage: atc tokens align <tokenId> --orders a,b,c | --default');
|
|
412
|
+
orders = ordersRaw.split(',').map((s) => s.trim()).filter(Boolean);
|
|
413
|
+
}
|
|
414
|
+
const r = await (0, human_1.humanApi)(session, 'PUT', `/v1/tokens/${encodeURIComponent(tokenId)}/standing`, { orders });
|
|
415
|
+
if (!r.ok)
|
|
416
|
+
fail(r.body?.error ?? `tokens align failed (${r.status})`);
|
|
417
|
+
return out(json, orders === null ? `${tokenId} → project default` : `${tokenId} → [${orders.join(', ')}]`, r.body);
|
|
418
|
+
}
|
|
419
|
+
fail(`unknown tokens subcommand "${sub}"`);
|
|
420
|
+
}
|
|
421
|
+
// ---- standing subcommands (management + agent) ----
|
|
422
|
+
if (cmd === 'standing') {
|
|
423
|
+
// No sub → agent plane read (existing behaviour; requires ATC_TOKEN session)
|
|
424
|
+
if (!positionals[0]) {
|
|
425
|
+
if (!cfg.api || !cfg.token) {
|
|
426
|
+
fail('ATC_API and ATC_TOKEN must be set (get a frequency token from the dashboard)');
|
|
427
|
+
}
|
|
428
|
+
const session = requireSession();
|
|
429
|
+
const tok = session.sessionToken;
|
|
430
|
+
const r = await (0, client_1.api)(cfg, tok, 'GET', '/v1/standing');
|
|
431
|
+
if (!r.ok)
|
|
432
|
+
fail(r.body?.error ?? `standing failed (${r.status})`, 2);
|
|
433
|
+
const s = r.body.standing;
|
|
434
|
+
return out(json, s
|
|
435
|
+
? `standing orders (${s.source}): ${s.orders.map((o) => `${o.name} v${o.version}`).join(', ')}\n\n${s.body}`
|
|
436
|
+
: '(no standing orders assigned to this session)', r.body);
|
|
437
|
+
}
|
|
438
|
+
const sub = positionals[0];
|
|
439
|
+
// propose — agent plane: uses the existing repo session token
|
|
440
|
+
if (sub === 'propose') {
|
|
441
|
+
if (!cfg.api || !cfg.token) {
|
|
442
|
+
fail('ATC_API and ATC_TOKEN must be set (get a frequency token from the dashboard)');
|
|
443
|
+
}
|
|
444
|
+
const session = requireSession();
|
|
445
|
+
const tok = session.sessionToken;
|
|
446
|
+
const name = positionals[1];
|
|
447
|
+
if (!name)
|
|
448
|
+
fail('usage: atc standing propose <name> (--body "…" | --file f)');
|
|
449
|
+
const body = readBodyFlag(flags);
|
|
450
|
+
if (!body)
|
|
451
|
+
fail('usage: atc standing propose <name> (--body "…" | --file f)');
|
|
452
|
+
const r = await (0, client_1.api)(cfg, tok, 'POST', '/v1/standing-orders', { name, body });
|
|
453
|
+
if (!r.ok)
|
|
454
|
+
fail(r.body?.error ?? `standing propose failed (${r.status})`, 2);
|
|
455
|
+
const order = r.body.order ?? r.body;
|
|
456
|
+
return out(json, `draft ${order.id ?? order.orderId} proposed — a human can activate/assign it`, r.body);
|
|
457
|
+
}
|
|
458
|
+
// Management subcommands below all require human login
|
|
459
|
+
const humanSession = (0, human_1.requireHuman)(fail);
|
|
460
|
+
const projectRef = typeof flags.project === 'string' ? flags.project : undefined;
|
|
461
|
+
if (!projectRef && sub !== 'propose') {
|
|
462
|
+
fail('--project <id|name> is required for standing management commands');
|
|
463
|
+
}
|
|
464
|
+
const pid = projectRef ? await resolveProject(humanSession, projectRef) : '';
|
|
465
|
+
if (sub === 'ls') {
|
|
466
|
+
const r = await (0, human_1.humanApi)(humanSession, 'GET', `/v1/projects/${encodeURIComponent(pid)}/standing-orders`);
|
|
467
|
+
if (!r.ok)
|
|
468
|
+
fail(r.body?.error ?? `standing ls failed (${r.status})`);
|
|
469
|
+
const { orders, defaultAssignment } = r.body;
|
|
470
|
+
if (!orders.length)
|
|
471
|
+
return out(json, '(no standing orders)', r.body);
|
|
472
|
+
const human = orders
|
|
473
|
+
.map((o) => {
|
|
474
|
+
const marker = defaultAssignment.includes(o.id) ? ' [default]' : '';
|
|
475
|
+
return `${o.id} ${o.name} ${o.status} v${o.version} ${o.updatedAt}${marker}`;
|
|
476
|
+
})
|
|
477
|
+
.join('\n');
|
|
478
|
+
return out(json, human, r.body);
|
|
479
|
+
}
|
|
480
|
+
if (sub === 'show') {
|
|
481
|
+
const ref = positionals[1];
|
|
482
|
+
if (!ref)
|
|
483
|
+
fail('usage: atc standing show <ref> --project <p>');
|
|
484
|
+
const r = await (0, human_1.humanApi)(humanSession, 'GET', `/v1/projects/${encodeURIComponent(pid)}/standing-orders/${encodeURIComponent(ref)}`);
|
|
485
|
+
if (!r.ok)
|
|
486
|
+
fail(r.body?.error ?? `standing show failed (${r.status})`);
|
|
487
|
+
const { order, history } = r.body;
|
|
488
|
+
const histLine = history?.length
|
|
489
|
+
? `\nhistory: ${history.map((h) => `v${h.version} (${h.updatedAt})`).join(', ')}`
|
|
490
|
+
: '';
|
|
491
|
+
return out(json, `[${order.id}] ${order.name} ${order.status} v${order.version} ${order.updatedAt}${histLine}\n\n${order.body}`, r.body);
|
|
492
|
+
}
|
|
493
|
+
if (sub === 'create') {
|
|
494
|
+
const name = positionals[1];
|
|
495
|
+
if (!name)
|
|
496
|
+
fail('usage: atc standing create <name> --project <p> (--body "…" | --file f) [--draft]');
|
|
497
|
+
const body = readBodyFlag(flags);
|
|
498
|
+
if (!body)
|
|
499
|
+
fail('--body "…" or --file <path> is required');
|
|
500
|
+
const status = flags.draft === true ? 'draft' : 'active';
|
|
501
|
+
const r = await (0, human_1.humanApi)(humanSession, 'POST', `/v1/projects/${encodeURIComponent(pid)}/standing-orders`, {
|
|
502
|
+
name,
|
|
503
|
+
body,
|
|
504
|
+
status,
|
|
505
|
+
});
|
|
506
|
+
if (!r.ok)
|
|
507
|
+
fail(r.body?.error ?? `standing create failed (${r.status})`);
|
|
508
|
+
const order = r.body.order;
|
|
509
|
+
return out(json, `created ${order.id} ${order.name} ${order.status}`, r.body);
|
|
510
|
+
}
|
|
511
|
+
if (sub === 'edit') {
|
|
512
|
+
const ref = positionals[1];
|
|
513
|
+
if (!ref)
|
|
514
|
+
fail('usage: atc standing edit <ref> --project <p> (--body "…" | --file f) [--name n] [--activate|--draft]');
|
|
515
|
+
const body = readBodyFlag(flags);
|
|
516
|
+
const name = typeof flags.name === 'string' ? flags.name : undefined;
|
|
517
|
+
const status = flags.activate === true ? 'active' : flags.draft === true ? 'draft' : undefined;
|
|
518
|
+
if (!body && !name && !status) {
|
|
519
|
+
fail('standing edit requires at least one of --body/--file, --name, --activate, or --draft');
|
|
520
|
+
}
|
|
521
|
+
const r = await (0, human_1.humanApi)(humanSession, 'PUT', `/v1/projects/${encodeURIComponent(pid)}/standing-orders/${encodeURIComponent(ref)}`, { body, name, status });
|
|
522
|
+
if (!r.ok)
|
|
523
|
+
fail(r.body?.error ?? `standing edit failed (${r.status})`);
|
|
524
|
+
const order = r.body.order;
|
|
525
|
+
return out(json, `updated ${order.id} ${order.name} ${order.status} v${order.version}`, r.body);
|
|
526
|
+
}
|
|
527
|
+
if (sub === 'rm') {
|
|
528
|
+
const ref = positionals[1];
|
|
529
|
+
if (!ref)
|
|
530
|
+
fail('usage: atc standing rm <ref> --project <p>');
|
|
531
|
+
const r = await (0, human_1.humanApi)(humanSession, 'DELETE', `/v1/projects/${encodeURIComponent(pid)}/standing-orders/${encodeURIComponent(ref)}`);
|
|
532
|
+
if (!r.ok)
|
|
533
|
+
fail(r.body?.error ?? `standing rm failed (${r.status})`);
|
|
534
|
+
return out(json, `deleted ${r.body.deleted ?? ref}`, r.body);
|
|
535
|
+
}
|
|
536
|
+
if (sub === 'assign') {
|
|
537
|
+
if (flags.none === true) {
|
|
538
|
+
const r = await (0, human_1.humanApi)(humanSession, 'PUT', `/v1/projects/${encodeURIComponent(pid)}/standing-assignment`, { orders: [] });
|
|
539
|
+
if (!r.ok)
|
|
540
|
+
fail(r.body?.error ?? `standing assign failed (${r.status})`);
|
|
541
|
+
return out(json, 'default assignment cleared', r.body);
|
|
542
|
+
}
|
|
543
|
+
// positionals[1] is the comma-separated list, or could be positionals[1..n]
|
|
544
|
+
const ordersRaw = positionals.slice(1).join(',');
|
|
545
|
+
if (!ordersRaw)
|
|
546
|
+
fail('usage: atc standing assign --project <p> <name1,name2,…> | --none');
|
|
547
|
+
const orders = ordersRaw.split(',').map((s) => s.trim()).filter(Boolean);
|
|
548
|
+
const r = await (0, human_1.humanApi)(humanSession, 'PUT', `/v1/projects/${encodeURIComponent(pid)}/standing-assignment`, { orders });
|
|
549
|
+
if (!r.ok)
|
|
550
|
+
fail(r.body?.error ?? `standing assign failed (${r.status})`);
|
|
551
|
+
return out(json, `default assignment → [${(r.body.defaultAssignment ?? orders).join(', ')}]`, r.body);
|
|
552
|
+
}
|
|
553
|
+
fail(`unknown standing subcommand "${sub}"`);
|
|
554
|
+
}
|
|
555
|
+
// ---- Agent-plane commands: require ATC_API + ATC_TOKEN ----
|
|
556
|
+
if (!cfg.api || !cfg.token) {
|
|
557
|
+
fail('ATC_API and ATC_TOKEN must be set (get a frequency token from the dashboard)');
|
|
558
|
+
}
|
|
559
|
+
if (cmd === 'checkin') {
|
|
560
|
+
const r = await (0, client_1.api)(cfg, cfg.token, 'POST', '/v1/sessions', {
|
|
561
|
+
workspaceId: (0, client_1.workspaceId)(),
|
|
562
|
+
callsign: flags.as,
|
|
563
|
+
cli: flags.cli || process.env.ATC_CLI || 'cli',
|
|
564
|
+
worktree: process.cwd(),
|
|
565
|
+
branch: (0, client_1.currentBranch)(),
|
|
566
|
+
task: flags.task,
|
|
567
|
+
});
|
|
568
|
+
if (!r.ok)
|
|
569
|
+
fail(r.body?.error ?? `checkin failed (${r.status})`, 2);
|
|
570
|
+
(0, client_1.saveSession)({ callsign: r.body.callsign, sessionToken: r.body.sessionToken, api: cfg.api });
|
|
571
|
+
const c = r.body.brief?.counts ?? {};
|
|
572
|
+
const so = r.body.brief?.standing;
|
|
573
|
+
const standing = so
|
|
574
|
+
? `\n⚑ standing orders present (${(so.orders ?? []).map((o) => o.name).join(', ') || 'assigned'}) — read them: atc standing`
|
|
575
|
+
: '';
|
|
576
|
+
const trafficNudge = (c.traffic ?? 0) > 0
|
|
577
|
+
? `\n✉ ${c.traffic} messages waiting — atc traffic`
|
|
578
|
+
: '';
|
|
579
|
+
return out(json, `checked in as ${r.body.callsign} · ${c.sessions ?? 0} on board, ${c.claims ?? 0} claims, ${c.notams ?? 0} notams${standing}${trafficNudge}`, r.body);
|
|
580
|
+
}
|
|
581
|
+
const session = requireSession();
|
|
582
|
+
const tok = session.sessionToken;
|
|
583
|
+
switch (cmd) {
|
|
584
|
+
case 'brief': {
|
|
585
|
+
const r = await (0, client_1.api)(cfg, tok, 'GET', '/v1/brief');
|
|
586
|
+
if (!r.ok)
|
|
587
|
+
fail(r.body?.error ?? `brief failed (${r.status})`, 2);
|
|
588
|
+
const b = r.body;
|
|
589
|
+
const standingLine = b.standing
|
|
590
|
+
? `standing orders (${b.standing.source}): ${b.standing.orders.map((o) => o.name).join(', ')}` +
|
|
591
|
+
`${b.standing.truncated ? ' (truncated here — atc standing for full)' : ''}\n${b.standing.body}\n---\n`
|
|
592
|
+
: '';
|
|
593
|
+
const who = (x) => `${x.callsign}(${x.code ?? x.statusText ?? x.status ?? 'working'}${x.claimCount ? `, ${x.claimCount} claims` : ''})`;
|
|
594
|
+
const trafficLine = (b.traffic?.unacked ?? 0) > 0
|
|
595
|
+
? `\ntraffic: ${b.traffic.unacked} unacked\n ✉ ${b.traffic.preview[0]?.from}: ${b.traffic.preview[0]?.body}`
|
|
596
|
+
: '\ntraffic: 0 unacked';
|
|
597
|
+
const human = standingLine +
|
|
598
|
+
`roster: ${b.roster.map(who).join(', ') || '(none)'}\n` +
|
|
599
|
+
`claims: ${b.claims.map((x) => `${x.callsign}:${x.glob}`).join(', ') || '(none)'}` +
|
|
600
|
+
(b.counts.claims > b.claims.length ? ` (+${b.counts.claims - b.claims.length} more on board)` : '') +
|
|
601
|
+
`\n` +
|
|
602
|
+
renderConflicts(b.conflictsWithMine) +
|
|
603
|
+
trafficLine +
|
|
604
|
+
`\nnotams: ${b.notams.length} shown (pinned + recent; truncated bodies — 'atc note show <id>' for full)`;
|
|
605
|
+
return out(json, human, b);
|
|
606
|
+
}
|
|
607
|
+
case 'squawk': {
|
|
608
|
+
const r = await (0, client_1.api)(cfg, tok, 'POST', '/v1/squawk', {
|
|
609
|
+
status: positionals.join(' ') || undefined,
|
|
610
|
+
code: flags.code,
|
|
611
|
+
});
|
|
612
|
+
if (!r.ok)
|
|
613
|
+
fail(r.body?.error ?? `squawk failed (${r.status})`, 2);
|
|
614
|
+
const squawkConflicts = renderConflicts(r.body.conflictsWithMine);
|
|
615
|
+
const unackedTraffic = r.body.unackedTraffic ?? 0;
|
|
616
|
+
const trafficHint = unackedTraffic > 0 ? `\n✉ ${unackedTraffic} unacked — atc traffic` : '';
|
|
617
|
+
return out(json, squawkConflicts + trafficHint, r.body);
|
|
618
|
+
}
|
|
619
|
+
case 'call': {
|
|
620
|
+
const callsign = positionals[0];
|
|
621
|
+
if (!callsign)
|
|
622
|
+
fail('usage: atc call <callsign> "<message>"');
|
|
623
|
+
const message = typeof flags.body === 'string' ? flags.body : positionals.slice(1).join(' ');
|
|
624
|
+
if (!message)
|
|
625
|
+
fail('usage: atc call <callsign> "<message>"');
|
|
626
|
+
const r = await (0, client_1.api)(cfg, tok, 'POST', '/v1/calls', { to: callsign, body: message });
|
|
627
|
+
if (!r.ok)
|
|
628
|
+
fail(r.body?.error ?? `call failed (${r.status})`, 2);
|
|
629
|
+
const delivered = r.body.delivered ?? [];
|
|
630
|
+
let human;
|
|
631
|
+
if (callsign === '*') {
|
|
632
|
+
human = `→ *: delivered to [${delivered.join(', ')}]`;
|
|
633
|
+
}
|
|
634
|
+
else {
|
|
635
|
+
human = `→ ${callsign}: delivered`;
|
|
636
|
+
if (r.body.recipientActive === false) {
|
|
637
|
+
human += '\n(offline — queued for next checkin)';
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return out(json, human, r.body);
|
|
641
|
+
}
|
|
642
|
+
case 'traffic': {
|
|
643
|
+
const all = flags.all === true;
|
|
644
|
+
const ackAll = flags['ack-all'] === true;
|
|
645
|
+
const ackId = typeof flags.ack === 'string' ? flags.ack : undefined;
|
|
646
|
+
const q = all ? '?all=1' : '';
|
|
647
|
+
const r = await (0, client_1.api)(cfg, tok, 'GET', `/v1/traffic${q}`);
|
|
648
|
+
if (!r.ok)
|
|
649
|
+
fail(r.body?.error ?? `traffic failed (${r.status})`, 2);
|
|
650
|
+
const messages = r.body.messages ?? [];
|
|
651
|
+
const unacked = r.body.unacked ?? 0;
|
|
652
|
+
const lines = messages.map((m) => {
|
|
653
|
+
const ackedMark = m.acked ? ' [acked]' : '';
|
|
654
|
+
return `[${m.id}] from ${m.from} at ${m.createdAt}: ${m.body}${ackedMark}`;
|
|
655
|
+
});
|
|
656
|
+
const listHuman = lines.length ? lines.join('\n') : '(no messages)';
|
|
657
|
+
if (ackAll) {
|
|
658
|
+
const ackR = await (0, client_1.api)(cfg, tok, 'POST', '/v1/traffic/ack', { all: true });
|
|
659
|
+
if (!ackR.ok)
|
|
660
|
+
fail(ackR.body?.error ?? `ack failed (${ackR.status})`, 2);
|
|
661
|
+
return out(json, listHuman + `\nacknowledged all`, { ...r.body, acked: ackR.body.acked });
|
|
662
|
+
}
|
|
663
|
+
if (ackId) {
|
|
664
|
+
const ackR = await (0, client_1.api)(cfg, tok, 'POST', '/v1/traffic/ack', { ids: [ackId] });
|
|
665
|
+
if (!ackR.ok)
|
|
666
|
+
fail(ackR.body?.error ?? `ack failed (${ackR.status})`, 2);
|
|
667
|
+
return out(json, listHuman + `\nacknowledged ${ackId}`, { ...r.body, acked: ackR.body.acked });
|
|
668
|
+
}
|
|
669
|
+
const hint = unacked > 0 ? `\n(ack with: atc traffic --ack-all)` : '';
|
|
670
|
+
return out(json, listHuman + hint, r.body);
|
|
671
|
+
}
|
|
672
|
+
case 'claim': {
|
|
673
|
+
const glob = positionals[0];
|
|
674
|
+
if (!glob)
|
|
675
|
+
fail('usage: atc claim "<glob>"');
|
|
676
|
+
const r = await (0, client_1.api)(cfg, tok, 'POST', '/v1/claims', { glob });
|
|
677
|
+
if (!r.ok)
|
|
678
|
+
fail(r.body?.error ?? `claim failed (${r.status})`, 2);
|
|
679
|
+
const conflicts = r.body.conflicts ?? [];
|
|
680
|
+
out(json, `claimed ${glob}\n${renderConflicts(conflicts)}`, r.body);
|
|
681
|
+
if (conflicts.length > 0)
|
|
682
|
+
process.exit(3);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
case 'clear': {
|
|
686
|
+
const r = await (0, client_1.api)(cfg, tok, 'POST', '/v1/clear', { glob: positionals[0] });
|
|
687
|
+
if (!r.ok)
|
|
688
|
+
fail(r.body?.error ?? `clear failed (${r.status})`, 2);
|
|
689
|
+
return out(json, `cleared: ${(r.body.cleared ?? []).join(', ') || '(none)'}`, r.body);
|
|
690
|
+
}
|
|
691
|
+
case 'note': {
|
|
692
|
+
if (positionals[0] === 'show') {
|
|
693
|
+
const id = positionals[1];
|
|
694
|
+
if (!id)
|
|
695
|
+
fail('usage: atc note show <id>');
|
|
696
|
+
const r = await (0, client_1.api)(cfg, tok, 'GET', `/v1/notams/${encodeURIComponent(id)}`);
|
|
697
|
+
if (!r.ok)
|
|
698
|
+
fail(r.body?.error ?? `note show failed (${r.status})`, 2);
|
|
699
|
+
const n = r.body.notam;
|
|
700
|
+
return out(json, `${n.pinned ? '📌 ' : ''}[${n.id}] by ${n.author} at ${n.createdAt}` +
|
|
701
|
+
`${n.tags?.length ? ' #' + n.tags.join(' #') : ''}${n.superseded ? ' (superseded)' : ''}\n${n.body}`, r.body);
|
|
702
|
+
}
|
|
703
|
+
const body = positionals.join(' ');
|
|
704
|
+
if (!body)
|
|
705
|
+
fail('usage: atc note "<body>" [--tag t] [--pin] [--supersede <id>] | atc note show <id>');
|
|
706
|
+
const supersedes = typeof flags.supersede === 'string' ? flags.supersede : undefined;
|
|
707
|
+
const r = await (0, client_1.api)(cfg, tok, 'POST', '/v1/notams', { body, tags, pinned: flags.pin === true, supersedes });
|
|
708
|
+
if (!r.ok)
|
|
709
|
+
fail(r.body?.error ?? `note failed (${r.status})`, 2);
|
|
710
|
+
return out(json, `noted (${r.body.id})`, r.body);
|
|
711
|
+
}
|
|
712
|
+
case 'notes': {
|
|
713
|
+
const q = tags[0] ? `?tag=${encodeURIComponent(tags[0])}` : '';
|
|
714
|
+
const r = await (0, client_1.api)(cfg, tok, 'GET', `/v1/notams${q}`);
|
|
715
|
+
if (!r.ok)
|
|
716
|
+
fail(r.body?.error ?? `notes failed (${r.status})`, 2);
|
|
717
|
+
const human = (r.body.notams ?? [])
|
|
718
|
+
.map((n) => `${n.pinned ? '📌 ' : ''}[${n.id}] ${n.body}${n.tags?.length ? ' #' + n.tags.join(' #') : ''}`)
|
|
719
|
+
.join('\n') || '(no notams)';
|
|
720
|
+
return out(json, human, r.body);
|
|
721
|
+
}
|
|
722
|
+
case 'checkout': {
|
|
723
|
+
const r = await (0, client_1.api)(cfg, tok, 'POST', '/v1/checkout');
|
|
724
|
+
(0, client_1.clearSession)();
|
|
725
|
+
if (!r.ok)
|
|
726
|
+
fail(r.body?.error ?? `checkout failed (${r.status})`, 2);
|
|
727
|
+
return out(json, 'checked out', r.body);
|
|
728
|
+
}
|
|
729
|
+
default:
|
|
730
|
+
fail(`command '${cmd}' not wired`);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
main(process.argv.slice(2)).catch((err) => {
|
|
734
|
+
process.stderr.write(`atc: ${err?.message ?? err}\n`);
|
|
735
|
+
process.exit(2);
|
|
736
|
+
});
|