@cordfuse/crosstalk 7.0.0-alpha.6 → 7.0.0-alpha.8
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/commands/chat.js +23 -153
- package/commands/init.js +134 -86
- package/commands/restart.js +10 -3
- package/commands/status.js +2 -2
- package/commands/up.js +1 -1
- package/lib/api-client.js +9 -2
- package/lib/resolve.js +11 -1
- package/package.json +1 -1
package/commands/chat.js
CHANGED
|
@@ -1,110 +1,33 @@
|
|
|
1
|
-
// crosstalk chat —
|
|
1
|
+
// crosstalk chat — interactive entry into the container.
|
|
2
2
|
//
|
|
3
|
-
// Modes:
|
|
4
|
-
// crosstalk chat --agent <name>
|
|
5
|
-
// crosstalk chat --
|
|
6
|
-
// crosstalk chat --shell # bash escape hatch (sysadmin / debug)
|
|
3
|
+
// Modes (alpha.8):
|
|
4
|
+
// crosstalk chat --agent <name> # interactive session with an agent CLI
|
|
5
|
+
// crosstalk chat --shell # bash escape hatch (debug, install CLIs, run setup commands)
|
|
7
6
|
//
|
|
8
|
-
// --agent is required for non-shell
|
|
9
|
-
//
|
|
10
|
-
//
|
|
7
|
+
// --agent is required for non-shell mode. There's no implicit default;
|
|
8
|
+
// containers may have multiple agent CLIs installed, the operator
|
|
9
|
+
// picks one explicitly.
|
|
11
10
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// browser launcher (`open` / `xdg-open` / `wslview` / `start`). Operator
|
|
18
|
-
// never has to read or copy-paste the URL.
|
|
19
|
-
//
|
|
20
|
-
// COLUMNS=1000 is still set under --login as a contingency: if browser
|
|
21
|
-
// open fails (headless SSH session, missing xdg-open, etc.), the URL
|
|
22
|
-
// still renders unwrapped in the terminal for manual copy. That's the
|
|
23
|
-
// fallback, not the primary mechanism.
|
|
11
|
+
// `--login` was removed in alpha.8. Per-agent auth is now handled via
|
|
12
|
+
// `data/crosstalk.yaml`'s per-provider `env_file:` (env-injected at
|
|
13
|
+
// agent spawn time inside the engine) plus operator-run setup commands
|
|
14
|
+
// for agents whose auth is a stateful CLI act (e.g. codex). Recipes are
|
|
15
|
+
// documented in transport/auth/README.md.
|
|
24
16
|
|
|
25
|
-
import {
|
|
17
|
+
import { spawnSync } from 'child_process';
|
|
26
18
|
import { requireInitialized } from '../lib/resolve.js';
|
|
27
19
|
import { apiFor, ConnectError } from '../lib/api-client.js';
|
|
28
20
|
import { flag, has } from '../lib/argv.js';
|
|
29
21
|
|
|
30
22
|
const KNOWN_AGENTS = ['claude', 'codex', 'gemini', 'qwen', 'opencode', 'agy'];
|
|
31
23
|
|
|
32
|
-
// URL extraction that survives agent TUI rendering. Naive regex against
|
|
33
|
-
// raw stdout fails because agent CLIs (Claude in particular) draw a box
|
|
34
|
-
// around the URL with line wraps every ~N chars; the captured URL is
|
|
35
|
-
// truncated at the first wrap, with ANSI control chars leaking in at
|
|
36
|
-
// the cut point. Real example from alpha.3 macOS test: extracted URL
|
|
37
|
-
// ended with `%18%6b…` (CAN char + box-drawing artifact) instead of
|
|
38
|
-
// the `redirect_uri=…` param, breaking OAuth.
|
|
39
|
-
//
|
|
40
|
-
// Approach:
|
|
41
|
-
// 1. ANSI-strip everything (CSI / OSC / SS3 escapes).
|
|
42
|
-
// 2. Strip control chars (0x00-0x1F except LF, plus DEL).
|
|
43
|
-
// 3. Join line wraps that occur INSIDE a URL-safe character run —
|
|
44
|
-
// `<url-char>\n<url-char>` → `<url-char><url-char>`. Preserves real
|
|
45
|
-
// `<url-char>\n<non-url-char>` boundaries.
|
|
46
|
-
// 4. Match `https?://<url-chars>+` against the joined buffer.
|
|
47
|
-
// 5. Only fire when the character AFTER the matched URL is a true
|
|
48
|
-
// terminator (whitespace, end-of-buffer) — guards against acting
|
|
49
|
-
// on a partial URL still being written.
|
|
50
|
-
|
|
51
|
-
const URL_SAFE_CLASS = "a-zA-Z0-9%/?#&=+\\-._~:@!$()*,;";
|
|
52
|
-
const URL_RE = new RegExp(`https?://[${URL_SAFE_CLASS}]+`);
|
|
53
|
-
|
|
54
|
-
function extractCompleteUrl(rawBuffer) {
|
|
55
|
-
// Strip ANSI CSI escapes (\x1B[ ... terminator).
|
|
56
|
-
let clean = rawBuffer.replace(/\x1B\[[0-9;?]*[a-zA-Z]/g, '');
|
|
57
|
-
// Strip ANSI OSC escapes (\x1B] ... BEL or ST).
|
|
58
|
-
clean = clean.replace(/\x1B\][^\x07\x1B]*(?:\x07|\x1B\\)/g, '');
|
|
59
|
-
// Strip SS3 escapes (\x1BO + one char).
|
|
60
|
-
clean = clean.replace(/\x1BO./g, '');
|
|
61
|
-
// Strip remaining single-char escapes that slipped through.
|
|
62
|
-
clean = clean.replace(/\x1B[^a-zA-Z0-9]/g, '');
|
|
63
|
-
// Strip control chars except LF (0x0A); we use LF for line-wrap rejoin.
|
|
64
|
-
clean = clean.replace(/[\x00-\x09\x0B-\x1F\x7F]/g, '');
|
|
65
|
-
// Rejoin URL-character wrap points: `<url-char>\n<url-char>` becomes
|
|
66
|
-
// `<url-char><url-char>`. Doesn't merge `<url-char>\n<space>` etc.
|
|
67
|
-
const safeRe = new RegExp(`[${URL_SAFE_CLASS}]\\n[${URL_SAFE_CLASS}]`, 'g');
|
|
68
|
-
let prev;
|
|
69
|
-
do {
|
|
70
|
-
prev = clean;
|
|
71
|
-
clean = clean.replace(safeRe, (m) => m[0] + m[2]);
|
|
72
|
-
} while (clean !== prev);
|
|
73
|
-
|
|
74
|
-
const match = clean.match(URL_RE);
|
|
75
|
-
if (!match) return null;
|
|
76
|
-
// Require a terminator after the URL — without it, the URL may still
|
|
77
|
-
// be growing in subsequent stdout chunks. Terminator = whitespace,
|
|
78
|
-
// quote/angle, or end-of-buffer-with-trailing-newline.
|
|
79
|
-
const after = clean.slice(match.index + match[0].length);
|
|
80
|
-
if (after.length === 0) return null; // EOF-as-grow case, wait
|
|
81
|
-
if (!/^[\s<>"']/.test(after)) return null; // URL still continuing
|
|
82
|
-
return match[0];
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function openInBrowser(url) {
|
|
86
|
-
const cmd =
|
|
87
|
-
process.platform === 'darwin' ? 'open' :
|
|
88
|
-
process.platform === 'win32' ? 'start' :
|
|
89
|
-
process.env['WSL_DISTRO_NAME'] ? 'wslview' :
|
|
90
|
-
/* default Linux */ 'xdg-open';
|
|
91
|
-
try {
|
|
92
|
-
const r = spawnSync(cmd, [url], { stdio: 'ignore' });
|
|
93
|
-
return r.status === 0;
|
|
94
|
-
} catch {
|
|
95
|
-
return false;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
24
|
function usage(exit = 0) {
|
|
100
25
|
const w = exit === 0 ? process.stdout : process.stderr;
|
|
101
26
|
w.write(
|
|
102
|
-
`Usage: crosstalk chat --agent <name>
|
|
27
|
+
`Usage: crosstalk chat --agent <name>
|
|
103
28
|
crosstalk chat --shell
|
|
104
29
|
|
|
105
|
-
Opens an interactive session inside the engine container
|
|
106
|
-
PTY-wrapped entry point for everything you'd do inside the container
|
|
107
|
-
(daily chat, OAuth setup, sysadmin).
|
|
30
|
+
Opens an interactive session inside the engine container.
|
|
108
31
|
|
|
109
32
|
--agent is REQUIRED for any chat mode. Containers can have multiple
|
|
110
33
|
agent CLIs installed; the operator must say which one to invoke.
|
|
@@ -112,20 +35,16 @@ agent CLIs installed; the operator must say which one to invoke.
|
|
|
112
35
|
Supported agents: ${KNOWN_AGENTS.join(', ')}.
|
|
113
36
|
|
|
114
37
|
Modes:
|
|
115
|
-
--agent <name>
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
--shell Drop into bash (install agent CLIs, debug,
|
|
122
|
-
inspect state). No --agent needed.
|
|
38
|
+
--agent <name> Interactive with the named agent CLI. Use the
|
|
39
|
+
agent's own keys/commands for first-time setup
|
|
40
|
+
(see transport/auth/README.md for recipes).
|
|
41
|
+
--shell Drop into bash (install agent CLIs, debug, run
|
|
42
|
+
one-time auth setup commands like
|
|
43
|
+
\`codex login --with-api-key\`).
|
|
123
44
|
|
|
124
45
|
Examples:
|
|
125
46
|
crosstalk chat --agent claude # interactive claude
|
|
126
47
|
crosstalk chat --agent gemini # interactive gemini
|
|
127
|
-
crosstalk chat --agent claude --login # OAuth, claude
|
|
128
|
-
crosstalk chat --agent codex --login # OAuth, codex
|
|
129
48
|
crosstalk chat --shell # bash
|
|
130
49
|
`,
|
|
131
50
|
);
|
|
@@ -135,8 +54,8 @@ Examples:
|
|
|
135
54
|
function runInteractive(container, command, env = {}) {
|
|
136
55
|
// Full TTY passthrough — docker exec -it gives the container a real
|
|
137
56
|
// PTY, and stdio: 'inherit' forwards stdin/stdout/stderr from the
|
|
138
|
-
// operator's terminal. Agent CLIs
|
|
139
|
-
//
|
|
57
|
+
// operator's terminal. Agent CLIs render their rich TUI normally and
|
|
58
|
+
// SIGWINCH propagates from the host's tty to the container's PTY.
|
|
140
59
|
const envFlags = [];
|
|
141
60
|
for (const [k, v] of Object.entries(env)) {
|
|
142
61
|
envFlags.push('--env', `${k}=${v}`);
|
|
@@ -147,51 +66,6 @@ function runInteractive(container, command, env = {}) {
|
|
|
147
66
|
return r.status ?? 1;
|
|
148
67
|
}
|
|
149
68
|
|
|
150
|
-
function runLogin(container, agent) {
|
|
151
|
-
// Login mode: tee the agent's stdout so we can spot the auth URL and
|
|
152
|
-
// open it in the operator's browser. The container still has a real
|
|
153
|
-
// PTY (docker exec -it allocates one inside the container regardless
|
|
154
|
-
// of host stdio configuration), so the agent CLI renders into a TTY;
|
|
155
|
-
// we just pipe the bytes on the host side to inspect them.
|
|
156
|
-
//
|
|
157
|
-
// No agent-specific "login" subcommand — we just run the agent's
|
|
158
|
-
// default interactive entry point. First-run auth flow happens
|
|
159
|
-
// automatically for every agent we support.
|
|
160
|
-
//
|
|
161
|
-
// No COLUMNS=1000 env: making the agent render the URL "unwrapped" via
|
|
162
|
-
// a wide terminal claim doesn't reliably propagate to the in-container
|
|
163
|
-
// PTY, and even when it does Claude's TUI hard-wraps anyway. The
|
|
164
|
-
// extractCompleteUrl() function joins wrapped URL fragments on the
|
|
165
|
-
// host side, so the agent's rendering width is irrelevant. As bonus,
|
|
166
|
-
// dropping COLUMNS=1000 fixes daily-chat soft-wrap weirdness in
|
|
167
|
-
// long-form prose (agents see the real host terminal width).
|
|
168
|
-
return new Promise((resolve) => {
|
|
169
|
-
const child = spawn(
|
|
170
|
-
'docker',
|
|
171
|
-
['exec', '-it', container, agent],
|
|
172
|
-
{ stdio: ['inherit', 'pipe', 'inherit'] },
|
|
173
|
-
);
|
|
174
|
-
let opened = false;
|
|
175
|
-
let buffer = '';
|
|
176
|
-
child.stdout.on('data', (chunk) => {
|
|
177
|
-
process.stdout.write(chunk);
|
|
178
|
-
if (opened) return;
|
|
179
|
-
buffer += chunk.toString();
|
|
180
|
-
const url = extractCompleteUrl(buffer);
|
|
181
|
-
if (!url) return;
|
|
182
|
-
opened = true;
|
|
183
|
-
const ok = openInBrowser(url);
|
|
184
|
-
process.stdout.write(
|
|
185
|
-
ok
|
|
186
|
-
? `\n[crosstalk chat] Opened auth URL in your default browser.\n`
|
|
187
|
-
: `\n[crosstalk chat] Could not auto-open browser. Open this URL manually:\n ${url}\n`,
|
|
188
|
-
);
|
|
189
|
-
});
|
|
190
|
-
child.on('exit', (code) => resolve(code ?? 0));
|
|
191
|
-
child.on('error', () => resolve(1));
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
|
|
195
69
|
export async function run(argv) {
|
|
196
70
|
if (has(argv, '--help') || has(argv, '-h')) usage(0);
|
|
197
71
|
|
|
@@ -241,9 +115,5 @@ export async function run(argv) {
|
|
|
241
115
|
return 1;
|
|
242
116
|
}
|
|
243
117
|
|
|
244
|
-
if (has(argv, '--login')) {
|
|
245
|
-
return await runLogin(name, agent);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
118
|
return runInteractive(name, agent);
|
|
249
119
|
}
|
package/commands/init.js
CHANGED
|
@@ -1,40 +1,40 @@
|
|
|
1
1
|
// crosstalk init — scaffold a transport into <base>/<name>/.
|
|
2
2
|
//
|
|
3
|
-
// alpha.
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// alpha.7 rewrite: phased + idempotent. Each phase is independent and
|
|
4
|
+
// safe to re-run; re-init detects missing markers and repairs them.
|
|
5
|
+
// Mac caught bug A in alpha.6 where the api-port file was written at
|
|
6
|
+
// the end of the pipeline, so any disruption (image pull, git failure)
|
|
7
|
+
// left storage existing but unstartable — and re-init silently
|
|
8
|
+
// no-op-ed instead of fixing it.
|
|
7
9
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
10
|
+
// Phases:
|
|
11
|
+
// 1. Storage layout — mkdir subdirs + write api-port. Pure FS, fast,
|
|
12
|
+
// runs first so the resolver-visible marker exists before anything
|
|
13
|
+
// else can go wrong.
|
|
14
|
+
// 2. Transport scaffold — `crosstalkd init` in a one-shot container.
|
|
15
|
+
// Skipped if transport/.git already exists. stdout/stderr captured
|
|
16
|
+
// (mac's bug B — in-container scaffold output was leaking).
|
|
17
|
+
// 3. Git init + initial commit. Skipped if .git exists.
|
|
18
|
+
// 4. Remote setup if --remote given (errors if a different origin
|
|
19
|
+
// is already set; idempotent on same-value).
|
|
20
|
+
// 5. Summary.
|
|
12
21
|
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
// bind-mounting transport/ as /init-target. The engine writes its
|
|
18
|
-
// template (CROSSTALK-VERSION, PROTOCOL.md, CROSSTALK.md, data/,
|
|
19
|
-
// local/) into the bind-mount.
|
|
20
|
-
// 3. `git init` inside transport/ + initial commit.
|
|
21
|
-
// 4. If --remote: `git remote add origin <url>`.
|
|
22
|
-
// 5. Pick a free API port (default container always 7000; named
|
|
23
|
-
// containers search from 7001) and write to <base>/<name>/api-port.
|
|
24
|
-
|
|
25
|
-
import { mkdirSync, existsSync, writeFileSync } from 'fs';
|
|
22
|
+
// Each phase reports created/skipped/repaired so operators can tell
|
|
23
|
+
// what just happened.
|
|
24
|
+
|
|
25
|
+
import { mkdirSync, existsSync, writeFileSync, readFileSync } from 'fs';
|
|
26
26
|
import { spawnSync } from 'child_process';
|
|
27
|
+
import { join } from 'path';
|
|
27
28
|
import { has, flag } from '../lib/argv.js';
|
|
28
29
|
import {
|
|
29
30
|
parseContainerName,
|
|
30
31
|
storagePaths,
|
|
31
|
-
isInitialized,
|
|
32
32
|
pickFreePort,
|
|
33
33
|
DEFAULT_CONTAINER_NAME,
|
|
34
34
|
} from '../lib/resolve.js';
|
|
35
35
|
|
|
36
36
|
const DEFAULT_IMAGE = process.env.CROSSTALK_IMAGE
|
|
37
|
-
?? 'ghcr.io/cordfuse/crosstalk-server:7.0.0-alpha.
|
|
37
|
+
?? 'ghcr.io/cordfuse/crosstalk-server:7.0.0-alpha.8';
|
|
38
38
|
|
|
39
39
|
function usage(exit = 0) {
|
|
40
40
|
const w = exit === 0 ? process.stdout : process.stderr;
|
|
@@ -42,8 +42,11 @@ function usage(exit = 0) {
|
|
|
42
42
|
`Usage:
|
|
43
43
|
crosstalk init [--containername <name>] [--remote <url>]
|
|
44
44
|
|
|
45
|
-
Creates a new transport on this machine
|
|
46
|
-
|
|
45
|
+
Creates a new transport on this machine, or repairs an existing
|
|
46
|
+
incomplete one. Idempotent — re-running fills in any missing markers
|
|
47
|
+
without disturbing existing state.
|
|
48
|
+
|
|
49
|
+
The transport git repo lives at <base>/<name>/transport/ where <base> is:
|
|
47
50
|
Linux: $XDG_DATA_HOME/crosstalk/ (or ~/.local/share/crosstalk/)
|
|
48
51
|
macOS: ~/Library/Application Support/crosstalk/
|
|
49
52
|
Windows: %LOCALAPPDATA%\\crosstalk\\
|
|
@@ -55,8 +58,8 @@ Options:
|
|
|
55
58
|
--containername <name> Container name (default: 'crosstalk'). Must be
|
|
56
59
|
lowercase alphanumeric + ._- (no leading . or -).
|
|
57
60
|
One default + N named containers per machine.
|
|
58
|
-
--remote <url> Set git upstream
|
|
59
|
-
|
|
61
|
+
--remote <url> Set git upstream. If a different remote is
|
|
62
|
+
already set, errors instead of overwriting.
|
|
60
63
|
--image <tag> Override the engine image (default: bundled
|
|
61
64
|
GHCR tag; or CROSSTALK_IMAGE env).
|
|
62
65
|
-c <name> Shorthand for --containername.
|
|
@@ -81,22 +84,15 @@ export async function run(argv) {
|
|
|
81
84
|
return 1;
|
|
82
85
|
}
|
|
83
86
|
|
|
84
|
-
if (isInitialized(name)) {
|
|
85
|
-
const paths = storagePaths(name);
|
|
86
|
-
process.stderr.write(
|
|
87
|
-
`crosstalk init: transport '${name}' already initialized at ${paths.transportDir}.\n` +
|
|
88
|
-
` Run 'crosstalk up${name === DEFAULT_CONTAINER_NAME ? '' : ` --containername ${name}`}' to start it,\n` +
|
|
89
|
-
` or 'crosstalk rm${name === DEFAULT_CONTAINER_NAME ? '' : ` --containername ${name}`}' to wipe it first.\n`,
|
|
90
|
-
);
|
|
91
|
-
return 1;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
87
|
const remote = flag(argv, '--remote');
|
|
95
88
|
const image = flag(argv, '--image') ?? DEFAULT_IMAGE;
|
|
96
89
|
const paths = storagePaths(name);
|
|
90
|
+
const report = [];
|
|
97
91
|
|
|
98
|
-
//
|
|
99
|
-
//
|
|
92
|
+
// ── Phase 1: Storage layout ────────────────────────────────────────
|
|
93
|
+
// mkdir is idempotent (recursive: true). Write api-port if absent.
|
|
94
|
+
// This is the load-bearing fix for bug A — port file lands FIRST,
|
|
95
|
+
// before anything else can go wrong (image pull, git init, etc.).
|
|
100
96
|
try {
|
|
101
97
|
mkdirSync(paths.transportDir, { recursive: true });
|
|
102
98
|
mkdirSync(paths.crosstalkRoot, { recursive: true });
|
|
@@ -114,81 +110,133 @@ export async function run(argv) {
|
|
|
114
110
|
return 1;
|
|
115
111
|
}
|
|
116
112
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
113
|
+
let port;
|
|
114
|
+
if (existsSync(paths.portFile)) {
|
|
115
|
+
const v = Number(readFileSync(paths.portFile, 'utf-8').trim());
|
|
116
|
+
if (Number.isInteger(v) && v > 0 && v < 65536) {
|
|
117
|
+
port = v;
|
|
118
|
+
report.push(` api port: ${port} (existing)`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (!port) {
|
|
122
|
+
try {
|
|
123
|
+
port = pickFreePort(name);
|
|
124
|
+
writeFileSync(paths.portFile, `${port}\n`);
|
|
125
|
+
report.push(` api port: ${port} (allocated)`);
|
|
126
|
+
} catch (err) {
|
|
127
|
+
process.stderr.write(`crosstalk init: port allocation failed — ${err.message}\n`);
|
|
128
|
+
return 1;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
132
131
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
132
|
+
// ── Phase 2: Transport scaffold ────────────────────────────────────
|
|
133
|
+
// The template-scaffold check is "does CROSSTALK-VERSION exist?" NOT
|
|
134
|
+
// ".git exists" — git init is a separate phase, and the engine refuses
|
|
135
|
+
// to re-scaffold over an existing CROSSTALK-VERSION (without --force).
|
|
136
|
+
// Decoupling means a disrupted init (template written, git init never
|
|
137
|
+
// ran) re-enters Phase 3 cleanly on re-run.
|
|
138
|
+
const transportGitDir = join(paths.transportDir, '.git');
|
|
139
|
+
const crosstalkVersionFile = join(paths.transportDir, 'CROSSTALK-VERSION');
|
|
140
|
+
if (!existsSync(crosstalkVersionFile)) {
|
|
141
|
+
const uid = typeof process.getuid === 'function' ? process.getuid() : null;
|
|
142
|
+
const gid = typeof process.getgid === 'function' ? process.getgid() : null;
|
|
143
|
+
const userArgs = (uid != null && gid != null) ? ['--user', `${uid}:${gid}`] : [];
|
|
144
|
+
|
|
145
|
+
const dockerArgs = [
|
|
146
|
+
'run', '--rm',
|
|
147
|
+
'-v', `${paths.transportDir}:/init-target`,
|
|
148
|
+
...userArgs,
|
|
149
|
+
'--entrypoint', 'crosstalkd',
|
|
150
|
+
image,
|
|
151
|
+
'init', '/init-target',
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
// Bug B fix: don't inherit stdio — the engine prints next-steps that
|
|
155
|
+
// confuse operators ("Transport initialized at /init-target" with
|
|
156
|
+
// container-internal paths). Capture, show only on failure.
|
|
157
|
+
const initRun = spawnSync('docker', dockerArgs, { stdio: 'pipe' });
|
|
158
|
+
if (initRun.status !== 0) {
|
|
159
|
+
const stderr = initRun.stderr?.toString() ?? '';
|
|
160
|
+
const stdout = initRun.stdout?.toString() ?? '';
|
|
161
|
+
process.stderr.write(`crosstalk init: docker run exited ${initRun.status}\n`);
|
|
162
|
+
if (stderr.trim()) process.stderr.write(` stderr: ${stderr.trim()}\n`);
|
|
163
|
+
if (stdout.trim()) process.stderr.write(` stdout: ${stdout.trim()}\n`);
|
|
164
|
+
process.stderr.write(` command was: docker ${dockerArgs.join(' ')}\n`);
|
|
165
|
+
return initRun.status ?? 1;
|
|
166
|
+
}
|
|
167
|
+
report.push(` template: scaffolded via ${image}`);
|
|
168
|
+
} else {
|
|
169
|
+
report.push(` template: present (skipped scaffold)`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── Phase 3: Git init + initial commit ─────────────────────────────
|
|
173
|
+
if (!existsSync(crosstalkVersionFile)) {
|
|
174
|
+
// Engine init succeeded but didn't leave files. Bail clearly — we
|
|
175
|
+
// won't proceed to git init on an empty dir.
|
|
176
|
+
process.stderr.write(
|
|
177
|
+
`crosstalk init: transport template missing at ${paths.transportDir}\n` +
|
|
178
|
+
` Engine init didn't write template files. Try 'crosstalk rm' then re-init.\n`,
|
|
179
|
+
);
|
|
180
|
+
return 1;
|
|
138
181
|
}
|
|
139
182
|
|
|
140
|
-
// Auto git init + initial commit. Doing it on the host so the commit
|
|
141
|
-
// author uses the operator's local git config — not the container's.
|
|
142
183
|
const gitSteps = [
|
|
143
184
|
['init', '--quiet', '--initial-branch=main'],
|
|
144
185
|
['add', '-A'],
|
|
145
186
|
['commit', '--quiet', '-m', 'initial transport'],
|
|
146
187
|
];
|
|
188
|
+
let gitOk = true;
|
|
147
189
|
for (const args of gitSteps) {
|
|
148
190
|
const gr = spawnSync('git', args, { cwd: paths.transportDir, stdio: 'pipe' });
|
|
149
191
|
if (gr.status !== 0) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
// Not fatal — storage exists and engine will tolerate uncommitted state
|
|
192
|
+
// 'git init' on existing repo is fine (it re-runs idempotently);
|
|
193
|
+
// 'add -A' is also idempotent. 'commit' fails when there's
|
|
194
|
+
// nothing to commit — that means a previous run already committed.
|
|
195
|
+
// Only the first call (git init) is load-bearing; the rest are
|
|
196
|
+
// best-effort to populate the initial state.
|
|
197
|
+
gitOk = false;
|
|
157
198
|
break;
|
|
158
199
|
}
|
|
159
200
|
}
|
|
201
|
+
report.push(gitOk ? ` git: initialized + committed` : ` git: already present (skipped)`);
|
|
160
202
|
|
|
161
|
-
//
|
|
162
|
-
// operator can retry with `cd <transport-dir> && git remote add`.
|
|
203
|
+
// ── Phase 4: Remote setup ──────────────────────────────────────────
|
|
163
204
|
if (remote) {
|
|
164
|
-
const
|
|
205
|
+
const cur = spawnSync('git', ['remote', 'get-url', 'origin'], {
|
|
165
206
|
cwd: paths.transportDir,
|
|
166
207
|
stdio: 'pipe',
|
|
167
208
|
});
|
|
168
|
-
|
|
209
|
+
const currentUrl = cur.status === 0 ? cur.stdout.toString().trim() : null;
|
|
210
|
+
if (!currentUrl) {
|
|
211
|
+
const rr = spawnSync('git', ['remote', 'add', 'origin', remote], {
|
|
212
|
+
cwd: paths.transportDir,
|
|
213
|
+
stdio: 'pipe',
|
|
214
|
+
});
|
|
215
|
+
if (rr.status !== 0) {
|
|
216
|
+
process.stderr.write(
|
|
217
|
+
`crosstalk init: 'git remote add origin ${remote}' failed — ${rr.stderr?.toString().trim()}\n`,
|
|
218
|
+
);
|
|
219
|
+
} else {
|
|
220
|
+
report.push(` remote: ${remote} (added)`);
|
|
221
|
+
}
|
|
222
|
+
} else if (currentUrl === remote) {
|
|
223
|
+
report.push(` remote: ${remote} (already set)`);
|
|
224
|
+
} else {
|
|
169
225
|
process.stderr.write(
|
|
170
|
-
`crosstalk init:
|
|
226
|
+
`crosstalk init: remote already set to '${currentUrl}'; refusing to overwrite with '${remote}'.\n` +
|
|
227
|
+
` Use 'crosstalk status' to see the transport path; cd there and adjust git config manually if intended.\n`,
|
|
171
228
|
);
|
|
229
|
+
return 1;
|
|
172
230
|
}
|
|
173
231
|
}
|
|
174
232
|
|
|
175
|
-
//
|
|
176
|
-
let port;
|
|
177
|
-
try {
|
|
178
|
-
port = pickFreePort(name);
|
|
179
|
-
writeFileSync(paths.portFile, `${port}\n`);
|
|
180
|
-
} catch (err) {
|
|
181
|
-
process.stderr.write(`crosstalk init: port allocation failed — ${err.message}\n`);
|
|
182
|
-
return 1;
|
|
183
|
-
}
|
|
184
|
-
|
|
233
|
+
// ── Phase 5: Summary ───────────────────────────────────────────────
|
|
185
234
|
process.stdout.write(
|
|
186
|
-
`\nTransport '${name}'
|
|
235
|
+
`\nTransport '${name}' ready:\n` +
|
|
187
236
|
` storage: ${paths.storageRoot}\n` +
|
|
188
237
|
` transport: ${paths.transportDir}\n` +
|
|
189
238
|
` image: ${image}\n` +
|
|
190
|
-
|
|
191
|
-
(remote ? ` remote: ${remote}\n` : '') +
|
|
239
|
+
report.join('\n') + '\n' +
|
|
192
240
|
`\nNext: crosstalk up${name === DEFAULT_CONTAINER_NAME ? '' : ` --containername ${name}`}\n`,
|
|
193
241
|
);
|
|
194
242
|
return 0;
|
package/commands/restart.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
// crosstalk restart — restart a transport's engine container.
|
|
2
|
+
//
|
|
3
|
+
// Uses `compose up -d --force-recreate` (not `compose restart`) so
|
|
4
|
+
// changes to env_file (e.g. tokens added via `crosstalk auth paste`)
|
|
5
|
+
// are picked up. Plain `compose restart` just stops + starts the same
|
|
6
|
+
// container without re-reading config.
|
|
2
7
|
|
|
3
8
|
import { existsSync } from 'fs';
|
|
4
9
|
import { spawnSync } from 'child_process';
|
|
@@ -15,8 +20,10 @@ export async function run(argv) {
|
|
|
15
20
|
process.stderr.write(`crosstalk restart: '${name}' has no compose file — run 'crosstalk up' first.\n`);
|
|
16
21
|
return 1;
|
|
17
22
|
}
|
|
18
|
-
const r = spawnSync(
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
const r = spawnSync(
|
|
24
|
+
'docker',
|
|
25
|
+
['compose', '-f', paths.composeFile, 'up', '-d', '--force-recreate'],
|
|
26
|
+
{ stdio: 'inherit' },
|
|
27
|
+
);
|
|
21
28
|
return r.status ?? 1;
|
|
22
29
|
}
|
package/commands/status.js
CHANGED
|
@@ -34,10 +34,10 @@ export async function run(argv) {
|
|
|
34
34
|
if (installed && installed.length > 0) {
|
|
35
35
|
const verb = installed.length === 1 ? 'references it' : 'references them';
|
|
36
36
|
process.stdout.write(
|
|
37
|
-
` (${installed.join(', ')} installed in the container but no data/
|
|
37
|
+
` (${installed.join(', ')} installed in the container but no data/crosstalk.yaml entry ${verb} — add one to claim)\n`,
|
|
38
38
|
);
|
|
39
39
|
} else {
|
|
40
|
-
process.stdout.write(' (no model CLIs found inside the container — install one via `crosstalk chat --shell` and add an entry to data/
|
|
40
|
+
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
41
|
}
|
|
42
42
|
} else {
|
|
43
43
|
for (const m of s.claimed_models) process.stdout.write(` - ${m}\n`);
|
package/commands/up.js
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
} from '../lib/resolve.js';
|
|
25
25
|
|
|
26
26
|
const DEFAULT_IMAGE = process.env.CROSSTALK_IMAGE
|
|
27
|
-
?? 'ghcr.io/cordfuse/crosstalk-server:7.0.0-alpha.
|
|
27
|
+
?? 'ghcr.io/cordfuse/crosstalk-server:7.0.0-alpha.8';
|
|
28
28
|
|
|
29
29
|
function usage(exit = 0) {
|
|
30
30
|
const w = exit === 0 ? process.stdout : process.stderr;
|
package/lib/api-client.js
CHANGED
|
@@ -111,9 +111,16 @@ async function call(method, path, body, opts = {}) {
|
|
|
111
111
|
|
|
112
112
|
// Bind an api client to a specific container name. All callers should
|
|
113
113
|
// either pass argv (auto-resolves name) or an explicit name. Internally
|
|
114
|
-
// each call resolves the port via the name's api-port file.
|
|
114
|
+
// each call resolves the port via the name's api-port file. Validation
|
|
115
|
+
// errors print cleanly (no stack trace) — bug C fix.
|
|
115
116
|
export function apiFor(argv) {
|
|
116
|
-
|
|
117
|
+
let name;
|
|
118
|
+
try {
|
|
119
|
+
name = parseContainerName(argv);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
process.stderr.write(`crosstalk: ${err.message}\n`);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
117
124
|
return {
|
|
118
125
|
name,
|
|
119
126
|
get: (path, opts = {}) => call('GET', path, undefined, { ...opts, name }),
|
package/lib/resolve.js
CHANGED
|
@@ -168,8 +168,18 @@ export function containerExists(name) {
|
|
|
168
168
|
// Look up the resolved name+paths from argv, validate that init has run,
|
|
169
169
|
// and exit with a clear error if not. For verbs that operate on existing
|
|
170
170
|
// transports (up after init, status, chat, down, rm, etc).
|
|
171
|
+
//
|
|
172
|
+
// Validation errors from parseContainerName are caught here and printed
|
|
173
|
+
// cleanly — Mac's bug C from alpha.6 was a raw stack trace when an
|
|
174
|
+
// invalid --containername reached the resolver.
|
|
171
175
|
export function requireInitialized(argv) {
|
|
172
|
-
|
|
176
|
+
let name;
|
|
177
|
+
try {
|
|
178
|
+
name = parseContainerName(argv);
|
|
179
|
+
} catch (err) {
|
|
180
|
+
process.stderr.write(`crosstalk: ${err.message}\n`);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
173
183
|
if (!isInitialized(name)) {
|
|
174
184
|
process.stderr.write(
|
|
175
185
|
`crosstalk: no transport '${name}' on this machine.\n` +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cordfuse/crosstalk",
|
|
3
|
-
"version": "7.0.0-alpha.
|
|
3
|
+
"version": "7.0.0-alpha.8",
|
|
4
4
|
"description": "Crosstalk client — host-side CLI that talks to the crosstalkd daemon running inside the crosstalk-server container.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|