@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
package/bin/crosstalk.js
CHANGED
|
@@ -1,84 +1,118 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// crosstalk —
|
|
2
|
+
// crosstalk — single-binary CLI for the Crosstalk protocol.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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.
|
|
45
|
-
|
|
50
|
+
const w = exitCode === 0 ? process.stdout : process.stderr;
|
|
51
|
+
w.write(
|
|
52
|
+
`Usage: crosstalk <noun> <verb> [args...]
|
|
46
53
|
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
51
|
-
|
|
70
|
+
Global flags:
|
|
71
|
+
--containername <name> | -c <name> target a named transport (default: 'crosstalk')
|
|
72
|
+
--help | -h print this help
|
|
52
73
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
62
|
-
|
|
63
|
-
if (argv[0] === '--version') {
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
|
|
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
|
|
70
|
-
const
|
|
71
|
-
if (!
|
|
72
|
-
process.stderr.write(`crosstalk: unknown
|
|
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
|
|
77
|
-
if (!existsSync(
|
|
78
|
-
process.stderr.write(`crosstalk:
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
+
}
|
package/commands/auth.js
ADDED
|
@@ -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
|
+
}
|
package/commands/channel.js
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
|
-
// crosstalk channel <
|
|
1
|
+
// crosstalk channel <list|create|rename|delete> — channel management.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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
|
|
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>
|
|
17
|
-
crosstalk channel <
|
|
18
|
-
crosstalk channel <
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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:
|
|
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 ${
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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
|
}
|