@cordfuse/crosstalk 7.0.0-alpha.9 → 8.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GUIDE-CLI.md +322 -0
- package/GUIDE-PROMPTS.md +118 -0
- package/LICENSE +21 -0
- package/README.md +422 -61
- package/bin/crosstalk.js +88 -54
- package/commands/agent.js +69 -0
- package/commands/auth.js +273 -0
- package/commands/channel.js +54 -44
- package/commands/chat.js +107 -71
- package/commands/daemon.js +120 -0
- package/commands/logs.js +108 -19
- package/commands/message.js +125 -0
- package/commands/server.js +185 -0
- package/commands/settings.js +49 -0
- package/commands/status.js +37 -13
- package/commands/token.js +136 -0
- package/commands/transport.js +299 -0
- package/commands/version.js +3 -3
- package/commands/workflow.js +234 -0
- package/deploy/crosstalk@.service +62 -0
- package/deploy/install.sh +82 -0
- package/lib/api-client.js +77 -22
- package/lib/credentials.js +207 -0
- package/lib/nativeServer.js +182 -0
- package/lib/resolve.js +106 -37
- package/package.json +27 -4
- package/src/activation.ts +104 -0
- package/src/api.ts +1717 -0
- package/src/auth/enforce.ts +68 -0
- package/src/auth/handlers.ts +266 -0
- package/src/auth/middleware.ts +132 -0
- package/src/auth/setup.ts +263 -0
- package/src/auth/tokens.ts +285 -0
- package/src/auth/users.ts +267 -0
- package/src/dispatch.ts +530 -0
- package/src/dispatchers.ts +91 -0
- package/src/filenames.ts +28 -0
- package/src/frontmatter.ts +26 -0
- package/src/init.ts +116 -0
- package/src/invoke.ts +201 -0
- package/src/log-buffer.ts +67 -0
- package/src/models.ts +300 -0
- package/src/resolve.ts +100 -0
- package/src/state.ts +191 -0
- package/src/stop.ts +37 -0
- package/src/transport.ts +353 -0
- package/src/web/auth-pages.ts +160 -0
- package/src/web/channels.ts +395 -0
- package/src/web/chat-page.ts +636 -0
- package/src/web/chat-pty.ts +254 -0
- package/src/web/dashboard.ts +130 -0
- package/src/web/layout.ts +237 -0
- package/src/web/stubs.ts +510 -0
- package/src/web/workflows.ts +490 -0
- package/src/workflow.ts +470 -0
- package/template/CLAUDE.md +10 -0
- package/template/CROSSTALK-VERSION +1 -0
- package/template/CROSSTALK.md +262 -0
- package/template/PROTOCOL.md +70 -0
- package/template/README.md +64 -0
- package/template/auth/.gitkeep +0 -0
- package/template/auth/README.md +224 -0
- package/template/data/crosstalk.yaml +196 -0
- package/template/gitignore +4 -0
- package/commands/down.js +0 -40
- package/commands/init.js +0 -243
- package/commands/pull.js +0 -22
- package/commands/replies.js +0 -40
- package/commands/restart.js +0 -29
- package/commands/rm.js +0 -109
- package/commands/run.js +0 -115
- package/commands/up.js +0 -135
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// crosstalk server — native engine lifecycle (v8-native).
|
|
2
|
+
//
|
|
3
|
+
// Replaces the v7 docker lifecycle (up/down/restart/pull/logs). The
|
|
4
|
+
// engine runs as a detached host process; state lives under
|
|
5
|
+
// `<base>/<name>/crosstalk-state/` (pid + log files).
|
|
6
|
+
//
|
|
7
|
+
// Subverbs:
|
|
8
|
+
// crosstalk server start [--containername N]
|
|
9
|
+
// crosstalk server stop [--containername N]
|
|
10
|
+
// crosstalk server restart [--containername N]
|
|
11
|
+
// crosstalk server status [--containername N] [--probe]
|
|
12
|
+
// crosstalk server logs [--containername N] [-f|--follow]
|
|
13
|
+
//
|
|
14
|
+
// All honor CROSSTALK_USER_MODE=1 (boolean opt-in). Default is system
|
|
15
|
+
// mode (paths under /var/lib/crosstalk/, runs as `crosstalk` service
|
|
16
|
+
// user under the systemd unit shipped in deploy/crosstalk@.service).
|
|
17
|
+
// CROSSTALK_USER_MODE=1 switches to user mode (paths under
|
|
18
|
+
// ~/.local/share/crosstalk/ on Linux) — intended for solo dev or
|
|
19
|
+
// operators who don't want a system service.
|
|
20
|
+
|
|
21
|
+
import { spawn } from 'child_process';
|
|
22
|
+
import { existsSync } from 'fs';
|
|
23
|
+
import { join, resolve } from 'path';
|
|
24
|
+
import { has, flag } from '../lib/argv.js';
|
|
25
|
+
import {
|
|
26
|
+
requireInitialized, storageMode, parseContainerName,
|
|
27
|
+
DEFAULT_CONTAINER_NAME,
|
|
28
|
+
} from '../lib/resolve.js';
|
|
29
|
+
import { startEngine, stopEngine, engineStatus } from '../lib/nativeServer.js';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* In system mode the engine is supposed to run as the `crosstalk`
|
|
33
|
+
* service user under systemd, not as the operator who invoked the CLI.
|
|
34
|
+
* Spawning a detached child from the operator's shell would be the
|
|
35
|
+
* wrong lifecycle. Redirect to systemctl instead — clear hint + exit 2.
|
|
36
|
+
*/
|
|
37
|
+
function redirectToSystemctl(verb, name) {
|
|
38
|
+
const unit = `crosstalk@${name}`;
|
|
39
|
+
process.stderr.write(
|
|
40
|
+
`crosstalk server ${verb}: system mode is the default — the engine\n` +
|
|
41
|
+
`runs as the 'crosstalk' service user under systemd, not as a child\n` +
|
|
42
|
+
`of this CLI.\n\n` +
|
|
43
|
+
` Use: sudo systemctl ${verb} ${unit}\n\n` +
|
|
44
|
+
`Or opt into user mode (spawn as yourself, no systemd):\n` +
|
|
45
|
+
` CROSSTALK_USER_MODE=1 crosstalk server ${verb}${name === DEFAULT_CONTAINER_NAME ? '' : ' --containername ' + name}\n`,
|
|
46
|
+
);
|
|
47
|
+
return 2;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function nameSuffix(name) {
|
|
51
|
+
return name === DEFAULT_CONTAINER_NAME ? '' : ` --containername ${name}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function usage(exit = 0) {
|
|
55
|
+
const w = exit === 0 ? process.stdout : process.stderr;
|
|
56
|
+
w.write(
|
|
57
|
+
`Usage: crosstalk server <subverb> [--containername <name>]
|
|
58
|
+
|
|
59
|
+
Subverbs:
|
|
60
|
+
start [--path <dir>] start the engine; --path runs it on a transport at an
|
|
61
|
+
arbitrary directory (e.g. a mounted share), state stays
|
|
62
|
+
machine-local (user mode only)
|
|
63
|
+
stop SIGTERM the running engine; SIGKILL after 5s grace
|
|
64
|
+
restart stop + start
|
|
65
|
+
status [--probe] report pid + uptime; --probe also pings /healthz
|
|
66
|
+
logs [-f|--follow] tail crosstalk-state/crosstalk.log
|
|
67
|
+
|
|
68
|
+
Storage mode defaults to 'system'; set CROSSTALK_USER_MODE=1 for user mode.
|
|
69
|
+
`);
|
|
70
|
+
process.exit(exit);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function cmdStart(argv) {
|
|
74
|
+
// --path <dir>: run the engine on a transport at an arbitrary directory
|
|
75
|
+
// (e.g. a mounted SMB/NFS share) instead of the managed storage layout.
|
|
76
|
+
// State (cursor/heartbeat/pid/port) stays machine-local, keyed by name.
|
|
77
|
+
const transportPath = flag(argv, '--path');
|
|
78
|
+
if (transportPath) {
|
|
79
|
+
if (storageMode() === 'system') {
|
|
80
|
+
process.stderr.write(`crosstalk server start: --path is user-mode only (set CROSSTALK_USER_MODE=1).\n`);
|
|
81
|
+
return 1;
|
|
82
|
+
}
|
|
83
|
+
if (!existsSync(join(transportPath, 'CROSSTALK-VERSION'))) {
|
|
84
|
+
process.stderr.write(
|
|
85
|
+
`crosstalk server start: '${transportPath}' is not a crosstalk transport (no CROSSTALK-VERSION).\n` +
|
|
86
|
+
` Scaffold it first: crosstalk daemon init ${transportPath}\n`,
|
|
87
|
+
);
|
|
88
|
+
return 1;
|
|
89
|
+
}
|
|
90
|
+
const name = parseContainerName(argv);
|
|
91
|
+
const r = startEngine(name, { transportPath });
|
|
92
|
+
if (!r.ok) {
|
|
93
|
+
process.stderr.write(`crosstalk server start: ${r.error}\n`);
|
|
94
|
+
return 1;
|
|
95
|
+
}
|
|
96
|
+
process.stdout.write(
|
|
97
|
+
`'${name}' started on ${resolve(transportPath)} (pid ${r.pid}, api 127.0.0.1:${r.port}).\n` +
|
|
98
|
+
` state stays machine-local; logs: crosstalk server logs${nameSuffix(name)} -f\n`,
|
|
99
|
+
);
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const { name } = requireInitialized(argv);
|
|
104
|
+
if (storageMode() === 'system') return redirectToSystemctl('start', name);
|
|
105
|
+
const r = startEngine(name);
|
|
106
|
+
if (!r.ok) {
|
|
107
|
+
process.stderr.write(`crosstalk server start: ${r.error}\n`);
|
|
108
|
+
return 1;
|
|
109
|
+
}
|
|
110
|
+
process.stdout.write(
|
|
111
|
+
`'${name}' started (pid ${r.pid}, api 127.0.0.1:${r.port}).\n` +
|
|
112
|
+
` logs: crosstalk server logs${nameSuffix(name)} -f\n`,
|
|
113
|
+
);
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function cmdStop(argv) {
|
|
118
|
+
const { name } = requireInitialized(argv);
|
|
119
|
+
if (storageMode() === 'system') return redirectToSystemctl('stop', name);
|
|
120
|
+
const r = stopEngine(name);
|
|
121
|
+
if (!r.wasRunning) {
|
|
122
|
+
process.stdout.write(`'${name}' was not running.\n`);
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
if (r.killed) {
|
|
126
|
+
process.stdout.write(`'${name}' did not shut down within 5s — SIGKILL'd (pid ${r.pid}).\n`);
|
|
127
|
+
} else {
|
|
128
|
+
process.stdout.write(`'${name}' stopped (pid ${r.pid}).\n`);
|
|
129
|
+
}
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function cmdRestart(argv) {
|
|
134
|
+
const { name } = requireInitialized(argv);
|
|
135
|
+
if (storageMode() === 'system') return redirectToSystemctl('restart', name);
|
|
136
|
+
cmdStop(argv);
|
|
137
|
+
return cmdStart(argv);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function cmdStatus(argv) {
|
|
141
|
+
const { name } = requireInitialized(argv);
|
|
142
|
+
const probe = has(argv, '--probe');
|
|
143
|
+
const s = await engineStatus(name, { probeApi: probe });
|
|
144
|
+
if (!s.running) {
|
|
145
|
+
process.stdout.write(`'${name}' is not running.\n Start it: crosstalk server start${nameSuffix(name)}\n`);
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
148
|
+
const lines = [
|
|
149
|
+
`'${name}' is running.`,
|
|
150
|
+
` pid : ${s.pid}`,
|
|
151
|
+
` api : http://127.0.0.1:${s.port}`,
|
|
152
|
+
];
|
|
153
|
+
if (s.uptimeSeconds !== undefined) lines.push(` uptime : ${s.uptimeSeconds}s`);
|
|
154
|
+
if (probe) lines.push(` /healthz : ${s.apiReachable ? 'ok' : 'unreachable'}`);
|
|
155
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function cmdLogs(argv) {
|
|
160
|
+
const { paths } = requireInitialized(argv);
|
|
161
|
+
if (!existsSync(paths.logFile)) {
|
|
162
|
+
process.stdout.write(`No log file yet at ${paths.logFile}.\n (engine may not have been started)\n`);
|
|
163
|
+
return 0;
|
|
164
|
+
}
|
|
165
|
+
const follow = has(argv, '-f') || has(argv, '--follow');
|
|
166
|
+
const args = follow ? ['-n', '200', '-f', paths.logFile] : ['-n', '200', paths.logFile];
|
|
167
|
+
const r = spawn('tail', args, { stdio: 'inherit' });
|
|
168
|
+
return new Promise((resolve) => r.on('exit', (code) => resolve(code ?? 0)));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function run(argv) {
|
|
172
|
+
if (argv.length === 0 || has(argv, '-h') || has(argv, '--help')) usage(0);
|
|
173
|
+
const sub = argv[0];
|
|
174
|
+
const rest = argv.slice(1);
|
|
175
|
+
switch (sub) {
|
|
176
|
+
case 'start': return cmdStart(rest);
|
|
177
|
+
case 'stop': return cmdStop(rest);
|
|
178
|
+
case 'restart': return cmdRestart(rest);
|
|
179
|
+
case 'status': return cmdStatus(rest);
|
|
180
|
+
case 'logs': return cmdLogs(rest);
|
|
181
|
+
default:
|
|
182
|
+
process.stderr.write(`crosstalk server: unknown subverb '${sub}'\n\n`);
|
|
183
|
+
usage(1);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// crosstalk settings <show> — current engine settings snapshot.
|
|
2
|
+
|
|
3
|
+
import { apiFor } from '../lib/api-client.js';
|
|
4
|
+
import { reportAndExit } from '../lib/errors.js';
|
|
5
|
+
import { has } from '../lib/argv.js';
|
|
6
|
+
|
|
7
|
+
function usage(exit = 0) {
|
|
8
|
+
const w = exit === 0 ? process.stdout : process.stderr;
|
|
9
|
+
w.write(
|
|
10
|
+
`Usage:
|
|
11
|
+
crosstalk settings show [--json]
|
|
12
|
+
crosstalk settings --help
|
|
13
|
+
`,
|
|
14
|
+
);
|
|
15
|
+
process.exit(exit);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function run(argv) {
|
|
19
|
+
if (has(argv, '--help') || has(argv, '-h')) usage(0);
|
|
20
|
+
const sub = argv[0];
|
|
21
|
+
if (!sub) usage(1);
|
|
22
|
+
if (sub !== 'show') {
|
|
23
|
+
process.stderr.write(`crosstalk settings: unknown subcommand '${sub}'.\n`);
|
|
24
|
+
usage(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const api = apiFor(argv);
|
|
28
|
+
let r;
|
|
29
|
+
try {
|
|
30
|
+
r = await api.get('/api/settings');
|
|
31
|
+
} catch (err) {
|
|
32
|
+
reportAndExit(err, 'crosstalk settings show');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (has(argv, '--json')) {
|
|
36
|
+
process.stdout.write(JSON.stringify(r, null, 2) + '\n');
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
process.stdout.write(
|
|
41
|
+
`alias: ${r.alias}\n` +
|
|
42
|
+
`version: ${r.version}\n` +
|
|
43
|
+
`transportRoot: ${r.transportRoot}\n` +
|
|
44
|
+
`apiPort: ${r.apiPort}\n` +
|
|
45
|
+
`claimedModels: ${(r.claimedModels || []).join(', ') || '(none)'}\n` +
|
|
46
|
+
(r.pollSeconds != null ? `pollSeconds: ${r.pollSeconds}\n` : ''),
|
|
47
|
+
);
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
package/commands/status.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// crosstalk status —
|
|
1
|
+
// crosstalk status — transport-level snapshot from the engine, fetched via API.
|
|
2
2
|
//
|
|
3
3
|
// alpha.6: also prints the host path of the transport git repo as an
|
|
4
4
|
// escape hatch for operators who want to poke around with raw git.
|
|
@@ -25,22 +25,46 @@ export async function run(argv) {
|
|
|
25
25
|
process.stdout.write('\n');
|
|
26
26
|
|
|
27
27
|
process.stdout.write(`Claimed models: ${s.claimed_models.length}\n`);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
for (const m of s.claimed_models) process.stdout.write(` - ${m}\n`);
|
|
29
|
+
|
|
30
|
+
// N1 (alpha.11): surface the install/claim diagnostic regardless of
|
|
31
|
+
// claimed_models count. Alpha.10 only rendered when claimed was zero,
|
|
32
|
+
// which missed the common mixed-state case — operator already had one
|
|
33
|
+
// agent running and installed a second one that's referenced in yaml
|
|
34
|
+
// but not yet claimed. Now the hint fires whenever there's a gap
|
|
35
|
+
// between installed CLIs and claimed CLIs.
|
|
36
|
+
let installed = null;
|
|
37
|
+
let yamlReferenced = null;
|
|
38
|
+
let claimedClis = null;
|
|
39
|
+
try {
|
|
40
|
+
const r = await api.get('/agents/installed');
|
|
41
|
+
installed = r.installed;
|
|
42
|
+
yamlReferenced = r.yaml_referenced;
|
|
43
|
+
claimedClis = r.claimed;
|
|
44
|
+
} catch { /* ignore — best-effort diagnostic */ }
|
|
45
|
+
if (installed) {
|
|
46
|
+
const claimedSet = new Set(claimedClis ?? []);
|
|
47
|
+
const referencedSet = new Set(yamlReferenced ?? []);
|
|
48
|
+
const installedReferencedUnclaimed = installed.filter(
|
|
49
|
+
(b) => referencedSet.has(b) && !claimedSet.has(b),
|
|
50
|
+
);
|
|
51
|
+
const installedNotReferenced = installed.filter((b) => !referencedSet.has(b));
|
|
52
|
+
if (installedReferencedUnclaimed.length > 0) {
|
|
53
|
+
const verb = installedReferencedUnclaimed.length === 1 ? 'is' : 'are';
|
|
36
54
|
process.stdout.write(
|
|
37
|
-
` (${
|
|
55
|
+
` (${installedReferencedUnclaimed.join(', ')} ${verb} installed and referenced in data/crosstalk.yaml ` +
|
|
56
|
+
`but not yet claimed — run 'crosstalk restart' to refresh the dispatcher's claim list)\n`,
|
|
38
57
|
);
|
|
39
|
-
}
|
|
58
|
+
}
|
|
59
|
+
if (installedNotReferenced.length > 0) {
|
|
60
|
+
const verb = installedNotReferenced.length === 1 ? 'references it' : 'references them';
|
|
61
|
+
process.stdout.write(
|
|
62
|
+
` (${installedNotReferenced.join(', ')} installed in the container but no data/crosstalk.yaml entry ${verb} — add one to claim)\n`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
if (s.claimed_models.length === 0 && installed.length === 0) {
|
|
40
66
|
process.stdout.write(' (no model CLIs found inside the container — install one via `crosstalk chat --shell` and add an entry to data/crosstalk.yaml)\n');
|
|
41
67
|
}
|
|
42
|
-
} else {
|
|
43
|
-
for (const m of s.claimed_models) process.stdout.write(` - ${m}\n`);
|
|
44
68
|
}
|
|
45
69
|
process.stdout.write('\n');
|
|
46
70
|
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// crosstalk token <list|create|revoke|rename> — manage bearer tokens.
|
|
2
|
+
//
|
|
3
|
+
// All endpoints scoped to the currently-authenticated user (the engine
|
|
4
|
+
// enforces ownership; you cannot rename or revoke another user's
|
|
5
|
+
// tokens via the CLI). Admin-only operations on other users' tokens
|
|
6
|
+
// remain web-UI only.
|
|
7
|
+
|
|
8
|
+
import { apiFor } from '../lib/api-client.js';
|
|
9
|
+
import { positionals, has, flag } from '../lib/argv.js';
|
|
10
|
+
import { reportAndExit } from '../lib/errors.js';
|
|
11
|
+
|
|
12
|
+
function usage(exit = 0) {
|
|
13
|
+
const w = exit === 0 ? process.stdout : process.stderr;
|
|
14
|
+
w.write(
|
|
15
|
+
`Usage:
|
|
16
|
+
crosstalk token list # list your tokens
|
|
17
|
+
crosstalk token create <name> # create a new token (prints secret once)
|
|
18
|
+
crosstalk token revoke <tokenId> # revoke a token by id
|
|
19
|
+
crosstalk token rename <tokenId> --name <label> # rename a token's label
|
|
20
|
+
crosstalk token --help
|
|
21
|
+
`,
|
|
22
|
+
);
|
|
23
|
+
process.exit(exit);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function fmtRel(iso) {
|
|
27
|
+
if (!iso) return '—';
|
|
28
|
+
const ms = Date.now() - new Date(iso).getTime();
|
|
29
|
+
if (Number.isNaN(ms)) return iso;
|
|
30
|
+
if (ms < 0) return 'in the future';
|
|
31
|
+
if (ms < 5_000) return 'just now';
|
|
32
|
+
if (ms < 60_000) return `${Math.floor(ms / 1000)}s ago`;
|
|
33
|
+
if (ms < 3_600_000) return `${Math.floor(ms / 60_000)}m ago`;
|
|
34
|
+
if (ms < 86_400_000) return `${Math.floor(ms / 3_600_000)}h ago`;
|
|
35
|
+
return `${Math.floor(ms / 86_400_000)}d ago`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function run(argv) {
|
|
39
|
+
if (has(argv, '--help') || has(argv, '-h')) usage(0);
|
|
40
|
+
const pos = positionals(argv, ['--name', '--profile']);
|
|
41
|
+
const sub = pos[0];
|
|
42
|
+
if (!sub) usage(1);
|
|
43
|
+
|
|
44
|
+
const api = apiFor(argv);
|
|
45
|
+
|
|
46
|
+
if (sub === 'list') {
|
|
47
|
+
let r;
|
|
48
|
+
try {
|
|
49
|
+
r = await api.get('/api/tokens');
|
|
50
|
+
} catch (err) {
|
|
51
|
+
reportAndExit(err, 'crosstalk token list');
|
|
52
|
+
}
|
|
53
|
+
const tokens = r.tokens ?? [];
|
|
54
|
+
if (tokens.length === 0) {
|
|
55
|
+
process.stdout.write('(no tokens)\n');
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
for (const t of tokens) {
|
|
59
|
+
const created = fmtRel(t.createdAt);
|
|
60
|
+
const used = t.lastUsedAt ? `last ${fmtRel(t.lastUsedAt)}` : 'never used';
|
|
61
|
+
const expires = t.expiresAt ? ` exp:${t.expiresAt}` : '';
|
|
62
|
+
process.stdout.write(`${t.tokenId} ${(t.name || '').padEnd(24)} created ${created} ${used}${expires}\n`);
|
|
63
|
+
}
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (sub === 'create') {
|
|
68
|
+
const name = pos[1];
|
|
69
|
+
if (!name) {
|
|
70
|
+
process.stderr.write('crosstalk token create: <name> required.\n');
|
|
71
|
+
return 1;
|
|
72
|
+
}
|
|
73
|
+
let r;
|
|
74
|
+
try {
|
|
75
|
+
r = await api.post('/api/tokens', { name });
|
|
76
|
+
} catch (err) {
|
|
77
|
+
reportAndExit(err, 'crosstalk token create');
|
|
78
|
+
}
|
|
79
|
+
if (!r || typeof r.plaintextSecret !== 'string') {
|
|
80
|
+
process.stderr.write('crosstalk token create: malformed response from engine.\n');
|
|
81
|
+
return 1;
|
|
82
|
+
}
|
|
83
|
+
process.stdout.write(
|
|
84
|
+
`tokenId: ${r.tokenId}\n` +
|
|
85
|
+
`secret: ${r.plaintextSecret}\n` +
|
|
86
|
+
`\n` +
|
|
87
|
+
`Copy the secret now — it won't be shown again. To use it from\n` +
|
|
88
|
+
`another machine: send 'Authorization: Bearer <secret>' on each request.\n`,
|
|
89
|
+
);
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (sub === 'revoke') {
|
|
94
|
+
const tokenId = pos[1];
|
|
95
|
+
if (!tokenId) {
|
|
96
|
+
process.stderr.write('crosstalk token revoke: <tokenId> required. Run `crosstalk token list` to see ids.\n');
|
|
97
|
+
return 1;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
await api.delete(`/api/tokens/${encodeURIComponent(tokenId)}`);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
reportAndExit(err, 'crosstalk token revoke');
|
|
103
|
+
}
|
|
104
|
+
process.stdout.write(`revoked ${tokenId}\n`);
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (sub === 'rename') {
|
|
109
|
+
const tokenId = pos[1];
|
|
110
|
+
if (!tokenId) {
|
|
111
|
+
process.stderr.write('crosstalk token rename: <tokenId> required. Run `crosstalk token list` to see ids.\n');
|
|
112
|
+
return 1;
|
|
113
|
+
}
|
|
114
|
+
const newName = flag(argv, '--name');
|
|
115
|
+
if (!newName) {
|
|
116
|
+
process.stderr.write('crosstalk token rename: --name <label> required.\n');
|
|
117
|
+
return 1;
|
|
118
|
+
}
|
|
119
|
+
let r;
|
|
120
|
+
try {
|
|
121
|
+
r = await api.patch(`/api/tokens/${encodeURIComponent(tokenId)}`, { name: newName });
|
|
122
|
+
} catch (err) {
|
|
123
|
+
reportAndExit(err, 'crosstalk token rename');
|
|
124
|
+
}
|
|
125
|
+
const updated = r && r.token;
|
|
126
|
+
if (updated) {
|
|
127
|
+
process.stdout.write(`renamed ${tokenId} → ${updated.name}\n`);
|
|
128
|
+
} else {
|
|
129
|
+
process.stdout.write(`renamed ${tokenId}\n`);
|
|
130
|
+
}
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
process.stderr.write(`crosstalk token: unknown subcommand '${sub}'.\n`);
|
|
135
|
+
usage(1);
|
|
136
|
+
}
|