@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.
Files changed (72) hide show
  1. package/GUIDE-CLI.md +322 -0
  2. package/GUIDE-PROMPTS.md +118 -0
  3. package/LICENSE +21 -0
  4. package/README.md +422 -61
  5. package/bin/crosstalk.js +88 -54
  6. package/commands/agent.js +69 -0
  7. package/commands/auth.js +273 -0
  8. package/commands/channel.js +54 -44
  9. package/commands/chat.js +107 -71
  10. package/commands/daemon.js +120 -0
  11. package/commands/logs.js +108 -19
  12. package/commands/message.js +125 -0
  13. package/commands/server.js +185 -0
  14. package/commands/settings.js +49 -0
  15. package/commands/status.js +37 -13
  16. package/commands/token.js +136 -0
  17. package/commands/transport.js +299 -0
  18. package/commands/version.js +3 -3
  19. package/commands/workflow.js +234 -0
  20. package/deploy/crosstalk@.service +62 -0
  21. package/deploy/install.sh +82 -0
  22. package/lib/api-client.js +77 -22
  23. package/lib/credentials.js +207 -0
  24. package/lib/nativeServer.js +182 -0
  25. package/lib/resolve.js +106 -37
  26. package/package.json +27 -4
  27. package/src/activation.ts +104 -0
  28. package/src/api.ts +1717 -0
  29. package/src/auth/enforce.ts +68 -0
  30. package/src/auth/handlers.ts +266 -0
  31. package/src/auth/middleware.ts +132 -0
  32. package/src/auth/setup.ts +263 -0
  33. package/src/auth/tokens.ts +285 -0
  34. package/src/auth/users.ts +267 -0
  35. package/src/dispatch.ts +530 -0
  36. package/src/dispatchers.ts +91 -0
  37. package/src/filenames.ts +28 -0
  38. package/src/frontmatter.ts +26 -0
  39. package/src/init.ts +116 -0
  40. package/src/invoke.ts +201 -0
  41. package/src/log-buffer.ts +67 -0
  42. package/src/models.ts +300 -0
  43. package/src/resolve.ts +100 -0
  44. package/src/state.ts +191 -0
  45. package/src/stop.ts +37 -0
  46. package/src/transport.ts +353 -0
  47. package/src/web/auth-pages.ts +160 -0
  48. package/src/web/channels.ts +395 -0
  49. package/src/web/chat-page.ts +636 -0
  50. package/src/web/chat-pty.ts +254 -0
  51. package/src/web/dashboard.ts +130 -0
  52. package/src/web/layout.ts +237 -0
  53. package/src/web/stubs.ts +510 -0
  54. package/src/web/workflows.ts +490 -0
  55. package/src/workflow.ts +470 -0
  56. package/template/CLAUDE.md +10 -0
  57. package/template/CROSSTALK-VERSION +1 -0
  58. package/template/CROSSTALK.md +262 -0
  59. package/template/PROTOCOL.md +70 -0
  60. package/template/README.md +64 -0
  61. package/template/auth/.gitkeep +0 -0
  62. package/template/auth/README.md +224 -0
  63. package/template/data/crosstalk.yaml +196 -0
  64. package/template/gitignore +4 -0
  65. package/commands/down.js +0 -40
  66. package/commands/init.js +0 -243
  67. package/commands/pull.js +0 -22
  68. package/commands/replies.js +0 -40
  69. package/commands/restart.js +0 -29
  70. package/commands/rm.js +0 -109
  71. package/commands/run.js +0 -115
  72. package/commands/up.js +0 -135
package/bin/crosstalk.js CHANGED
@@ -1,84 +1,118 @@
1
1
  #!/usr/bin/env node
2
- // crosstalk — host-side client for the crosstalkd daemon.
2
+ // crosstalk — single-binary CLI for the Crosstalk protocol.
3
3
  //
4
- // Thin subcommand dispatcher. Routes operator commands to either:
5
- // - the engine's HTTP API on 127.0.0.1:<port> (protocol commands)
6
- // - the local docker daemon (lifecycle: up, down, restart, pull, logs)
7
- // - `docker exec -it <container>` via a PTY wrapper (chat, shell)
4
+ // v8-monorepo: the daemon is an internal subcommand of this CLI.
5
+ // `crosstalk daemon dispatch ...` is what `crosstalk server start`
6
+ // spawns under the hood.
8
7
  //
