@cordfuse/crosstalk 7.0.0-alpha.5 → 7.0.0-alpha.7

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 CHANGED
@@ -19,16 +19,14 @@ Requires: Node.js ≥ 20 + Docker on PATH + git on PATH. Works on Linux, macOS,
19
19
  ## Quickstart
20
20
 
21
21
  ```sh
22
- # 1. Scaffold a transport.
23
- crosstalk init mytransport
24
- cd mytransport
22
+ # 1. Scaffold a transport. Runtime-owned at <base>/crosstalk/transport/.
23
+ crosstalk init
25
24
 
26
- # 2. Bring up the engine container for this transport.
25
+ # 2. Bring up the engine container.
27
26
  crosstalk up
28
- # Generates docker-compose.yml in the transport root and runs
29
- # `docker compose up -d` for ghcr.io/cordfuse/crosstalk-server:<latest>.
30
- # Bind-mounts $PWD as the transport any `git remote add` from the
31
- # host is immediately visible to the engine.
27
+ # Generates <base>/crosstalk/docker-compose.yml and runs `docker
28
+ # compose up -d` for ghcr.io/cordfuse/crosstalk-server:<latest>.
29
+ # No cwd magic works from any directory.
32
30
 
33
31
  # 3. Install an agent CLI inside the container, then authenticate.
34
32
  crosstalk chat --shell
@@ -36,8 +34,8 @@ crosstalk chat --shell
36
34
  exit
37
35
  crosstalk chat --agent claude --login
38
36
  # Auth URL opens in your default browser via xdg-open / open / start.
39
- # Operator never has to copy-paste it. Token saved in the /crosstalk-root
40
- # volume so subsequent dispatched workers reuse it.
37
+ # Token saved in the /crosstalk-root volume so subsequent dispatched
38
+ # workers reuse it.
41
39
 
42
40
  # 4. Send your first message.
43
41
  crosstalk channel general
@@ -49,16 +47,19 @@ crosstalk replies <relPath printed by step above>
49
47
 
50
48
  ## Subcommand surface
51
49
 
50
+ Every name-resolved verb takes an optional `--containername <name>` (or `-c <name>`) flag. Bare verb = default container (literally named `crosstalk`).
51
+
52
52
  | Group | Subcommand | What it does |
53
53
  |---|---|---|
54
54
  | Meta | `crosstalk version` | Print client + engine versions (warns on skew) |
55
- | Protocol | `crosstalk init <dir>` | Scaffold a transport + `git init` |
55
+ | Protocol | `crosstalk init [-c <name>] [--remote <url>]` | Scaffold a transport at `<base>/<name>/transport/` |
56
56
  | Protocol | `crosstalk run --type primitive\|workflow ...` | Dispatch a primitive or workflow |
57
57
  | Protocol | `crosstalk replies <relPath>...` | Poll reply status |
58
- | Protocol | `crosstalk status` | Engine heartbeat, claimed models, channels |
58
+ | Protocol | `crosstalk status` | Container name, transport host path, engine state, channels |
59
59
  | Protocol | `crosstalk channel <name> [--rename\|--delete]` | Channel operations |
60
- | Lifecycle | `crosstalk up` | Generate docker-compose, start engine container |
61
- | Lifecycle | `crosstalk down [--volumes]` | Stop the engine container |
60
+ | Lifecycle | `crosstalk up` | Generate docker-compose, start the engine container |
61
+ | Lifecycle | `crosstalk down` | Stop the engine container (keep storage) |
62
+ | Lifecycle | `crosstalk rm [--force]` | Remove container AND wipe storage (cannot undo) |
62
63
  | Lifecycle | `crosstalk restart` | Down + up |
63
64
  | Lifecycle | `crosstalk pull` | Refresh the engine image |
64
65
  | Lifecycle | `crosstalk logs [-f]` | Stream the engine container's logs |
@@ -72,10 +73,14 @@ crosstalk replies <relPath printed by step above>
72
73
 
