@cordfuse/crosstalk 7.0.0-alpha.1 → 7.0.0-alpha.11
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 +82 -11
- package/bin/crosstalk.js +1 -0
- package/commands/channel.js +63 -14
- package/commands/chat.js +66 -106
- package/commands/down.js +18 -15
- package/commands/init.js +206 -68
- package/commands/logs.js +8 -10
- package/commands/pull.js +7 -9
- package/commands/replies.js +4 -3
- package/commands/restart.js +16 -11
- package/commands/rm.js +109 -0
- package/commands/run.js +9 -15
- package/commands/status.js +53 -9
- package/commands/up.js +81 -75
- package/commands/version.js +5 -2
- package/lib/api-client.js +75 -14
- package/lib/argv.js +4 -2
- package/lib/resolve.js +191 -0
- package/package.json +1 -1
- package/lib/transport.js +0 -51
package/commands/init.js
CHANGED
|
@@ -1,42 +1,73 @@
|
|
|
1
|
-
// crosstalk init
|
|
1
|
+
// crosstalk init — scaffold a transport into <base>/<name>/.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
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
|
-
//
|
|
12
|
-
//
|
|
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.
|
|
13
21
|
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// operator's UID/GID keeps file ownership sane.
|
|
22
|
+
// Each phase reports created/skipped/repaired so operators can tell
|
|
23
|
+
// what just happened.
|
|
17
24
|
|
|
18
|
-
import { mkdirSync, existsSync } from 'fs';
|
|
19
|
-
import { resolve } from 'path';
|
|
25
|
+
import { mkdirSync, existsSync, writeFileSync, readFileSync } from 'fs';
|
|
20
26
|
import { spawnSync } from 'child_process';
|
|
21
|
-
import {
|
|
27
|
+
import { join } from 'path';
|
|
28
|
+
import { has, flag } from '../lib/argv.js';
|
|
29
|
+
import {
|
|
30
|
+
parseContainerName,
|
|
31
|
+
storagePaths,
|
|
32
|
+
pickFreePort,
|
|
33
|
+
DEFAULT_CONTAINER_NAME,
|
|
34
|
+
} from '../lib/resolve.js';
|
|
22
35
|
|
|
23
36
|
const DEFAULT_IMAGE = process.env.CROSSTALK_IMAGE
|
|
24
|
-
?? 'ghcr.io/cordfuse/crosstalk-server:7.0.0-alpha.
|
|
37
|
+
?? 'ghcr.io/cordfuse/crosstalk-server:7.0.0-alpha.8';
|
|
25
38
|
|
|
26
39
|
function usage(exit = 0) {
|
|
27
40
|
const w = exit === 0 ? process.stdout : process.stderr;
|
|
28
41
|
w.write(
|
|
29
42
|
`Usage:
|
|
30
|
-
crosstalk init <
|
|
43
|
+
crosstalk init [--containername <name>] [--remote <url>]
|
|
31
44
|
|
|
32
|
-
|
|
33
|
-
|
|
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.
|
|
34
48
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
49
|
+
The transport git repo lives at <base>/<name>/transport/ where <base> is:
|
|
50
|
+
Linux: $XDG_DATA_HOME/crosstalk/ (or ~/.local/share/crosstalk/)
|
|
51
|
+
macOS: ~/Library/Application Support/crosstalk/
|
|
52
|
+
Windows: %LOCALAPPDATA%\\crosstalk\\
|
|
53
|
+
|
|
54
|
+
Set CROSSTALK_STORAGE_MODE=system to use the system-wide path (sudo/UAC
|
|
55
|
+
required; intended for headless multi-operator hosts).
|
|
56
|
+
|
|
57
|
+
Options:
|
|
58
|
+
--containername <name> Container name (default: 'crosstalk'). Must be
|
|
59
|
+
lowercase alphanumeric + ._- (no leading . or -).
|
|
60
|
+
One default + N named containers per machine.
|
|
61
|
+
--remote <url> Set git upstream. If a different remote is
|
|
62
|
+
already set, errors instead of overwriting.
|
|
63
|
+
--image <tag> Override the engine image (default: bundled
|
|
64
|
+
GHCR tag; or CROSSTALK_IMAGE env).
|
|
65
|
+
-c <name> Shorthand for --containername.
|
|
66
|
+
|
|
67
|
+
Examples:
|
|
68
|
+
crosstalk init # default container
|
|
69
|
+
crosstalk init --containername uat # second container
|
|
70
|
+
crosstalk init --remote git\\@github.com:me/t.git # with upstream
|
|
40
71
|
`,
|
|
41
72
|
);
|
|
42
73
|
process.exit(exit);
|
|
@@ -44,62 +75,169 @@ function usage(exit = 0) {
|
|
|
44
75
|
|
|
45
76
|
export async function run(argv) {
|
|
46
77
|
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
78
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
79
|
+
let name;
|
|
80
|
+
try {
|
|
81
|
+
name = parseContainerName(argv);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
process.stderr.write(`crosstalk init: ${err.message}\n`);
|
|
84
|
+
return 1;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const remote = flag(argv, '--remote');
|
|
88
|
+
const image = flag(argv, '--image') ?? DEFAULT_IMAGE;
|
|
89
|
+
const paths = storagePaths(name);
|
|
90
|
+
const report = [];
|
|
91
|
+
|
|
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.).
|
|
96
|
+
try {
|
|
97
|
+
mkdirSync(paths.transportDir, { recursive: true });
|
|
98
|
+
mkdirSync(paths.crosstalkRoot, { recursive: true });
|
|
99
|
+
mkdirSync(paths.crosstalkState, { recursive: true });
|
|
100
|
+
} catch (err) {
|
|
101
|
+
process.stderr.write(
|
|
102
|
+
`crosstalk init: failed to create storage at ${paths.storageRoot} — ${err.message}\n`,
|
|
103
|
+
);
|
|
104
|
+
if (paths.mode === 'system') {
|
|
105
|
+
process.stderr.write(
|
|
106
|
+
` System-mode storage requires write access to ${paths.base}.\n` +
|
|
107
|
+
` Try: sudo mkdir -p ${paths.storageRoot} && sudo chown -R $USER ${paths.storageRoot}\n`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return 1;
|
|
111
|
+
}
|
|
112
|
+
|
|
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
|
+
}
|
|
131
|
+
|
|
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;
|
|
76
181
|
}
|
|
77
182
|
|
|
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
183
|
const gitSteps = [
|
|
87
184
|
['init', '--quiet', '--initial-branch=main'],
|
|
88
185
|
['add', '-A'],
|
|
89
186
|
['commit', '--quiet', '-m', 'initial transport'],
|
|
90
187
|
];
|
|
188
|
+
let gitOk = true;
|
|
91
189
|
for (const args of gitSteps) {
|
|
92
|
-
const gr = spawnSync('git', args, { cwd:
|
|
190
|
+
const gr = spawnSync('git', args, { cwd: paths.transportDir, stdio: 'pipe' });
|
|
93
191
|
if (gr.status !== 0) {
|
|
94
|
-
|
|
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;
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
report.push(gitOk ? ` git: initialized + committed` : ` git: already present (skipped)`);
|
|
202
|
+
|
|
203
|
+
// ── Phase 4: Remote setup ──────────────────────────────────────────
|
|
204
|
+
if (remote) {
|
|
205
|
+
const cur = spawnSync('git', ['remote', 'get-url', 'origin'], {
|
|
206
|
+
cwd: paths.transportDir,
|
|
207
|
+
stdio: 'pipe',
|
|
208
|
+
});
|
|
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 {
|
|
95
225
|
process.stderr.write(
|
|
96
|
-
`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`,
|
|
97
228
|
);
|
|
98
|
-
|
|
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
|
|
229
|
+
return 1;
|
|
101
230
|
}
|
|
102
231
|
}
|
|
103
|
-
|
|
232
|
+
|
|
233
|
+
// ── Phase 5: Summary ───────────────────────────────────────────────
|
|
234
|
+
process.stdout.write(
|
|
235
|
+
`\nTransport '${name}' ready:\n` +
|
|
236
|
+
` storage: ${paths.storageRoot}\n` +
|
|
237
|
+
` transport: ${paths.transportDir}\n` +
|
|
238
|
+
` image: ${image}\n` +
|
|
239
|
+
report.join('\n') + '\n' +
|
|
240
|
+
`\nNext: crosstalk up${name === DEFAULT_CONTAINER_NAME ? '' : ` --containername ${name}`}\n`,
|
|
241
|
+
);
|
|
104
242
|
return 0;
|
|
105
243
|
}
|
package/commands/logs.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
// crosstalk logs — tail
|
|
1
|
+
// crosstalk logs — tail a transport's engine logs.
|
|
2
2
|
|
|
3
3
|
import { existsSync } from 'fs';
|
|
4
4
|
import { spawnSync } from 'child_process';
|
|
5
|
-
import { requireTransportRoot, composeFile } from '../lib/transport.js';
|
|
6
5
|
import { has } from '../lib/argv.js';
|
|
6
|
+
import { requireInitialized } from '../lib/resolve.js';
|
|
7
7
|
|
|
8
8
|
function usage(exit = 0) {
|
|
9
9
|
const w = exit === 0 ? process.stdout : process.stderr;
|
|
10
10
|
w.write(
|
|
11
|
-
`Usage: crosstalk logs [-f|--follow] [-n <lines>]
|
|
11
|
+
`Usage: crosstalk logs [--containername <name>] [-f|--follow] [-n <lines>]
|
|
12
12
|
|
|
13
13
|
Streams the engine container's stdout/stderr. -f to follow (Ctrl-C to stop).
|
|
14
14
|
`,
|
|
@@ -19,21 +19,19 @@ Streams the engine container's stdout/stderr. -f to follow (Ctrl-C to stop).
|
|
|
19
19
|
export async function run(argv) {
|
|
20
20
|
if (has(argv, '--help') || has(argv, '-h')) usage(0);
|
|
21
21
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
process.stderr.write(`crosstalk logs: no docker-compose.yml — run 'crosstalk up' first.\n`);
|
|
22
|
+
const { name, paths } = requireInitialized(argv);
|
|
23
|
+
if (!existsSync(paths.composeFile)) {
|
|
24
|
+
process.stderr.write(`crosstalk logs: '${name}' has no compose file — run 'crosstalk up' first.\n`);
|
|
26
25
|
return 1;
|
|
27
26
|
}
|
|
28
|
-
const args = ['compose', '-f',
|
|
27
|
+
const args = ['compose', '-f', paths.composeFile, 'logs'];
|
|
29
28
|
if (has(argv, '-f') || has(argv, '--follow')) args.push('--follow');
|
|
30
|
-
// Pass through tail count if provided.
|
|
31
29
|
for (let i = 0; i < argv.length; i++) {
|
|
32
30
|
if (argv[i] === '-n' || argv[i] === '--tail') {
|
|
33
31
|
args.push('--tail', argv[i + 1] ?? '100');
|
|
34
32
|
i++;
|
|
35
33
|
}
|
|
36
34
|
}
|
|
37
|
-
const r = spawnSync('docker', args, {
|
|
35
|
+
const r = spawnSync('docker', args, { stdio: 'inherit' });
|
|
38
36
|
return r.status ?? 1;
|
|
39
37
|
}
|
package/commands/pull.js
CHANGED
|
@@ -1,23 +1,21 @@
|
|
|
1
|
-
// crosstalk pull — refresh the engine image.
|
|
1
|
+
// crosstalk pull — refresh the engine image for a transport.
|
|
2
2
|
|
|
3
3
|
import { existsSync } from 'fs';
|
|
4
4
|
import { spawnSync } from 'child_process';
|
|
5
|
-
import { requireTransportRoot, composeFile } from '../lib/transport.js';
|
|
6
5
|
import { has } from '../lib/argv.js';
|
|
6
|
+
import { requireInitialized } from '../lib/resolve.js';
|
|
7
7
|
|
|
8
8
|
export async function run(argv) {
|
|
9
9
|
if (has(argv, '--help') || has(argv, '-h')) {
|
|
10
|
-
process.stdout.write('Usage: crosstalk pull\n');
|
|
10
|
+
process.stdout.write('Usage: crosstalk pull [--containername <name>]\n');
|
|
11
11
|
return 0;
|
|
12
12
|
}
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
process.stderr.write(`crosstalk pull: no docker-compose.yml — run 'crosstalk up' first.\n`);
|
|
13
|
+
const { name, paths } = requireInitialized(argv);
|
|
14
|
+
if (!existsSync(paths.composeFile)) {
|
|
15
|
+
process.stderr.write(`crosstalk pull: '${name}' has no compose file — run 'crosstalk up' first.\n`);
|
|
17
16
|
return 1;
|
|
18
17
|
}
|
|
19
|
-
const r = spawnSync('docker', ['compose', '-f',
|
|
20
|
-
cwd: root,
|
|
18
|
+
const r = spawnSync('docker', ['compose', '-f', paths.composeFile, 'pull'], {
|
|
21
19
|
stdio: 'inherit',
|
|
22
20
|
});
|
|
23
21
|
return r.status ?? 1;
|
package/commands/replies.js
CHANGED
|
@@ -3,19 +3,20 @@
|
|
|
3
3
|
// Exit 0 if all targets have REPLIED or FAILED, exit 2 while any PENDING
|
|
4
4
|
// — agents can poll cheaply in a loop. Mirrors crosstalkd replies.
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { apiFor } from '../lib/api-client.js';
|
|
7
7
|
import { reportAndExit } from '../lib/errors.js';
|
|
8
8
|
import { positionals, has } from '../lib/argv.js';
|
|
9
9
|
|
|
10
10
|
function usage(exit = 0) {
|
|
11
11
|
const w = exit === 0 ? process.stdout : process.stderr;
|
|
12
|
-
w.write('Usage: crosstalk replies <relPath> [<relPath>...]\n');
|
|
12
|
+
w.write('Usage: crosstalk replies [--containername <name>] <relPath> [<relPath>...]\n');
|
|
13
13
|
process.exit(exit);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export async function run(argv) {
|
|
17
17
|
if (has(argv, '--help') || has(argv, '-h')) usage(0);
|
|
18
|
-
const
|
|
18
|
+
const api = apiFor(argv);
|
|
19
|
+
const targets = positionals(argv, ['--containername', '-c']);
|
|
19
20
|
if (targets.length === 0) usage(1);
|
|
20
21
|
|
|
21
22
|
let resp;
|
package/commands/restart.js
CHANGED
|
@@ -1,24 +1,29 @@
|
|
|
1
|
-
// crosstalk restart — restart
|
|
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';
|
|
5
|
-
import { requireTransportRoot, composeFile } from '../lib/transport.js';
|
|
6
10
|
import { has } from '../lib/argv.js';
|
|
11
|
+
import { requireInitialized } from '../lib/resolve.js';
|
|
7
12
|
|
|
8
13
|
export async function run(argv) {
|
|
9
14
|
if (has(argv, '--help') || has(argv, '-h')) {
|
|
10
|
-
process.stdout.write('Usage: crosstalk restart\n');
|
|
15
|
+
process.stdout.write('Usage: crosstalk restart [--containername <name>]\n');
|
|
11
16
|
return 0;
|
|
12
17
|
}
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
process.stderr.write(`crosstalk restart: no docker-compose.yml — run 'crosstalk up' first.\n`);
|
|
18
|
+
const { name, paths } = requireInitialized(argv);
|
|
19
|
+
if (!existsSync(paths.composeFile)) {
|
|
20
|
+
process.stderr.write(`crosstalk restart: '${name}' has no compose file — run 'crosstalk up' first.\n`);
|
|
17
21
|
return 1;
|
|
18
22
|
}
|
|
19
|
-
const r = spawnSync(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
const r = spawnSync(
|
|
24
|
+
'docker',
|
|
25
|
+
['compose', '-f', paths.composeFile, 'up', '-d', '--force-recreate'],
|
|
26
|
+
{ stdio: 'inherit' },
|
|
27
|
+
);
|
|
23
28
|
return r.status ?? 1;
|
|
24
29
|
}
|
package/commands/rm.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// crosstalk rm — remove a transport completely.
|
|
2
|
+
//
|
|
3
|
+
// Stops the container if running, removes the container, deletes the
|
|
4
|
+
// entire <base>/<name>/ tree (transport git repo, installed CLIs, auth
|
|
5
|
+
// state, dispatcher state, compose file, port file). Confirmation
|
|
6
|
+
// prompt by default; --force / -f to skip for scripted use.
|
|
7
|
+
//
|
|
8
|
+
// Cannot undo. All message history is lost.
|
|
9
|
+
|
|
10
|
+
import { existsSync, rmSync } from 'fs';
|
|
11
|
+
import { spawnSync } from 'child_process';
|
|
12
|
+
import { createInterface } from 'readline';
|
|
13
|
+
import { has } from '../lib/argv.js';
|
|
14
|
+
import {
|
|
15
|
+
parseContainerName,
|
|
16
|
+
storagePaths,
|
|
17
|
+
isInitialized,
|
|
18
|
+
isRunning,
|
|
19
|
+
containerExists,
|
|
20
|
+
DEFAULT_CONTAINER_NAME,
|
|
21
|
+
} from '../lib/resolve.js';
|
|
22
|
+
|
|
23
|
+
function usage(exit = 0) {
|
|
24
|
+
const w = exit === 0 ? process.stdout : process.stderr;
|
|
25
|
+
w.write(
|
|
26
|
+
`Usage: crosstalk rm [--containername <name>] [--force]
|
|
27
|
+
|
|
28
|
+
Stops and removes the engine container, then deletes the entire
|
|
29
|
+
storage tree under <base>/<name>/. Includes the transport git repo
|
|
30
|
+
(all message history), installed agent CLIs + auth tokens, and
|
|
31
|
+
dispatcher state. CANNOT BE UNDONE.
|
|
32
|
+
|
|
33
|
+
Options:
|
|
34
|
+
--containername <name> Container to remove (default: 'crosstalk').
|
|
35
|
+
--force, -f Skip the confirmation prompt.
|
|
36
|
+
`,
|
|
37
|
+
);
|
|
38
|
+
process.exit(exit);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function confirm(promptText) {
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
44
|
+
rl.question(promptText, (ans) => {
|
|
45
|
+
rl.close();
|
|
46
|
+
const yes = /^y(es)?$/i.test(ans.trim());
|
|
47
|
+
resolve(yes);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function run(argv) {
|
|
53
|
+
if (has(argv, '--help') || has(argv, '-h')) usage(0);
|
|
54
|
+
|
|
55
|
+
let name;
|
|
56
|
+
try {
|
|
57
|
+
name = parseContainerName(argv);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
process.stderr.write(`crosstalk rm: ${err.message}\n`);
|
|
60
|
+
return 1;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const paths = storagePaths(name);
|
|
64
|
+
const haveStorage = isInitialized(name) || existsSync(paths.storageRoot);
|
|
65
|
+
const haveContainer = containerExists(name);
|
|
66
|
+
|
|
67
|
+
if (!haveStorage && !haveContainer) {
|
|
68
|
+
process.stderr.write(`crosstalk rm: transport '${name}' not found.\n`);
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!has(argv, '--force') && !has(argv, '-f')) {
|
|
73
|
+
process.stderr.write(
|
|
74
|
+
`Remove '${name}'?\n` +
|
|
75
|
+
` This deletes:\n` +
|
|
76
|
+
` - transport git repo at ${paths.transportDir}\n` +
|
|
77
|
+
` (all message history, channels, replies)\n` +
|
|
78
|
+
` - installed agent CLIs + auth tokens (claude, codex, ...)\n` +
|
|
79
|
+
` - dispatcher state (cursor, heartbeat, errors.log)\n` +
|
|
80
|
+
` Cannot undo.\n`,
|
|
81
|
+
);
|
|
82
|
+
const ok = await confirm('[y/N]: ');
|
|
83
|
+
if (!ok) {
|
|
84
|
+
process.stderr.write('crosstalk rm: cancelled.\n');
|
|
85
|
+
return 1;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Stop + remove the container (best-effort).
|
|
90
|
+
if (isRunning(name) && existsSync(paths.composeFile)) {
|
|
91
|
+
spawnSync('docker', ['compose', '-f', paths.composeFile, 'down'], { stdio: 'inherit' });
|
|
92
|
+
} else if (haveContainer) {
|
|
93
|
+
spawnSync('docker', ['rm', '-f', name], { stdio: 'ignore' });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Wipe storage.
|
|
97
|
+
try {
|
|
98
|
+
rmSync(paths.storageRoot, { recursive: true, force: true });
|
|
99
|
+
} catch (err) {
|
|
100
|
+
process.stderr.write(
|
|
101
|
+
`crosstalk rm: failed to remove ${paths.storageRoot} — ${err.message}\n` +
|
|
102
|
+
` (container is removed, but storage cleanup needs intervention)\n`,
|
|
103
|
+
);
|
|
104
|
+
return 1;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
process.stdout.write(`Removed '${name}'.\n`);
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
package/commands/run.js
CHANGED
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
// [--new] <file|->
|
|
9
9
|
|
|
10
10
|
import { readFileSync, existsSync, statSync } from 'fs';
|
|
11
|
-
import {
|
|
11
|
+
import { apiFor } from '../lib/api-client.js';
|
|
12
12
|
import { reportAndExit } from '../lib/errors.js';
|
|
13
|
-
import { flag, has } from '../lib/argv.js';
|
|
13
|
+
import { flag, has, positionals } from '../lib/argv.js';
|
|
14
14
|
|
|
15
|
-
const FLAGS_WITH_VALUE = new Set(['--type', '--to', '--as', '--fanout', '--channel', '--from']);
|
|
15
|
+
const FLAGS_WITH_VALUE = new Set(['--type', '--to', '--as', '--fanout', '--channel', '--from', '--containername', '-c']);
|
|
16
16
|
|
|
17
17
|
function usage(exit = 0) {
|
|
18
18
|
const w = exit === 0 ? process.stdout : process.stderr;
|
|
@@ -52,21 +52,15 @@ function resolveBody(arg) {
|
|
|
52
52
|
export async function run(argv) {
|
|
53
53
|
if (has(argv, '--help') || has(argv, '-h')) usage(0);
|
|
54
54
|
|
|
55
|
+
const api = apiFor(argv);
|
|
55
56
|
const type = flag(argv, '--type');
|
|
56
57
|
if (type !== 'primitive' && type !== 'workflow') usage(1);
|
|
57
58
|
|
|
58
|
-
//
|
|
59
|
-
// flag
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (a.startsWith('--')) break;
|
|
64
|
-
// Make sure we're not the value of a flag like `--to <value>`
|
|
65
|
-
const prev = argv[i - 1];
|
|
66
|
-
if (prev && FLAGS_WITH_VALUE.has(prev)) continue;
|
|
67
|
-
bodyArg = a;
|
|
68
|
-
break;
|
|
69
|
-
}
|
|
59
|
+
// Body is the last positional. positionals() skips flag values so the
|
|
60
|
+
// body resolves regardless of flag order — `--containername a8 hello`
|
|
61
|
+
// and `hello --containername a8` both work.
|
|
62
|
+
const pos = positionals(argv, [...FLAGS_WITH_VALUE]);
|
|
63
|
+
const bodyArg = pos[pos.length - 1];
|
|
70
64
|
|
|
71
65
|
const body = resolveBody(bodyArg);
|
|
72
66
|
if (body == null || body.length === 0) {
|