9
- // Subcommands and their groups:
10
- //
11
- // Protocol (HTTP API): run, replies, status, channel, init
12
- // Lifecycle (docker): up, down, restart, pull, logs
13
- // Interactive (pty): chat, shell
14
- // Observability: version
15
- //
16
- // P3 only ships `version` as a proof-of-life. Other subcommands land
17
- // in P4 (protocol), P5 (lifecycle), P6 (interactive).
8
+ // Strict noun-verb shape, mirroring @cordfuse/llmux where the concepts
9
+ // overlap. See V8-CLI-ALIGN.md for the full surface + design rationale.
18
10
 
19
11
  import { dirname, join } from 'path';
20
12
  import { fileURLToPath } from 'url';
21
- import { existsSync } from 'fs';
13
+ import { existsSync, readFileSync } from 'fs';
22
14
 
23
- const SUBCOMMANDS = {
24
- // each entry is the file in commands/ to load
15
+ // Each entry is a file in commands/ that exports an async `run(argv)`.
16
+ // The noun handler receives the full argv MINUS the noun itself; it's
17
+ // responsible for dispatching its own verbs.
18
+ const NOUNS = {
19
+ // Meta — no verb required
25
20
  version: 'version.js',
26
- run: 'run.js',
27
- replies: 'replies.js',
28
- status: 'status.js',
29
- channel: 'channel.js',
30
- init: 'init.js',
31
- up: 'up.js',
32
- down: 'down.js',
33
- rm: 'rm.js',
34
- restart: 'restart.js',
35
- pull: 'pull.js',
36
- logs: 'logs.js',
37
- chat: 'chat.js',
21
+
22
+ // Auth
23
+ auth: 'auth.js', // login | logout | whoami | list | use
24
+ token: 'token.js', // create | list | revoke | rename
25
+
26
+ // Lifecycle + transport
27
+ server: 'server.js', // start | stop | restart | status | logs
28
+ transport:'transport.js',// init | rm
29
+ daemon: 'daemon.js', // dispatch | init | stop (internal — spawned by server start + systemd)
30
+
31
+ // Protocol surface
32
+ channel: 'channel.js', // list | create | rename | delete
33
+ message: 'message.js', // send | replies
34
+ workflow: 'workflow.js', // compose | run | status
35
+
36
+ // Observability
37
+ agent: 'agent.js', // list
38
+ logs: 'logs.js', // list | tail
39
+ settings: 'settings.js', // show
40
+ status: 'status.js', // (no verb — transport-level snapshot)
41
+
42
+ // Interactive PTY
43
+ chat: 'chat.js', // <agent> positional | --shell
38
44
  };
39
45
 
40
46
  const argv = process.argv.slice(2);
41
47
  const thisDir = dirname(fileURLToPath(import.meta.url));
42
48
 
43
49
  function printUsage(exitCode = 0) {
44
- process.stdout.write(
45
- `Usage: crosstalk <subcommand> [args...]
50
+ const w = exitCode === 0 ? process.stdout : process.stderr;
51
+ w.write(
52
+ `Usage: crosstalk <noun> <verb> [args...]
46
53
 
47
- The host-side client for crosstalkd. Most commands talk to the engine's
48
- HTTP API on 127.0.0.1; lifecycle commands talk to the local docker daemon.
54
+ Nouns:
55
+ version meta
56
+ auth login|logout|whoami|list|use credentials
57
+ token create|list|revoke|rename bearer tokens
58
+ server start|stop|restart|status|logs daemon lifecycle
59
+ transport init|rm transport scaffold + teardown
60
+ daemon dispatch|init internal — spawned by 'server start'
61
+ channel list|create|rename|delete bus channels
62
+ message send|replies send a primitive or check replies
63
+ workflow compose|run|status workflow runtime
64
+ agent list installed agent CLI catalog
65
+ logs list|tail engine event log
66
+ settings show engine config snapshot
67
+ status transport-level snapshot
68
+ chat <agent>|--shell interactive PTY
49
69
 
50
- Subcommands:
51
- ${Object.keys(SUBCOMMANDS).map((s) => ` ${s}`).join('\n')}
70
+ Global flags:
71
+ --containername <name> | -c <name> target a named transport (default: 'crosstalk')
72
+ --help | -h print this help
52
73
 
53
- If your engine listens on a non-default port, set CROSSTALK_API_PORT.
54
-
55
- v7.0.0-dev many subcommands are not implemented yet (see bin/crosstalk.js).
74
+ Env:
75
+ CROSSTALK_USER_MODE=1 run engine + state under $HOME (user mode)
76
+ CROSSTALK_API_PORT=<port> override the engine HTTP port
56
77
  `,
57
78
  );
58
79
  process.exit(exitCode);
59
80
  }
60
81
 
61
- if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') printUsage(0);
62
-
63
- if (argv[0] === '--version') {
64
- // delegate to the version command so the engine-side number can also
65
- // be queried in a uniform place
66
- argv[0] = 'version';
82
+ if (argv.length === 0) printUsage(1);
83
+ if (argv[0] === '--help' || argv[0] === '-h') printUsage(0);
84
+ if (argv[0] === '--version' || argv[0] === '-v') {
85
+ // Short-circuit version to avoid loading the noun module for this hot path.
86
+ // (commands/version.js still handles `crosstalk version` for the engine comparison.)
87
+ const pkg = JSON.parse(readFileSync(join(thisDir, '..', 'package.json'), 'utf-8'));
88
+ process.stdout.write(`crosstalk ${pkg.version}\n`);
89
+ process.exit(0);
67
90
  }
68
91
 
69
- const cmd = argv[0];
70
- const file = SUBCOMMANDS[cmd];
71
- if (!file) {
72
- process.stderr.write(`crosstalk: unknown subcommand '${cmd}'\n\n`);
92
+ const noun = argv[0];
93
+ const nounFile = NOUNS[noun];
94
+ if (!nounFile) {
95
+ process.stderr.write(`crosstalk: unknown noun '${noun}'\n\n`);
73
96
  printUsage(1);
74
97
  }
75
98
 
76
- const modulePath = join(thisDir, '..', 'commands', file);
77
- if (!existsSync(modulePath)) {
78
- process.stderr.write(`crosstalk: command file missing for '${cmd}' (expected ${modulePath})\n`);
99
+ const handlerPath = join(thisDir, '..', 'commands', nounFile);
100
+ if (!existsSync(handlerPath)) {
101
+ process.stderr.write(`crosstalk: handler missing ${handlerPath}\n`);
102
+ process.exit(2);
103
+ }
104
+
105
+ const mod = await import(handlerPath);
106
+ if (typeof mod.run !== 'function') {
107
+ process.stderr.write(`crosstalk: ${nounFile} does not export run()\n`);
79
108
  process.exit(2);
80
109
  }
81
110
 
82
- const mod = await import(modulePath);
83
- const exit = await mod.run(argv.slice(1));
84
- process.exit(typeof exit === 'number' ? exit : 0);
111
+ // Hand the noun-stripped argv to the handler. It dispatches its own verbs.
112
+ try {
113
+ const code = await mod.run(argv.slice(1));
114
+ process.exit(typeof code === 'number' ? code : 0);
115
+ } catch (err) {
116
+ process.stderr.write(`crosstalk ${noun}: ${err?.message || err}\n`);
117
+ process.exit(1);
118
+ }
@@ -0,0 +1,69 @@
1
+ // crosstalk agent <list> — installed CLI agents on the host.
2
+ //
3
+ // Mirrors llmux's `agent list`. Talks to /agents/installed on the
4
+ // engine, which is responsible for actually probing PATH; the CLI just
5
+ // presents the result.
6
+
7
+ import { apiFor } from '../lib/api-client.js';
8
+ import { reportAndExit } from '../lib/errors.js';
9
+ import { has } from '../lib/argv.js';
10
+
11
+ function usage(exit = 0) {
12
+ const w = exit === 0 ? process.stdout : process.stderr;
13
+ w.write(
14
+ `Usage:
15
+ crosstalk agent list [--installed] [--json]
16
+ crosstalk agent --help
17
+
18
+ --installed Only show CLIs found on PATH (default: show all known
19
+ agents, marking which are installed/claimed/yaml-known).
20
+ --json Print raw JSON instead of human-readable lines.
21
+ `,
22
+ );
23
+ process.exit(exit);
24
+ }
25
+
26
+ export async function run(argv) {
27
+ if (has(argv, '--help') || has(argv, '-h')) usage(0);
28
+ const sub = argv[0];
29
+ if (!sub) usage(1);
30
+
31
+ if (sub !== 'list') {
32
+ process.stderr.write(`crosstalk agent: unknown subcommand '${sub}'.\n`);
33
+ usage(1);
34
+ }
35
+
36
+ const api = apiFor(argv);
37
+ let r;
38
+ try {
39
+ r = await api.get('/agents/installed');
40
+ } catch (err) {
41
+ reportAndExit(err, 'crosstalk agent list');
42
+ }
43
+
44
+ if (has(argv, '--json')) {
45
+ process.stdout.write(JSON.stringify(r, null, 2) + '\n');
46
+ return 0;
47
+ }
48
+
49
+ const installed = new Set(r.installed || []);
50
+ const yamlRef = new Set(r.yaml_referenced || []);
51
+ const claimed = new Set(r.claimed || []);
52
+ const onlyInstalled = has(argv, '--installed');
53
+ const universe = onlyInstalled ? [...installed] : (r.known || []);
54
+
55
+ if (universe.length === 0) {
56
+ process.stdout.write('(no agents)\n');
57
+ return 0;
58
+ }
59
+
60
+ for (const name of universe) {
61
+ const flags = [
62
+ installed.has(name) ? 'installed' : '-',
63
+ yamlRef.has(name) ? 'yaml' : '-',
64
+ claimed.has(name) ? 'claimed' : '-',
65
+ ].join(' ');
66
+ process.stdout.write(` ${name.padEnd(12)} ${flags}\n`);
67
+ }
68
+ return 0;
69
+ }
@@ -0,0 +1,273 @@
1
+ // crosstalk auth <login|logout|whoami|list|use> — CLI-side authentication.
2
+ //
3
+ // Multi-profile (v8 alpha.18): a single CLI can hold credentials for
4
+ // multiple engines (local loopback, hosted prod, etc.) in
5
+ // ~/.config/crosstalk/credentials.json. The active profile drives
6
+ // outbound calls; `--profile <name>` overrides per-invocation.
7
+ //
8
+ // Verbs:
9
+ // login — POST /api/auth/login → bearer token; saves new profile
10
+ // logout — POST /api/auth/logout (best-effort) + delete profile locally
11
+ // whoami — GET /api/account on the active profile
12
+ // list — show all saved profiles + which is active
13
+ // use — mark a saved profile as active
14
+
15
+ import { apiFor, ApiError } from '../lib/api-client.js';
16
+ import {
17
+ saveProfile,
18
+ setActiveProfile,
19
+ deleteActiveProfile,
20
+ getActiveProfile,
21
+ listProfiles,
22
+ credentialsFilePath,
23
+ } from '../lib/credentials.js';
24
+ import { positionals, has, flag } from '../lib/argv.js';
25
+ import { reportAndExit } from '../lib/errors.js';
26
+
27
+ function usage(exit = 0) {
28
+ const w = exit === 0 ? process.stdout : process.stderr;
29
+ w.write(
30
+ `Usage:
31
+ crosstalk auth login [--server <url>] [--username <name>] [--passphrase <pp>] [--profile <name>]
32
+ crosstalk auth logout [--profile <name>] [--no-revoke]
33
+ crosstalk auth whoami [--profile <name>]
34
+ crosstalk auth list
35
+ crosstalk auth use <profile>
36
+ crosstalk auth --help
37
+
38
+ Interactive: --username and --passphrase are prompted when omitted.
39
+ Credentials stored at: ${credentialsFilePath()}
40
+ `,
41
+ );
42
+ process.exit(exit);
43
+ }
44
+
45
+ // Control bytes the raw-mode passphrase reader handles.
46
+ const CTRL_C = String.fromCharCode(3);
47
+ const BS = String.fromCharCode(127);
48
+ const BS_ALT = String.fromCharCode(8);
49
+
50
+ /** Read a single line from stdin with no echo (for passphrase entry). */
51
+ function promptHidden(label) {
52
+ return new Promise((resolve, reject) => {
53
+ if (!process.stdin.isTTY) {
54
+ let data = '';
55
+ process.stdin.setEncoding('utf-8');
56
+ process.stdin.on('data', (chunk) => { data += chunk; });
57
+ process.stdin.once('end', () => resolve(data.replace(/\r?\n$/, '')));
58
+ process.stdin.once('error', reject);
59
+ return;
60
+ }
61
+ process.stdout.write(label);
62
+ const stdin = process.stdin;
63
+ const wasRaw = stdin.isRaw;
64
+ stdin.setRawMode(true);
65
+ stdin.resume();
66
+ stdin.setEncoding('utf-8');
67
+ let buf = '';
68
+ const onData = (ch) => {
69
+ if (ch === CTRL_C) {
70
+ stdin.setRawMode(wasRaw);
71
+ stdin.pause();
72
+ stdin.removeListener('data', onData);
73
+ process.stdout.write('\n');
74
+ reject(new Error('aborted'));
75
+ return;
76
+ }
77
+ if (ch === '\r' || ch === '\n') {
78
+ stdin.setRawMode(wasRaw);
79
+ stdin.pause();
80
+ stdin.removeListener('data', onData);
81
+ process.stdout.write('\n');
82
+ resolve(buf);
83
+ return;
84
+ }
85
+ if (ch === BS || ch === BS_ALT) {
86
+ if (buf.length > 0) buf = buf.slice(0, -1);
87
+ return;
88
+ }
89
+ buf += ch;
90
+ };
91
+ stdin.on('data', onData);
92
+ });
93
+ }
94
+
95
+ function promptLine(label) {
96
+ return new Promise((resolve, reject) => {
97
+ process.stdout.write(label);
98
+ const stdin = process.stdin;
99
+ stdin.setEncoding('utf-8');
100
+ stdin.resume();
101
+ let buf = '';
102
+ const onData = (chunk) => {
103
+ buf += chunk;
104
+ const nl = buf.indexOf('\n');
105
+ if (nl !== -1) {
106
+ stdin.removeListener('data', onData);
107
+ stdin.pause();
108
+ resolve(buf.slice(0, nl).replace(/\r$/, ''));
109
+ }
110
+ };
111
+ stdin.on('data', onData);
112
+ stdin.once('error', reject);
113
+ });
114
+ }
115
+
116
+ export async function run(argv) {
117
+ if (has(argv, '--help') || has(argv, '-h')) usage(0);
118
+ const pos = positionals(argv, [
119
+ '--username', '--passphrase', '--server', '--profile',
120
+ ]);
121
+ const sub = pos[0];
122
+ if (!sub) usage(1);
123
+
124
+ if (sub === 'login') return await runLogin(argv);
125
+ if (sub === 'logout') return await runLogout(argv);
126
+ if (sub === 'whoami') return await runWhoami(argv);
127
+ if (sub === 'list') return runList();
128
+ if (sub === 'use') return runUse(pos[1]);
129
+
130
+ process.stderr.write(`crosstalk auth: unknown subcommand '${sub}'.\n`);
131
+ usage(1);
132
+ }
133
+
134
+ async function runLogin(argv) {
135
+ const server = flag(argv, '--server') || 'http://127.0.0.1:7000';
136
+ const profileName = flag(argv, '--profile') || 'default';
137
+ // serverOverride: the explicit --server (or default) wins over any
138
+ // existing active profile. Otherwise a re-login that targets a new
139
+ // engine would silently hit the old profile's URL.
140
+ const api = apiFor(argv, { serverOverride: server });
141
+ let username = flag(argv, '--username');
142
+ let passphrase = flag(argv, '--passphrase');
143
+ try {
144
+ if (!username) username = (await promptLine('username: ')).trim();
145
+ if (!passphrase) passphrase = await promptHidden('passphrase: ');
146
+ } catch (err) {
147
+ process.stderr.write(`crosstalk auth login: ${err.message}\n`);
148
+ return 1;
149
+ }
150
+ if (!username || !passphrase) {
151
+ process.stderr.write('crosstalk auth login: username + passphrase required.\n');
152
+ return 1;
153
+ }
154
+ let r;
155
+ try {
156
+ r = await api.postNoAuth('/api/auth/login', { username, passphrase });
157
+ } catch (err) {
158
+ if (err instanceof ApiError && err.status === 503) {
159
+ process.stderr.write(
160
+ 'crosstalk auth login: setup needed.\n' +
161
+ 'The engine has no admin user yet. Visit /setup in a browser using\n' +
162
+ 'the URL printed at engine boot, then run `crosstalk auth login` again.\n',
163
+ );
164
+ return 1;
165
+ }
166
+ reportAndExit(err, 'crosstalk auth login');
167
+ }
168
+ if (!r || typeof r.token !== 'string') {
169
+ process.stderr.write('crosstalk auth login: malformed response from engine.\n');
170
+ return 1;
171
+ }
172
+ // sas_<tokenId>.<secret> — pull the tokenId out for display + future
173
+ // revoke. Fallback: store null tokenId if the format ever changes.
174
+ const tokenId = parseTokenId(r.token);
175
+ saveProfile(profileName, {
176
+ server,
177
+ username,
178
+ token: r.token,
179
+ tokenId,
180
+ });
181
+ setActiveProfile(profileName);
182
+ process.stdout.write(
183
+ `logged in as ${username} (profile '${profileName}' → ${server})\n` +
184
+ ` credentials stored at ${credentialsFilePath()}\n`,
185
+ );
186
+ return 0;
187
+ }
188
+
189
+ function parseTokenId(token) {
190
+ // Wire format: sas_<id>.<secret>
191
+ const m = /^sas_([^.]+)\./.exec(token);
192
+ return m ? m[1] : null;
193
+ }
194
+
195
+ async function runLogout(argv) {
196
+ const profile = getActiveProfile(argv);
197
+ if (!profile) {
198
+ process.stdout.write('not logged in.\n');
199
+ return 0;
200
+ }
201
+ if (!has(argv, '--no-revoke')) {
202
+ const api = apiFor(argv);
203
+ try {
204
+ await api.post('/api/auth/logout');
205
+ } catch (_err) {
206
+ // Best-effort. Even if the server rejects (token already
207
+ // revoked elsewhere, network gone), still clean up locally.
208
+ }
209
+ }
210
+ const removed = deleteActiveProfile();
211
+ if (removed) {
212
+ process.stdout.write(`logged out (profile '${removed}' removed)\n`);
213
+ } else {
214
+ process.stdout.write('logged out\n');
215
+ }
216
+ return 0;
217
+ }
218
+
219
+ async function runWhoami(argv) {
220
+ if (!getActiveProfile(argv)) {
221
+ process.stderr.write("not logged in. Run 'crosstalk auth login'.\n");
222
+ return 1;
223
+ }
224
+ const api = apiFor(argv);
225
+ let r;
226
+ try {
227
+ r = await api.get('/api/account');
228
+ } catch (err) {
229
+ reportAndExit(err, 'crosstalk auth whoami');
230
+ }
231
+ process.stdout.write(
232
+ `username: ${r.username}\n` +
233
+ `name: ${r.name}\n` +
234
+ `role: ${r.admin ? 'admin' : 'member'}\n` +
235
+ `created: ${r.createdAt}\n`,
236
+ );
237
+ return 0;
238
+ }
239
+
240
+ function runList() {
241
+ const profiles = listProfiles();
242
+ if (profiles.length === 0) {
243
+ process.stdout.write('no profiles. Run `crosstalk auth login` to create one.\n');
244
+ return 0;
245
+ }
246
+ // Compact tabular: active marker, name, username, server.
247
+ const nameW = Math.max(7, ...profiles.map((p) => p.name.length));
248
+ const userW = Math.max(8, ...profiles.map((p) => (p.username || '-').length));
249
+ const header = ` ${'PROFILE'.padEnd(nameW)} ${'USERNAME'.padEnd(userW)} SERVER`;
250
+ process.stdout.write(`${header}\n`);
251
+ for (const p of profiles) {
252
+ const marker = p.active ? '*' : ' ';
253
+ process.stdout.write(
254
+ `${marker} ${p.name.padEnd(nameW)} ${(p.username || '-').padEnd(userW)} ${p.server}\n`,
255
+ );
256
+ }
257
+ return 0;
258
+ }
259
+
260
+ function runUse(name) {
261
+ if (!name) {
262
+ process.stderr.write('crosstalk auth use: profile name required.\n');
263
+ return 1;
264
+ }
265
+ try {
266
+ setActiveProfile(name);
267
+ } catch (err) {
268
+ process.stderr.write(`crosstalk auth use: ${err.message}\n`);
269
+ return 1;
270
+ }
271
+ process.stdout.write(`active profile: ${name}\n`);
272
+ return 0;
273
+ }
@@ -1,22 +1,24 @@
1
- // crosstalk channel <name> — create a channel via POST /channels.
1
+ // crosstalk channel <list|create|rename|delete> — channel management.
2
2
  //
3
- // P4 only ships create. --rename and --delete need PATCH/DELETE
4
- // endpoints that haven't been added to the engine yet; they land
5
- // when the engine gains them.
3
+ // alpha.18 noun-verb shape: every operation has an explicit verb.
4
+ // The old flag-form (`channel <handle> --rename <new>`, `--delete`,
5
+ // bare `channel <name>` create) is gone — clean break, no aliases.
6
6
 
7
7
  import { apiFor } from '../lib/api-client.js';
8
8
  import { reportAndExit } from '../lib/errors.js';
9
- import { positionals, has, flag } from '../lib/argv.js';
9
+ import { positionals, has } from '../lib/argv.js';
10
10
 
11
11
  function usage(exit = 0) {
12
12
  const w = exit === 0 ? process.stdout : process.stderr;
13
13
  w.write(
14
14
  `Usage:
15
15
  crosstalk channel list # list channels
16
- crosstalk channel <name> # create a channel
17
- crosstalk channel <name-or-uuid> --rename <new> # rename
18
- crosstalk channel <name-or-uuid> --delete # delete (with confirmation)
16
+ crosstalk channel create <name> # create a channel
17
+ crosstalk channel rename <handle> <new-name> # rename
18
+ crosstalk channel delete <handle> # delete
19
19
  crosstalk channel --help
20
+
21
+ <handle> is either the channel name or its UUID.
20
22
  `,
21
23
  );
22
24
  process.exit(exit);
@@ -24,13 +26,15 @@ function usage(exit = 0) {
24
26
 
25
27
  export async function run(argv) {
26
28
  if (has(argv, '--help') || has(argv, '-h')) usage(0);
29
+ const pos = positionals(argv, [
30
+ '--containername', '-c', '--profile', '--server',
31
+ ]);
32
+ const sub = pos[0];
33
+ if (!sub) usage(1);
27
34
 
28
35
  const api = apiFor(argv);
29
- const pos = positionals(argv, ['--rename', '--containername', '-c']);
30
- if (pos.length === 0) usage(1);
31
36
 
32
- // `crosstalk channel list` → list channels
33
- if (pos[0] === 'list') {
37
+ if (sub === 'list') {
34
38
  let r;
35
39
  try {
36
40
  r = await api.get('/channels');
@@ -49,47 +53,61 @@ export async function run(argv) {
49
53
  return 0;
50
54
  }
51
55
 
52
- const handle = pos[0];
53
- const renameTo = flag(argv, '--rename');
54
- const wantDelete = has(argv, '--delete');
55
-
56
- if (renameTo && wantDelete) {
57
- process.stderr.write('crosstalk channel: --rename and --delete are mutually exclusive.\n');
58
- return 1;
56
+ if (sub === 'create') {
57
+ const name = pos[1];
58
+ if (!name) {
59
+ process.stderr.write('crosstalk channel create: <name> required.\n');
60
+ return 1;
61
+ }
62
+ let r;
63
+ try {
64
+ r = await api.post('/channels', { name });
65
+ } catch (err) {
66
+ reportAndExit(err, 'crosstalk channel create');
67
+ }
68
+ process.stdout.write(`Created channel: ${r.uuid}\n`);
69
+ process.stdout.write(` name: ${r.name}\n`);
70
+ return 0;
59
71
  }
60
72
 
61
- // Rename: PATCH /channels/<handle> { name: <new> }
62
- if (renameTo) {
73
+ if (sub === 'rename') {
74
+ const handle = pos[1];
75
+ const newName = pos[2];
76
+ if (!handle || !newName) {
77
+ process.stderr.write('crosstalk channel rename: <handle> <new-name> required.\n');
78
+ return 1;
79
+ }
63
80
  let r;
64
81
  try {
65
- r = await api.patch(`/channels/${encodeURIComponent(handle)}`, { name: renameTo });
82
+ r = await api.patch(`/channels/${encodeURIComponent(handle)}`, { name: newName });
66
83
  } catch (err) {
67
- reportAndExit(err, 'crosstalk channel');
84
+ reportAndExit(err, 'crosstalk channel rename');
68
85
  }
69
86
  if (r.noop) {
70
- process.stdout.write(`No-op: ${handle} already named ${renameTo}.\n`);
87
+ process.stdout.write(`No-op: ${handle} already named ${newName}.\n`);
71
88
  return 0;
72
89
  }
73
90
  process.stdout.write(`Renamed: ${handle} -> ${r.name} (${r.uuid})\n`);
74
91
  return 0;
75
92
  }
76
93
 
77
- // Delete: DELETE /channels/<handle>?confirm=<name>
78
- // Engine returns 400 if confirm is missing/wrong, with the expected
79
- // value in the error message. Client reads the channel's name first
80
- // and passes it as confirm — operator's explicit --delete flag IS the
81
- // confirmation for now. Interactive prompt deferred (would block on
82
- // stdin in scripts).
83
- if (wantDelete) {
94
+ if (sub === 'delete') {
95
+ const handle = pos[1];
96
+ if (!handle) {
97
+ process.stderr.write('crosstalk channel delete: <handle> required.\n');
98
+ return 1;
99
+ }
100
+ // Engine requires ?confirm=<name>. We resolve the channel's current
101
+ // name first so the verb itself is the operator's explicit consent.
84
102
  let target;
85
103
  try {
86
104
  const all = await api.get('/channels');
87
105
  target = all.channels.find((c) => c.uuid === handle || c.name === handle);
88
106
  } catch (err) {
89
- reportAndExit(err, 'crosstalk channel');
107
+ reportAndExit(err, 'crosstalk channel delete');
90
108
  }
91
109
  if (!target) {
92
- process.stderr.write(`crosstalk channel: '${handle}' not found.\n`);
110
+ process.stderr.write(`crosstalk channel delete: '${handle}' not found.\n`);
93
111
  return 1;
94
112
  }
95
113
  const confirm = target.name ?? target.uuid;
@@ -99,20 +117,12 @@ export async function run(argv) {
99
117
  `/channels/${encodeURIComponent(handle)}?confirm=${encodeURIComponent(confirm)}`,
100
118
  );
101
119
  } catch (err) {
102
- reportAndExit(err, 'crosstalk channel');
120
+ reportAndExit(err, 'crosstalk channel delete');
103
121
  }
104
122
  process.stdout.write(`Deleted channel: ${r.name ?? '(unnamed)'} (${r.uuid})\n`);
105
123
  return 0;
106
124
  }
107
125
 
108
- // No flags → create a channel with the given name.
109
- let r;
110
- try {
111
- r = await api.post('/channels', { name: handle });
112
- } catch (err) {
113
- reportAndExit(err, 'crosstalk channel');
114
- }
115
- process.stdout.write(`Created channel: ${r.uuid}\n`);
116
- process.stdout.write(` name: ${r.name}\n`);
117
- return 0;
126
+ process.stderr.write(`crosstalk channel: unknown subcommand '${sub}'.\n`);
127
+ usage(1);
118
128
  }