73
74
  ## What lives where
74
75
 
75
- - **Host** — `crosstalk` binary on PATH, `~/path/to/mytransport/` (the operator's transport directory, bind-mounted).
76
- - **Container** (`crosstalk-server`, image `ghcr.io/cordfuse/crosstalk-server`) — `crosstalkd` daemon, dispatch state bind-mounted at `<base>/<transport>/crosstalk-state:/var/lib/crosstalk-state`, operator-installed agent CLIs + auth tokens bind-mounted at `<base>/<transport>/crosstalk-root:/crosstalk-root`. `<base>` is per-OS (`$XDG_DATA_HOME/crosstalk` on Linux, `~/Library/Application Support/crosstalk` on macOS, `%LOCALAPPDATA%\crosstalk` on Windows); `CROSSTALK_STORAGE_MODE=system` switches to `/var/lib`-style paths instead.
77
- - **Engine HTTP API** — `127.0.0.1:7000` (compose `127.0.0.1:HOST:CONTAINER` port mapping is the actual security boundary; engine binds 0.0.0.0 inside the container).
78
- - **Transport bind-mount** `$PWD:/var/lib/crosstalk-transport`. Operator git commands on the host are seen by the engine immediately.
76
+ - **Host** — `crosstalk` binary on PATH. Per-transport storage at `<base>/<name>/` (one tree per container).
77
+ - **Container** (literally named whatever the operator picked at `init`, default `crosstalk`; image `ghcr.io/cordfuse/crosstalk-server`) — `crosstalkd` daemon, bind-mounted from:
78
+ - `<base>/<name>/transport/` `/var/lib/crosstalk-transport` (runtime-owned git repo)
79
+ - `<base>/<name>/crosstalk-root/` `/crosstalk-root` (operator CLIs + auth)
80
+ - `<base>/<name>/crosstalk-state/` → `/var/lib/crosstalk-state` (cursor + heartbeat)
81
+ - **`<base>`** is per-OS user-mode: `$XDG_DATA_HOME/crosstalk` on Linux, `~/Library/Application Support/crosstalk` on macOS, `%LOCALAPPDATA%\crosstalk` on Windows. `CROSSTALK_STORAGE_MODE=system` switches to `/var/lib`-style paths.
82
+ - **Engine HTTP API** — `127.0.0.1:<port>`. Default container = port 7000; named containers allocate at init from 7001 upward, recorded in `<base>/<name>/api-port`.
83
+ - **Container identification** — every crosstalkd container gets the Docker label `crosstalk.transport=true`. The client uses the label for inventory queries, not name-pattern matching.
79
84
 
80
85
  ---
81
86
 
package/bin/crosstalk.js CHANGED
@@ -30,6 +30,8 @@ const SUBCOMMANDS = {
30
30
  init: 'init.js',
31
31
  up: 'up.js',
32
32
  down: 'down.js',
33
+ rm: 'rm.js',
34
+ auth: 'auth.js',
33
35
  restart: 'restart.js',
34
36
  pull: 'pull.js',
35
37
  logs: 'logs.js',
@@ -0,0 +1,218 @@
1
+ // crosstalk auth — manage agent auth tokens/keys.
2
+ //
3
+ // One subverb: `paste`. Operator runs the agent's headless-token
4
+ // generator on their host (`claude setup-token`, etc.), pastes the
5
+ // resulting token at our prompt, and we persist it in the container's
6
+ // env so subsequent dispatch + chat invocations are authenticated.
7
+ //
8
+ // Storage: <base>/<name>/auth.env (0600, operator-UID owned). Docker
9
+ // Compose's `env_file:` references it; each line is `KEY=value`.
10
+ // Existing keys are replaced in-place; new keys append. Empty lines
11
+ // and `#` comments preserved.
12
+ //
13
+ // Why this exists: 5 of the 6 supported agents work cleanly with env-
14
+ // var auth (the headless path), but none of them have a uniform "set
15
+ // the token" UX inside the container. This verb is the operator's
16
+ // one-stop shop — same UX for any agent.
17
+ //
18
+ // Per-agent env var (the audit on 2026-06-14 confirmed each):
19
+ // claude → CLAUDE_CODE_OAUTH_TOKEN (preferred: `claude setup-token`)
20
+ // ANTHROPIC_API_KEY (alternate: Console API key)
21
+ // codex → OPENAI_API_KEY (preferred)
22
+ // CODEX_ACCESS_TOKEN (alternate: device-auth output)
23
+ // gemini → GEMINI_API_KEY
24
+ // qwen → OPENAI_API_KEY (qwen --auth-type openai)
25
+ // agy → none (interactive OAuth only; errors with hint)
26
+
27
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
28
+ import { createInterface } from 'readline';
29
+ import { has, flag } from '../lib/argv.js';
30
+ import { requireInitialized, DEFAULT_CONTAINER_NAME } from '../lib/resolve.js';
31
+
32
+ const AGENT_ENV = {
33
+ claude: { default: 'CLAUDE_CODE_OAUTH_TOKEN', label: 'OAuth token' },
34
+ codex: { default: 'OPENAI_API_KEY', label: 'API key' },
35
+ gemini: { default: 'GEMINI_API_KEY', label: 'API key' },
36
+ qwen: { default: 'OPENAI_API_KEY', label: 'API key' },
37
+ opencode: { default: 'OPENAI_API_KEY', label: 'provider API key' },
38
+ };
39
+
40
+ const AGENT_GEN_CMD = {
41
+ claude: 'claude setup-token # on a host with a browser',
42
+ codex: 'echo $OPENAI_API_KEY # or platform.openai.com/api-keys',
43
+ gemini: 'echo $GEMINI_API_KEY # or aistudio.google.com/apikey',
44
+ qwen: 'echo $OPENAI_API_KEY # OpenAI-compatible',
45
+ opencode: 'echo $OPENAI_API_KEY # or provider-specific',
46
+ };
47
+
48
+ // Control byte values used by the TTY prompt — avoid embedding raw
49
+ // bytes inline so the file is editor/linter clean.
50
+ const CTRL_C = String.fromCharCode(3); // 0x03 — interrupt
51
+ const CTRL_D = String.fromCharCode(4); // 0x04 — EOF
52
+ const BS = String.fromCharCode(8); // 0x08 — backspace
53
+ const DEL = String.fromCharCode(127); // 0x7F — terminal backspace
54
+
55
+ function usage(exit = 0) {
56
+ const w = exit === 0 ? process.stdout : process.stderr;
57
+ w.write(
58
+ `Usage: crosstalk auth paste --agent <name> [--containername <name>]
59
+
60
+ Persists an auth token/key for an agent CLI into the container's
61
+ environment. Operator generates the value on the host (which has a
62
+ browser) and pastes at the prompt; crosstalk writes it to
63
+ <base>/<name>/auth.env. Subsequent dispatch + chat invocations pick
64
+ it up via the engine container's env.
65
+
66
+ Options:
67
+ --agent <name> One of: claude, codex, gemini, qwen, opencode.
68
+ agy is interactive-only; use 'crosstalk chat
69
+ --agent agy --login' for OAuth instead.
70
+ --containername <name> Which crosstalk container to set the env on
71
+ (default: 'crosstalk').
72
+ -c <name> Shorthand for --containername.
73
+
74
+ Reads from stdin (hidden) — if the input isn't a TTY, the entire stdin
75
+ is read as the token (e.g. piped from a setup script).
76
+
77
+ After paste, run 'crosstalk restart' to pick up the new env.
78
+ `,
79
+ );
80
+ process.exit(exit);
81
+ }
82
+
83
+ function promptHidden(promptText) {
84
+ return new Promise((resolve) => {
85
+ process.stderr.write(promptText);
86
+ if (!process.stdin.isTTY) {
87
+ let buf = '';
88
+ process.stdin.on('data', (chunk) => { buf += chunk.toString(); });
89
+ process.stdin.on('end', () => resolve(buf.trim()));
90
+ return;
91
+ }
92
+ if (typeof process.stdin.setRawMode !== 'function') {
93
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
94
+ rl.question('', (ans) => { rl.close(); resolve(ans.trim()); });
95
+ return;
96
+ }
97
+ process.stdin.setRawMode(true);
98
+ process.stdin.resume();
99
+ process.stdin.setEncoding('utf-8');
100
+ let buf = '';
101
+ const onData = (chunk) => {
102
+ for (const ch of chunk) {
103
+ if (ch === '\r' || ch === '\n') {
104
+ process.stderr.write('\n');
105
+ process.stdin.setRawMode(false);
106
+ process.stdin.pause();
107
+ process.stdin.removeListener('data', onData);
108
+ resolve(buf.trim());
109
+ return;
110
+ }
111
+ if (ch === CTRL_C || ch === CTRL_D) {
112
+ process.stderr.write('\n');
113
+ process.stdin.setRawMode(false);
114
+ process.exit(130);
115
+ }
116
+ if (ch === BS || ch === DEL) {
117
+ buf = buf.slice(0, -1);
118
+ continue;
119
+ }
120
+ buf += ch;
121
+ }
122
+ };
123
+ process.stdin.on('data', onData);
124
+ });
125
+ }
126
+
127
+ // Upsert a KEY=value line in env-file content. Preserves other lines +
128
+ // comments. Returns the new content.
129
+ function upsertEnv(existing, key, value) {
130
+ const lines = existing.split('\n');
131
+ let replaced = false;
132
+ const out = lines.map((line) => {
133
+ if (line.startsWith(`${key}=`)) {
134
+ replaced = true;
135
+ return `${key}=${value}`;
136
+ }
137
+ return line;
138
+ });
139
+ if (!replaced) {
140
+ while (out.length > 0 && out[out.length - 1] === '') out.pop();
141
+ out.push(`${key}=${value}`);
142
+ out.push('');
143
+ }
144
+ return out.join('\n');
145
+ }
146
+
147
+ export async function run(argv) {
148
+ if (has(argv, '--help') || has(argv, '-h')) usage(0);
149
+
150
+ const pos = argv.filter((a) => !a.startsWith('-'));
151
+ const subverb = pos[0];
152
+ if (subverb !== 'paste') {
153
+ if (subverb) {
154
+ process.stderr.write(`crosstalk auth: unknown subverb '${subverb}'. Try 'paste'.\n`);
155
+ } else {
156
+ usage(1);
157
+ }
158
+ return 1;
159
+ }
160
+
161
+ const agent = flag(argv, '--agent');
162
+ if (!agent) {
163
+ process.stderr.write(`crosstalk auth paste: --agent <name> is required.\n`);
164
+ return 1;
165
+ }
166
+
167
+ if (agent === 'agy') {
168
+ process.stderr.write(
169
+ `crosstalk auth paste: agy has no headless env-var path — its only auth is interactive OAuth.\n` +
170
+ ` Use 'crosstalk chat --agent agy --login' to set up auth.\n`,
171
+ );
172
+ return 1;
173
+ }
174
+
175
+ const cfg = AGENT_ENV[agent];
176
+ if (!cfg) {
177
+ process.stderr.write(
178
+ `crosstalk auth paste: unknown agent '${agent}'.\n` +
179
+ ` Supported: ${Object.keys(AGENT_ENV).join(', ')}.\n`,
180
+ );
181
+ return 1;
182
+ }
183
+
184
+ const { name, paths } = requireInitialized(argv);
185
+
186
+ if (process.stdin.isTTY) {
187
+ process.stderr.write(
188
+ `\nPasting ${cfg.label} for ${agent} into '${name}' container.\n` +
189
+ ` Env var: ${cfg.default}\n` +
190
+ ` Storage: ${paths.authEnvFile}\n` +
191
+ `\nGenerate on host:\n ${AGENT_GEN_CMD[agent] ?? '(see agent docs)'}\n` +
192
+ `\nPaste the value below (input hidden, press Enter):\n`,
193
+ );
194
+ }
195
+ const value = await promptHidden('> ');
196
+ if (!value) {
197
+ process.stderr.write(`crosstalk auth paste: empty input — aborted.\n`);
198
+ return 1;
199
+ }
200
+ if (value.includes('\n')) {
201
+ process.stderr.write(
202
+ `crosstalk auth paste: input contains a newline. Tokens are single-line; check copy/paste.\n`,
203
+ );
204
+ return 1;
205
+ }
206
+
207
+ const existing = existsSync(paths.authEnvFile)
208
+ ? readFileSync(paths.authEnvFile, 'utf-8')
209
+ : '';
210
+ const updated = upsertEnv(existing, cfg.default, value);
211
+ writeFileSync(paths.authEnvFile, updated, { mode: 0o600 });
212
+
213
+ process.stdout.write(
214
+ `\nSaved ${cfg.default} to ${paths.authEnvFile}.\n` +
215
+ `Run 'crosstalk restart${name === DEFAULT_CONTAINER_NAME ? '' : ` --containername ${name}`}' to pick up the new env.\n`,
216
+ );
217
+ return 0;
218
+ }
@@ -4,7 +4,7 @@
4
4
  // endpoints that haven't been added to the engine yet; they land
5
5
  // when the engine gains them.
6
6
 
7
- import { api } from '../lib/api-client.js';
7
+ import { apiFor } from '../lib/api-client.js';
8
8
  import { reportAndExit } from '../lib/errors.js';
9
9
  import { positionals, has, flag } from '../lib/argv.js';
10
10
 
@@ -25,7 +25,8 @@ function usage(exit = 0) {
25
25
  export async function run(argv) {
26
26
  if (has(argv, '--help') || has(argv, '-h')) usage(0);
27
27
 
28
- const pos = positionals(argv, ['--rename']);
28
+ const api = apiFor(argv);
29
+ const pos = positionals(argv, ['--rename', '--containername', '-c']);
29
30
  if (pos.length === 0) usage(1);
30
31
 
31
32
  // `crosstalk channel list` → list channels
package/commands/chat.js CHANGED
@@ -23,8 +23,8 @@
23
23
  // fallback, not the primary mechanism.
24
24
 
25
25
  import { spawn, spawnSync } from 'child_process';
26
- import { requireTransportRoot, containerName } from '../lib/transport.js';
27
- import { api, ConnectError } from '../lib/api-client.js';
26
+ import { requireInitialized } from '../lib/resolve.js';
27
+ import { apiFor, ConnectError } from '../lib/api-client.js';
28
28
  import { flag, has } from '../lib/argv.js';
29
29
 
30
30
  const KNOWN_AGENTS = ['claude', 'codex', 'gemini', 'qwen', 'opencode', 'agy'];
@@ -132,6 +132,17 @@ Examples:
132
132
  process.exit(exit);
133
133
  }
134
134
 
135
+ // Detect host terminal dims with sane fallbacks. Pass into the
136
+ // container via -e COLUMNS/LINES so the agent CLI renders at the
137
+ // operator's real terminal size instead of docker's 80×24 fallback.
138
+ // (When stdout is piped — like in --login — docker can't auto-detect
139
+ // the host's stdout terminal, which is mac's alpha.6 bug D.)
140
+ function hostTermDims() {
141
+ const cols = process.stdout.columns || process.stderr.columns || 120;
142
+ const rows = process.stdout.rows || process.stderr.rows || 40;
143
+ return { cols, rows };
144
+ }
145
+
135
146
  function runInteractive(container, command, env = {}) {
136
147
  // Full TTY passthrough — docker exec -it gives the container a real
137
148
  // PTY, and stdio: 'inherit' forwards stdin/stdout/stderr from the
@@ -147,30 +158,79 @@ function runInteractive(container, command, env = {}) {
147
158
  return r.status ?? 1;
148
159
  }
149
160
 
161
+ // Per-agent login command. Returns null for agents without an OAuth
162
+ // path (gemini, qwen, opencode) — these need env-var auth, not browser
163
+ // OAuth. The full audit (cachy 2026-06-14) confirmed each agent's
164
+ // surface:
165
+ // claude → `claude auth login` (clean single-line URL output)
166
+ // codex → `codex login` (clean single-line URL output)
167
+ // agy → `agy --print init` (triggers OAuth, clean URL)
168
+ // We use the explicit auth subcommand instead of bare invocation —
169
+ // that's mac's bug D fix root cause. Bare `claude` drops into a TUI
170
+ // with line wraps + winsize sensitivity that broke URL extraction
171
+ // across alpha.3 → alpha.6. The official auth subcommand prints a
172
+ // plain single-line URL on stdout, no TUI involved.
173
+ function loginCommand(agent) {
174
+ if (agent === 'claude') return ['claude', 'auth', 'login'];
175
+ if (agent === 'codex') return ['codex', 'login'];
176
+ if (agent === 'agy') return ['agy', '--print', 'init'];
177
+ return null; // gemini, qwen, opencode — env-based
178
+ }
179
+
180
+ // Per-agent env-var hint for the no-OAuth agents.
181
+ function envHint(agent) {
182
+ if (agent === 'gemini') return 'GEMINI_API_KEY';
183
+ if (agent === 'qwen') return 'OPENAI_API_KEY (qwen --auth-type openai)';
184
+ if (agent === 'opencode') return 'per-provider keys in opencode config';
185
+ return null;
186
+ }
187
+
150
188
  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).
189
+ const cmd = loginCommand(agent);
190
+ if (!cmd) {
191
+ const env = envHint(agent);
192
+ process.stderr.write(
193
+ `crosstalk chat --login --agent ${agent}: this agent has no browser OAuth flow.\n` +
194
+ (env ? ` Use environment-based auth instead:\n crosstalk auth paste --agent ${agent}\n (or set ${env} directly in docker-compose.yml)\n` : '') +
195
+ ` Documented in GUIDE-CLI.md § Agent auth.\n`,
196
+ );
197
+ return Promise.resolve(1);
198
+ }
199
+
200
+ // Pass host terminal dims explicitly. Docker exec -t normally sizes
201
+ // the container PTY off its stdout terminal, but we PIPE stdout (to
202
+ // scrape the OAuth URL), so docker has no TTY to size from and falls
203
+ // back to 80×24 that's mac's bug D root cause. Setting COLUMNS/LINES
204
+ // env vars gets the agent CLI to render at the right size from the
205
+ // start. (Live SIGWINCH updates aren't propagated docker env is
206
+ // set-at-spawn — but OAuth flows are short; operators don't typically
207
+ // resize the terminal mid-auth.)
208
+ const { cols, rows } = hostTermDims();
209
+
168
210
  return new Promise((resolve) => {
169
211
  const child = spawn(
170
212
  'docker',
171
- ['exec', '-it', container, agent],
213
+ ['exec', '-it',
214
+ '-e', `COLUMNS=${cols}`,
215
+ '-e', `LINES=${rows}`,
216
+ '-e', 'TERM=xterm-256color',
217
+ container, ...cmd],
172
218
  { stdio: ['inherit', 'pipe', 'inherit'] },
173
219
  );
220
+
221
+ // SIGWINCH handler: docker env is set-at-spawn so we can't dynamically
222
+ // resize the in-container PTY without a control channel (would need
223
+ // node-pty or a sidecar process). Best-effort: surface that a resize
224
+ // happened so the operator knows why the TUI may not have responded.
225
+ const onWinch = () => {
226
+ const { cols: c, rows: r } = hostTermDims();
227
+ process.stderr.write(
228
+ `\n[crosstalk chat] Terminal resized to ${c}×${r} — in-container PTY won't update mid-session. ` +
229
+ `Exit (Ctrl-C) and retry if rendering is off.\n`,
230
+ );
231
+ };
232
+ process.on('SIGWINCH', onWinch);
233
+
174
234
  let opened = false;
175
235
  let buffer = '';
176
236
  child.stdout.on('data', (chunk) => {
@@ -187,16 +247,22 @@ function runLogin(container, agent) {
187
247
  : `\n[crosstalk chat] Could not auto-open browser. Open this URL manually:\n ${url}\n`,
188
248
  );
189
249
  });
190
- child.on('exit', (code) => resolve(code ?? 0));
191
- child.on('error', () => resolve(1));
250
+ child.on('exit', (code) => {
251
+ process.removeListener('SIGWINCH', onWinch);
252
+ resolve(code ?? 0);
253
+ });
254
+ child.on('error', () => {
255
+ process.removeListener('SIGWINCH', onWinch);
256
+ resolve(1);
257
+ });
192
258
  });
193
259
  }
194
260
 
195
261
  export async function run(argv) {
196
262
  if (has(argv, '--help') || has(argv, '-h')) usage(0);
197
263
 
198
- const root = requireTransportRoot();
199
- const name = containerName(root);
264
+ const { name } = requireInitialized(argv);
265
+ const api = apiFor(argv);
200
266
 
201
267
  if (has(argv, '--shell')) {
202
268
  return runInteractive(name, 'bash');
package/commands/down.js CHANGED
@@ -1,18 +1,22 @@
1
- // crosstalk down — stop the engine container for this transport.
1
+ // crosstalk down — stop a transport's container, keep storage.
2
+ //
3
+ // alpha.6: name-resolved. --volumes flag retires (no named volumes in
4
+ // the bind-mount model). Reversible — next `up` resumes against the
5
+ // preserved <base>/<name>/ storage.
2
6
 
3
7
  import { existsSync } from 'fs';
4
8
  import { spawnSync } from 'child_process';
5
- import { requireTransportRoot, composeFile } from '../lib/transport.js';
6
9
  import { has } from '../lib/argv.js';
10
+ import { requireInitialized, DEFAULT_CONTAINER_NAME } from '../lib/resolve.js';
7
11
 
8
12
  function usage(exit = 0) {
9
13
  const w = exit === 0 ? process.stdout : process.stderr;
10
14
  w.write(
11
- `Usage: crosstalk down [--volumes]
15
+ `Usage: crosstalk down [--containername <name>]
12
16
 
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).
17
+ Stops the engine container for a transport. Storage at <base>/<name>/
18
+ is preserved next 'crosstalk up' resumes against it. Use
19
+ 'crosstalk rm' if you want to wipe storage as well.
16
20
  `,
17
21
  );
18
22
  process.exit(exit);
@@ -21,17 +25,16 @@ installed CLIs + auth state; you'll have to re-install + re-auth).
21
25
  export async function run(argv) {
22
26
  if (has(argv, '--help') || has(argv, '-h')) usage(0);
23
27
 
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`);
28
+ const { name, paths } = requireInitialized(argv);
29
+ if (!existsSync(paths.composeFile)) {
30
+ process.stderr.write(
31
+ `crosstalk down: '${name}' has no compose file (never brought up). Nothing to do.\n`,
32
+ );
29
33
  return 0;
30
34
  }
31
35
 
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
+ const r = spawnSync('docker', ['compose', '-f', paths.composeFile, 'down'], {
37
+ stdio: 'inherit',
38
+ });
36
39
  return r.status ?? 1;
37
40
  }