@cordfuse/crosstalk 6.0.0-alpha.9 → 7.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/README.md +26 -0
- package/bin/crosstalk.js +60 -74
- package/commands/channel.js +69 -0
- package/commands/chat.js +159 -0
- package/commands/down.js +37 -0
- package/commands/init.js +105 -0
- package/commands/logs.js +39 -0
- package/commands/pull.js +24 -0
- package/commands/replies.js +39 -0
- package/commands/restart.js +24 -0
- package/commands/run.js +121 -0
- package/commands/status.js +52 -0
- package/commands/up.js +129 -0
- package/commands/version.js +30 -0
- package/lib/api-client.js +80 -0
- package/lib/argv.js +28 -0
- package/lib/errors.js +19 -0
- package/lib/transport.js +51 -0
- package/package.json +5 -21
- package/src/activation.ts +0 -104
- package/src/actor.ts +0 -131
- package/src/attach.ts +0 -118
- package/src/channel.ts +0 -49
- package/src/chat.ts +0 -142
- package/src/dispatch.ts +0 -531
- package/src/dlq.ts +0 -216
- package/src/filenames.ts +0 -28
- package/src/frontmatter.ts +0 -26
- package/src/init.ts +0 -138
- package/src/open.ts +0 -207
- package/src/replies.ts +0 -59
- package/src/send.ts +0 -122
- package/src/state.ts +0 -173
- package/src/status.ts +0 -75
- package/src/stop.ts +0 -37
- package/src/transport.ts +0 -213
- package/src/turnq.ts +0 -91
- package/src/upgrade.ts +0 -211
- package/src/wake.ts +0 -7
- package/template/CLAUDE.md +0 -12
- package/template/gitignore +0 -4
- package/template/upstream/CROSSTALK-VERSION +0 -1
- package/template/upstream/CROSSTALK.md +0 -298
- package/template/upstream/OPERATOR.md +0 -60
- package/template/upstream/PROTOCOL.md +0 -80
- package/template/upstream/actors/concierge.md +0 -36
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# @cordfuse/crosstalk — host client
|
|
2
|
+
|
|
3
|
+
> **README is a stub.** Full operator-facing docs land in the docs-consolidation pass. The body below is intentionally short; it covers what's actually implemented today.
|
|
4
|
+
|
|
5
|
+
The host-side CLI for the Crosstalk protocol. Pairs with `@cordfuse/crosstalkd`, the daemon that runs inside the `crosstalk-server` container.
|
|
6
|
+
|
|
7
|
+
- Operator installs this package: `npm install -g @cordfuse/crosstalk`
|
|
8
|
+
- Operator runs `docker compose up -d` to bring up the `crosstalk-server` container (which runs `crosstalkd`)
|
|
9
|
+
- Operator runs `crosstalk <subcommand>` — the client talks to the daemon's HTTP API on `127.0.0.1:7000`
|
|
10
|
+
|
|
11
|
+
This split mirrors `docker` (host CLI) / `dockerd` (daemon). The client never reads or writes transport files directly — all protocol operations go through the API.
|
|
12
|
+
|
|
13
|
+
## Status (v7.0.0-dev.0)
|
|
14
|
+
|
|
15
|
+
- `crosstalk version` — works
|
|
16
|
+
- All other subcommands — landing in P4–P6
|
|
17
|
+
|
|
18
|
+
## Install (post-publish; today this package is unpublished)
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npm install -g @cordfuse/crosstalk
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## License
|
|
25
|
+
|
|
26
|
+
MIT.
|
package/bin/crosstalk.js
CHANGED
|
@@ -1,97 +1,83 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// crosstalk —
|
|
2
|
+
// crosstalk — host-side client for the crosstalkd daemon.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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)
|
|
8
|
+
//
|
|
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).
|
|
9
18
|
|
|
10
|
-
import {
|
|
11
|
-
import { resolve, join, dirname } from 'path';
|
|
12
|
-
import { spawnSync, spawn } from 'child_process';
|
|
19
|
+
import { dirname, join } from 'path';
|
|
13
20
|
import { fileURLToPath } from 'url';
|
|
14
|
-
import {
|
|
21
|
+
import { existsSync } from 'fs';
|
|
15
22
|
|
|
16
|
-
const SUBCOMMANDS =
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
const SUBCOMMANDS = {
|
|
24
|
+
// each entry is the file in commands/ to load
|
|
25
|
+
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
|
+
restart: 'restart.js',
|
|
34
|
+
pull: 'pull.js',
|
|
35
|
+
logs: 'logs.js',
|
|
36
|
+
chat: 'chat.js',
|
|
37
|
+
};
|
|
21
38
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
while (true) {
|
|
25
|
-
const versionFile = join(dir, 'upstream', 'CROSSTALK-VERSION');
|
|
26
|
-
if (existsSync(versionFile) && statSync(versionFile).isFile()) return dir;
|
|
27
|
-
const parent = dirname(dir);
|
|
28
|
-
if (parent === dir) return null;
|
|
29
|
-
dir = parent;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
39
|
+
const argv = process.argv.slice(2);
|
|
40
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
32
41
|
|
|
33
42
|
function printUsage(exitCode = 0) {
|
|
34
43
|
process.stdout.write(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
`Usage: crosstalk <subcommand> [args...]
|
|
45
|
+
|
|
46
|
+
The host-side client for crosstalkd. Most commands talk to the engine's
|
|
47
|
+
HTTP API on 127.0.0.1; lifecycle commands talk to the local docker daemon.
|
|
48
|
+
|
|
49
|
+
Subcommands:
|
|
50
|
+
${Object.keys(SUBCOMMANDS).map((s) => ` ${s}`).join('\n')}
|
|
51
|
+
|
|
52
|
+
If your engine listens on a non-default port, set CROSSTALK_API_PORT.
|
|
53
|
+
|
|
54
|
+
v7.0.0-dev — many subcommands are not implemented yet (see bin/crosstalk.js).
|
|
55
|
+
`,
|
|
40
56
|
);
|
|
41
57
|
process.exit(exitCode);
|
|
42
58
|
}
|
|
43
59
|
|
|
44
|
-
const argv = process.argv.slice(2);
|
|
45
60
|
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') printUsage(0);
|
|
46
61
|
|
|
47
|
-
if (argv[0] === '--version'
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
process.exit(0);
|
|
62
|
+
if (argv[0] === '--version') {
|
|
63
|
+
// delegate to the version command so the engine-side number can also
|
|
64
|
+
// be queried in a uniform place
|
|
65
|
+
argv[0] = 'version';
|
|
52
66
|
}
|
|
53
67
|
|
|
54
68
|
const cmd = argv[0];
|
|
55
|
-
|
|
56
|
-
|
|
69
|
+
const file = SUBCOMMANDS[cmd];
|
|
70
|
+
if (!file) {
|
|
71
|
+
process.stderr.write(`crosstalk: unknown subcommand '${cmd}'\n\n`);
|
|
57
72
|
printUsage(1);
|
|
58
73
|
}
|
|
59
74
|
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (!STANDALONE_SUBCOMMANDS.has(cmd)) {
|
|
65
|
-
const root = findTransportRoot(cwd);
|
|
66
|
-
if (!root) {
|
|
67
|
-
console.error(
|
|
68
|
-
`crosstalk ${cmd}: not inside a Crosstalk transport ` +
|
|
69
|
-
`(no upstream/CROSSTALK-VERSION found from ${cwd} upward).`,
|
|
70
|
-
);
|
|
71
|
-
process.exit(2);
|
|
72
|
-
}
|
|
73
|
-
cwd = root;
|
|
75
|
+
const modulePath = join(thisDir, '..', 'commands', file);
|
|
76
|
+
if (!existsSync(modulePath)) {
|
|
77
|
+
process.stderr.write(`crosstalk: command file missing for '${cmd}' (expected ${modulePath})\n`);
|
|
78
|
+
process.exit(2);
|
|
74
79
|
}
|
|
75
80
|
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
// dispatch is long-running: use async spawn so SIGTERM/SIGINT forwarded to
|
|
80
|
-
// the tsx child kills the whole chain cleanly. All other subcommands are
|
|
81
|
-
// short-lived and spawnSync is fine.
|
|
82
|
-
if (cmd === 'dispatch') {
|
|
83
|
-
const child = spawn(process.execPath, [tsxCli, srcFile, ...argv.slice(1)], {
|
|
84
|
-
cwd,
|
|
85
|
-
stdio: 'inherit',
|
|
86
|
-
});
|
|
87
|
-
const forward = (sig) => { try { child.kill(sig); } catch {} };
|
|
88
|
-
process.on('SIGTERM', () => forward('SIGTERM'));
|
|
89
|
-
process.on('SIGINT', () => forward('SIGTERM'));
|
|
90
|
-
child.on('exit', (code, signal) => process.exit(signal ? 1 : (code ?? 0)));
|
|
91
|
-
} else {
|
|
92
|
-
const r = spawnSync(process.execPath, [tsxCli, srcFile, ...argv.slice(1)], {
|
|
93
|
-
cwd,
|
|
94
|
-
stdio: 'inherit',
|
|
95
|
-
});
|
|
96
|
-
process.exit(r.status ?? 1);
|
|
97
|
-
}
|
|
81
|
+
const mod = await import(modulePath);
|
|
82
|
+
const exit = await mod.run(argv.slice(1));
|
|
83
|
+
process.exit(typeof exit === 'number' ? exit : 0);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// crosstalk channel <name> — create a channel via POST /channels.
|
|
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.
|
|
6
|
+
|
|
7
|
+
import { api } from '../lib/api-client.js';
|
|
8
|
+
import { reportAndExit } from '../lib/errors.js';
|
|
9
|
+
import { positionals, 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 channel <name> # create a channel
|
|
16
|
+
crosstalk channel list # list channels
|
|
17
|
+
crosstalk channel --help
|
|
18
|
+
|
|
19
|
+
Not yet implemented (engine API needs PATCH/DELETE first):
|
|
20
|
+
crosstalk channel <name-or-uuid> --rename <new>
|
|
21
|
+
crosstalk channel <name-or-uuid> --delete
|
|
22
|
+
`,
|
|
23
|
+
);
|
|
24
|
+
process.exit(exit);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function run(argv) {
|
|
28
|
+
if (has(argv, '--help') || has(argv, '-h')) usage(0);
|
|
29
|
+
|
|
30
|
+
const pos = positionals(argv, []);
|
|
31
|
+
if (pos.length === 0) usage(1);
|
|
32
|
+
|
|
33
|
+
// `crosstalk channel list` → list channels
|
|
34
|
+
if (pos[0] === 'list') {
|
|
35
|
+
let r;
|
|
36
|
+
try {
|
|
37
|
+
r = await api.get('/channels');
|
|
38
|
+
} catch (err) {
|
|
39
|
+
reportAndExit(err, 'crosstalk channel list');
|
|
40
|
+
}
|
|
41
|
+
if (r.channels.length === 0) {
|
|
42
|
+
process.stdout.write('(no channels)\n');
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
for (const ch of r.channels) {
|
|
46
|
+
const name = ch.name ?? '(unnamed)';
|
|
47
|
+
const parentSuffix = ch.parent ? ` [child of ${ch.parent.slice(0, 8)}]` : '';
|
|
48
|
+
process.stdout.write(`${ch.uuid} ${name}${parentSuffix}\n`);
|
|
49
|
+
}
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Otherwise: create a channel with the given name.
|
|
54
|
+
if (has(argv, '--rename') || has(argv, '--delete')) {
|
|
55
|
+
process.stderr.write('crosstalk channel: --rename and --delete are not implemented yet (engine API needs PATCH/DELETE).\n');
|
|
56
|
+
return 1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const name = pos[0];
|
|
60
|
+
let r;
|
|
61
|
+
try {
|
|
62
|
+
r = await api.post('/channels', { name });
|
|
63
|
+
} catch (err) {
|
|
64
|
+
reportAndExit(err, 'crosstalk channel');
|
|
65
|
+
}
|
|
66
|
+
process.stdout.write(`Created channel: ${r.uuid}\n`);
|
|
67
|
+
process.stdout.write(` name: ${r.name}\n`);
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
package/commands/chat.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// crosstalk chat — the single interactive entry point into the container.
|
|
2
|
+
//
|
|
3
|
+
// Modes:
|
|
4
|
+
// crosstalk chat # default agent (claude), interactive
|
|
5
|
+
// crosstalk chat --agent <name> # interactive with a specific agent
|
|
6
|
+
// crosstalk chat --login [--agent X] # OAuth setup flow with browser-open
|
|
7
|
+
// crosstalk chat --shell # bash escape hatch (sysadmin / debug)
|
|
8
|
+
//
|
|
9
|
+
// Login flow design (root-cause fix, not a bandaid):
|
|
10
|
+
// The container has no DISPLAY / no browser / no xdg-open. So the agent
|
|
11
|
+
// CLI prints a URL and expects the operator to manually open it. THIS
|
|
12
|
+
// wrapper runs on the HOST — where the browser lives. It intercepts the
|
|
13
|
+
// URL on the agent's stdout and opens it directly via the OS-native
|
|
14
|
+
// browser launcher (`open` / `xdg-open` / `wslview` / `start`). Operator
|
|
15
|
+
// never has to read or copy-paste the URL.
|
|
16
|
+
//
|
|
17
|
+
// COLUMNS=1000 is still set under --login as a contingency: if browser
|
|
18
|
+
// open fails (headless SSH session, missing xdg-open, etc.), the URL
|
|
19
|
+
// still renders unwrapped in the terminal for manual copy. That's the
|
|
20
|
+
// fallback, not the primary mechanism.
|
|
21
|
+
|
|
22
|
+
import { spawn, spawnSync } from 'child_process';
|
|
23
|
+
import { requireTransportRoot, containerName } from '../lib/transport.js';
|
|
24
|
+
import { flag, has } from '../lib/argv.js';
|
|
25
|
+
|
|
26
|
+
const KNOWN_AGENTS = ['claude', 'codex', 'gemini', 'qwen', 'opencode', 'agy'];
|
|
27
|
+
// Conservative URL regex: matches the leading https://...token boundary,
|
|
28
|
+
// stops at whitespace/quote/angle. False positives are bounded to --login
|
|
29
|
+
// mode where the first URL printed is almost certainly the auth URL.
|
|
30
|
+
const URL_RE = /https?:\/\/[^\s<>"']+/;
|
|
31
|
+
|
|
32
|
+
function openInBrowser(url) {
|
|
33
|
+
const cmd =
|
|
34
|
+
process.platform === 'darwin' ? 'open' :
|
|
35
|
+
process.platform === 'win32' ? 'start' :
|
|
36
|
+
process.env['WSL_DISTRO_NAME'] ? 'wslview' :
|
|
37
|
+
/* default Linux */ 'xdg-open';
|
|
38
|
+
try {
|
|
39
|
+
const r = spawnSync(cmd, [url], { stdio: 'ignore' });
|
|
40
|
+
return r.status === 0;
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function usage(exit = 0) {
|
|
47
|
+
const w = exit === 0 ? process.stdout : process.stderr;
|
|
48
|
+
w.write(
|
|
49
|
+
`Usage: crosstalk chat [--agent <name>] [--login] [--shell]
|
|
50
|
+
|
|
51
|
+
Opens an interactive session inside the engine container — the single
|
|
52
|
+
PTY-wrapped entry point for everything you'd do inside the container
|
|
53
|
+
(daily chat, OAuth setup, sysadmin).
|
|
54
|
+
|
|
55
|
+
Modes:
|
|
56
|
+
(default) Interactive with the default agent (claude)
|
|
57
|
+
--agent <name> Use a specific agent. Supported:
|
|
58
|
+
${KNOWN_AGENTS.join(', ')}
|
|
59
|
+
--login First-time auth flow. Intercepts the OAuth URL on
|
|
60
|
+
the agent's stdout and opens it in your default
|
|
61
|
+
browser via the OS-native launcher. Falls back to
|
|
62
|
+
printing the URL unwrapped if no browser is
|
|
63
|
+
reachable (headless / SSH session).
|
|
64
|
+
--shell Drop into bash (install agent CLIs, debug,
|
|
65
|
+
inspect state).
|
|
66
|
+
|
|
67
|
+
Examples:
|
|
68
|
+
crosstalk chat # interactive claude
|
|
69
|
+
crosstalk chat --agent gemini # interactive gemini
|
|
70
|
+
crosstalk chat --login # OAuth, claude
|
|
71
|
+
crosstalk chat --login --agent codex # OAuth, codex
|
|
72
|
+
crosstalk chat --shell # bash
|
|
73
|
+
`,
|
|
74
|
+
);
|
|
75
|
+
process.exit(exit);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function runInteractive(container, command, env = {}) {
|
|
79
|
+
// Full TTY passthrough — docker exec -it gives the container a real
|
|
80
|
+
// PTY, and stdio: 'inherit' forwards stdin/stdout/stderr from the
|
|
81
|
+
// operator's terminal. Agent CLIs get to render their rich TUI
|
|
82
|
+
// normally.
|
|
83
|
+
const envFlags = [];
|
|
84
|
+
for (const [k, v] of Object.entries(env)) {
|
|
85
|
+
envFlags.push('--env', `${k}=${v}`);
|
|
86
|
+
}
|
|
87
|
+
const r = spawnSync('docker', ['exec', '-it', ...envFlags, container, command], {
|
|
88
|
+
stdio: 'inherit',
|
|
89
|
+
});
|
|
90
|
+
return r.status ?? 1;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function runLogin(container, agent) {
|
|
94
|
+
// Login mode: tee the agent's stdout so we can spot the auth URL and
|
|
95
|
+
// open it in the operator's browser. The container still has a real
|
|
96
|
+
// PTY (docker exec -it allocates one inside the container regardless
|
|
97
|
+
// of host stdio configuration), so the agent CLI renders into a TTY;
|
|
98
|
+
// we just pipe the bytes on the host side to inspect them.
|
|
99
|
+
//
|
|
100
|
+
// No agent-specific "login" subcommand — we just run the agent's
|
|
101
|
+
// default interactive entry point. First-run auth flow happens
|
|
102
|
+
// automatically for every agent we support. Operator can pass
|
|
103
|
+
// --agent <name> to target a specific one.
|
|
104
|
+
return new Promise((resolve) => {
|
|
105
|
+
const child = spawn(
|
|
106
|
+
'docker',
|
|
107
|
+
[
|
|
108
|
+
'exec', '-it',
|
|
109
|
+
'--env', 'COLUMNS=1000', // fallback for browser-open failure
|
|
110
|
+
'--env', 'LINES=40',
|
|
111
|
+
container,
|
|
112
|
+
agent,
|
|
113
|
+
],
|
|
114
|
+
{ stdio: ['inherit', 'pipe', 'inherit'] },
|
|
115
|
+
);
|
|
116
|
+
let opened = false;
|
|
117
|
+
child.stdout.on('data', (chunk) => {
|
|
118
|
+
process.stdout.write(chunk);
|
|
119
|
+
if (opened) return;
|
|
120
|
+
const match = chunk.toString().match(URL_RE);
|
|
121
|
+
if (!match) return;
|
|
122
|
+
const url = match[0];
|
|
123
|
+
opened = true;
|
|
124
|
+
const ok = openInBrowser(url);
|
|
125
|
+
process.stdout.write(
|
|
126
|
+
ok
|
|
127
|
+
? `\n[crosstalk chat] Opened auth URL in your default browser.\n`
|
|
128
|
+
: `\n[crosstalk chat] Could not auto-open browser. Open this URL manually:\n ${url}\n`,
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
child.on('exit', (code) => resolve(code ?? 0));
|
|
132
|
+
child.on('error', () => resolve(1));
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function run(argv) {
|
|
137
|
+
if (has(argv, '--help') || has(argv, '-h')) usage(0);
|
|
138
|
+
|
|
139
|
+
const root = requireTransportRoot();
|
|
140
|
+
const name = containerName(root);
|
|
141
|
+
|
|
142
|
+
if (has(argv, '--shell')) {
|
|
143
|
+
return runInteractive(name, 'bash');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const agent = flag(argv, '--agent') ?? 'claude';
|
|
147
|
+
if (!KNOWN_AGENTS.includes(agent)) {
|
|
148
|
+
process.stderr.write(
|
|
149
|
+
`crosstalk chat: unknown agent '${agent}'. Supported: ${KNOWN_AGENTS.join(', ')}\n`,
|
|
150
|
+
);
|
|
151
|
+
return 1;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (has(argv, '--login')) {
|
|
155
|
+
return await runLogin(name, agent);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return runInteractive(name, agent);
|
|
159
|
+
}
|
package/commands/down.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// crosstalk down — stop the engine container for this transport.
|
|
2
|
+
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import { spawnSync } from 'child_process';
|
|
5
|
+
import { requireTransportRoot, composeFile } from '../lib/transport.js';
|
|
6
|
+
import { has } from '../lib/argv.js';
|
|
7
|
+
|
|
8
|
+
function usage(exit = 0) {
|
|
9
|
+
const w = exit === 0 ? process.stdout : process.stderr;
|
|
10
|
+
w.write(
|
|
11
|
+
`Usage: crosstalk down [--volumes]
|
|
12
|
+
|
|
13
|
+
Stops the engine container for the transport in the current directory.
|
|
14
|
+
With --volumes, also removes the persistent volumes (clears operator-
|
|
15
|
+
installed CLIs + auth state; you'll have to re-install + re-auth).
|
|
16
|
+
`,
|
|
17
|
+
);
|
|
18
|
+
process.exit(exit);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function run(argv) {
|
|
22
|
+
if (has(argv, '--help') || has(argv, '-h')) usage(0);
|
|
23
|
+
|
|
24
|
+
const root = requireTransportRoot();
|
|
25
|
+
const composeYml = composeFile(root);
|
|
26
|
+
if (!existsSync(composeYml)) {
|
|
27
|
+
process.stderr.write(`crosstalk down: no docker-compose.yml at ${composeYml}\n`);
|
|
28
|
+
process.stderr.write(` (this transport was probably never brought up — nothing to do)\n`);
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const args = ['compose', '-f', composeYml, 'down'];
|
|
33
|
+
if (has(argv, '--volumes')) args.push('--volumes');
|
|
34
|
+
|
|
35
|
+
const r = spawnSync('docker', args, { cwd: root, stdio: 'inherit' });
|
|
36
|
+
return r.status ?? 1;
|
|
37
|
+
}
|
package/commands/init.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// crosstalk init <dir> — scaffold a new transport via docker.
|
|
2
|
+
//
|
|
3
|
+
// Doesn't go through the engine HTTP API — there's no running engine
|
|
4
|
+
// at init time. Instead, runs the engine in a one-shot container with
|
|
5
|
+
// a bind-mount of the target directory, and lets crosstalkd's init
|
|
6
|
+
// subcommand write the template files directly.
|
|
7
|
+
//
|
|
8
|
+
// docker run --rm \
|
|
9
|
+
// -v "$(realpath <dir>):/init-target" \
|
|
10
|
+
// --user "$(id -u):$(id -g)" \
|
|
11
|
+
// <image> \
|
|
12
|
+
// crosstalkd init /init-target
|
|
13
|
+
//
|
|
14
|
+
// The --user flag is critical: without it, the template files would
|
|
15
|
+
// land owned by root (the container's user). Setting it to the
|
|
16
|
+
// operator's UID/GID keeps file ownership sane.
|
|
17
|
+
|
|
18
|
+
import { mkdirSync, existsSync } from 'fs';
|
|
19
|
+
import { resolve } from 'path';
|
|
20
|
+
import { spawnSync } from 'child_process';
|
|
21
|
+
import { has, positionals } from '../lib/argv.js';
|
|
22
|
+
|
|
23
|
+
const DEFAULT_IMAGE = process.env.CROSSTALK_IMAGE
|
|
24
|
+
?? 'ghcr.io/cordfuse/crosstalk-server:7.0.0-alpha.1';
|
|
25
|
+
|
|
26
|
+
function usage(exit = 0) {
|
|
27
|
+
const w = exit === 0 ? process.stdout : process.stderr;
|
|
28
|
+
w.write(
|
|
29
|
+
`Usage:
|
|
30
|
+
crosstalk init <dir> # scaffold a new transport at <dir>
|
|
31
|
+
|
|
32
|
+
Image: ${DEFAULT_IMAGE}
|
|
33
|
+
Override: CROSSTALK_IMAGE=<image-tag> crosstalk init <dir>
|
|
34
|
+
|
|
35
|
+
The image must be pullable (or already present locally). For local dev
|
|
36
|
+
before the GHCR publish pipeline ships, build the image first:
|
|
37
|
+
docker build -t crosstalk-server:7.0.0-alpha.1 -f server/Dockerfile .
|
|
38
|
+
Then run:
|
|
39
|
+
CROSSTALK_IMAGE=crosstalk-server:7.0.0-alpha.1 crosstalk init mytransport
|
|
40
|
+
`,
|
|
41
|
+
);
|
|
42
|
+
process.exit(exit);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function run(argv) {
|
|
46
|
+
if (has(argv, '--help') || has(argv, '-h')) usage(0);
|
|
47
|
+
const pos = positionals(argv, []);
|
|
48
|
+
if (pos.length === 0) usage(1);
|
|
49
|
+
|
|
50
|
+
const target = resolve(pos[0]);
|
|
51
|
+
if (!existsSync(target)) mkdirSync(target, { recursive: true });
|
|
52
|
+
|
|
53
|
+
// Resolve uid/gid via process.getuid() — POSIX only. On Windows the
|
|
54
|
+
// --user flag is omitted; Docker Desktop handles ownership differently.
|
|
55
|
+
const uid = typeof process.getuid === 'function' ? process.getuid() : null;
|
|
56
|
+
const gid = typeof process.getgid === 'function' ? process.getgid() : null;
|
|
57
|
+
const userArgs = (uid != null && gid != null) ? ['--user', `${uid}:${gid}`] : [];
|
|
58
|
+
|
|
59
|
+
// --entrypoint crosstalkd overrides the image's default ENTRYPOINT
|
|
60
|
+
// (which is the dispatcher entrypoint.sh requiring TRANSPORT_GIT_URL).
|
|
61
|
+
// For init we want to call crosstalkd directly with no env requirements.
|
|
62
|
+
const args = [
|
|
63
|
+
'run', '--rm',
|
|
64
|
+
'-v', `${target}:/init-target`,
|
|
65
|
+
...userArgs,
|
|
66
|
+
'--entrypoint', 'crosstalkd',
|
|
67
|
+
DEFAULT_IMAGE,
|
|
68
|
+
'init', '/init-target',
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
const r = spawnSync('docker', args, { stdio: 'inherit' });
|
|
72
|
+
if (r.status !== 0) {
|
|
73
|
+
process.stderr.write(`crosstalk init: docker run exited ${r.status}\n`);
|
|
74
|
+
process.stderr.write(` command was: docker ${args.join(' ')}\n`);
|
|
75
|
+
return r.status ?? 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Auto git init + initial commit so the transport is ready for
|
|
79
|
+
// `crosstalk up` immediately. Engine expects a git repo (cursor +
|
|
80
|
+
// commit operations on every tick). Doing this on the host so the
|
|
81
|
+
// commit author uses the operator's local git config — not the
|
|
82
|
+
// container's default.
|
|
83
|
+
//
|
|
84
|
+
// Best-effort: if any git step fails, print a warning but don't fail
|
|
85
|
+
// the init (operator can complete the steps manually).
|
|
86
|
+
const gitSteps = [
|
|
87
|
+
['init', '--quiet', '--initial-branch=main'],
|
|
88
|
+
['add', '-A'],
|
|
89
|
+
['commit', '--quiet', '-m', 'initial transport'],
|
|
90
|
+
];
|
|
91
|
+
for (const args of gitSteps) {
|
|
92
|
+
const gr = spawnSync('git', args, { cwd: target, stdio: 'pipe' });
|
|
93
|
+
if (gr.status !== 0) {
|
|
94
|
+
const stderr = gr.stderr?.toString().trim() ?? '';
|
|
95
|
+
process.stderr.write(
|
|
96
|
+
`crosstalk init: \`git ${args.join(' ')}\` failed (transport scaffolded but not committed)\n`,
|
|
97
|
+
);
|
|
98
|
+
if (stderr) process.stderr.write(` ${stderr.split('\n').join('\n ')}\n`);
|
|
99
|
+
process.stderr.write(` Finish manually: cd ${target} && git init && git add -A && git commit -m "initial transport"\n`);
|
|
100
|
+
return 0; // not fatal — scaffold succeeded
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
process.stdout.write(`\nGit initialized; transport is ready for \`crosstalk up\`.\n`);
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
package/commands/logs.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// crosstalk logs — tail the engine container's logs.
|
|
2
|
+
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import { spawnSync } from 'child_process';
|
|
5
|
+
import { requireTransportRoot, composeFile } from '../lib/transport.js';
|
|
6
|
+
import { has } from '../lib/argv.js';
|
|
7
|
+
|
|
8
|
+
function usage(exit = 0) {
|
|
9
|
+
const w = exit === 0 ? process.stdout : process.stderr;
|
|
10
|
+
w.write(
|
|
11
|
+
`Usage: crosstalk logs [-f|--follow] [-n <lines>]
|
|
12
|
+
|
|
13
|
+
Streams the engine container's stdout/stderr. -f to follow (Ctrl-C to stop).
|
|
14
|
+
`,
|
|
15
|
+
);
|
|
16
|
+
process.exit(exit);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function run(argv) {
|
|
20
|
+
if (has(argv, '--help') || has(argv, '-h')) usage(0);
|
|
21
|
+
|
|
22
|
+
const root = requireTransportRoot();
|
|
23
|
+
const composeYml = composeFile(root);
|
|
24
|
+
if (!existsSync(composeYml)) {
|
|
25
|
+
process.stderr.write(`crosstalk logs: no docker-compose.yml — run 'crosstalk up' first.\n`);
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
const args = ['compose', '-f', composeYml, 'logs'];
|
|
29
|
+
if (has(argv, '-f') || has(argv, '--follow')) args.push('--follow');
|
|
30
|
+
// Pass through tail count if provided.
|
|
31
|
+
for (let i = 0; i < argv.length; i++) {
|
|
32
|
+
if (argv[i] === '-n' || argv[i] === '--tail') {
|
|
33
|
+
args.push('--tail', argv[i + 1] ?? '100');
|
|
34
|
+
i++;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const r = spawnSync('docker', args, { cwd: root, stdio: 'inherit' });
|
|
38
|
+
return r.status ?? 1;
|
|
39
|
+
}
|
package/commands/pull.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// crosstalk pull — refresh the engine image.
|
|
2
|
+
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import { spawnSync } from 'child_process';
|
|
5
|
+
import { requireTransportRoot, composeFile } from '../lib/transport.js';
|
|
6
|
+
import { has } from '../lib/argv.js';
|
|
7
|
+
|
|
8
|
+
export async function run(argv) {
|
|
9
|
+
if (has(argv, '--help') || has(argv, '-h')) {
|
|
10
|
+
process.stdout.write('Usage: crosstalk pull\n');
|
|
11
|
+
return 0;
|
|
12
|
+
}
|
|
13
|
+
const root = requireTransportRoot();
|
|
14
|
+
const composeYml = composeFile(root);
|
|
15
|
+
if (!existsSync(composeYml)) {
|
|
16
|
+
process.stderr.write(`crosstalk pull: no docker-compose.yml — run 'crosstalk up' first.\n`);
|
|
17
|
+
return 1;
|
|
18
|
+
}
|
|
19
|
+
const r = spawnSync('docker', ['compose', '-f', composeYml, 'pull'], {
|
|
20
|
+
cwd: root,
|
|
21
|
+
stdio: 'inherit',
|
|
22
|
+
});
|
|
23
|
+
return r.status ?? 1;
|
|
24
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// crosstalk replies <relPath> [<relPath>...] — poll reply status.
|
|
2
|
+
//
|
|
3
|
+
// Exit 0 if all targets have REPLIED or FAILED, exit 2 while any PENDING
|
|
4
|
+
// — agents can poll cheaply in a loop. Mirrors crosstalkd replies.
|
|
5
|
+
|
|
6
|
+
import { api } from '../lib/api-client.js';
|
|
7
|
+
import { reportAndExit } from '../lib/errors.js';
|
|
8
|
+
import { positionals, has } from '../lib/argv.js';
|
|
9
|
+
|
|
10
|
+
function usage(exit = 0) {
|
|
11
|
+
const w = exit === 0 ? process.stdout : process.stderr;
|
|
12
|
+
w.write('Usage: crosstalk replies <relPath> [<relPath>...]\n');
|
|
13
|
+
process.exit(exit);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function run(argv) {
|
|
17
|
+
if (has(argv, '--help') || has(argv, '-h')) usage(0);
|
|
18
|
+
const targets = positionals(argv, []);
|
|
19
|
+
if (targets.length === 0) usage(1);
|
|
20
|
+
|
|
21
|
+
let resp;
|
|
22
|
+
try {
|
|
23
|
+
resp = await api.get(`/replies?relPaths=${encodeURIComponent(targets.join(','))}`);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
reportAndExit(err, 'crosstalk replies');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let pending = 0;
|
|
29
|
+
for (const r of resp.replies) {
|
|
30
|
+
if (r.status === 'PENDING') {
|
|
31
|
+
process.stdout.write(`PENDING ${r.target}\n`);
|
|
32
|
+
pending++;
|
|
33
|
+
} else {
|
|
34
|
+
const tag = r.status === 'FAILED' ? 'FAILED ' : 'REPLIED ';
|
|
35
|
+
process.stdout.write(`${tag} ${r.target} <- ${r.from} (${r.replyRelPath})\n`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return pending > 0 ? 2 : 0;
|
|
39
|
+
}
|