@cordfuse/crosstalk 7.0.0-alpha.4 → 7.0.0-alpha.6
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 +23 -18
- package/bin/crosstalk.js +1 -0
- package/commands/channel.js +3 -2
- package/commands/chat.js +4 -4
- package/commands/down.js +18 -15
- package/commands/init.js +146 -56
- package/commands/logs.js +8 -10
- package/commands/pull.js +7 -9
- package/commands/replies.js +4 -3
- package/commands/restart.js +7 -9
- package/commands/rm.js +109 -0
- package/commands/run.js +4 -3
- package/commands/status.js +15 -10
- package/commands/up.js +81 -75
- package/commands/version.js +3 -2
- package/lib/api-client.js +36 -17
- package/lib/argv.js +4 -2
- package/lib/resolve.js +181 -0
- package/package.json +1 -1
- package/lib/transport.js +0 -51
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
13
|
import { flag, has } 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,6 +52,7 @@ 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
|
|
|
@@ -60,7 +61,7 @@ export async function run(argv) {
|
|
|
60
61
|
let bodyArg;
|
|
61
62
|
for (let i = argv.length - 1; i >= 0; i--) {
|
|
62
63
|
const a = argv[i];
|
|
63
|
-
if (a.startsWith('
|
|
64
|
+
if (a.startsWith('-')) break;
|
|
64
65
|
// Make sure we're not the value of a flag like `--to <value>`
|
|
65
66
|
const prev = argv[i - 1];
|
|
66
67
|
if (prev && FLAGS_WITH_VALUE.has(prev)) continue;
|
package/commands/status.js
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
// crosstalk status — mirror of crosstalkd status output, fetched via API.
|
|
2
|
+
//
|
|
3
|
+
// alpha.6: also prints the host path of the transport git repo as an
|
|
4
|
+
// escape hatch for operators who want to poke around with raw git.
|
|
2
5
|
|
|
3
|
-
import {
|
|
6
|
+
import { apiFor } from '../lib/api-client.js';
|
|
4
7
|
import { reportAndExit } from '../lib/errors.js';
|
|
8
|
+
import { requireInitialized } from '../lib/resolve.js';
|
|
9
|
+
|
|
10
|
+
export async function run(argv) {
|
|
11
|
+
const { name, paths } = requireInitialized(argv);
|
|
12
|
+
const api = apiFor(argv);
|
|
5
13
|
|
|
6
|
-
export async function run(_argv) {
|
|
7
14
|
let s;
|
|
8
15
|
try {
|
|
9
16
|
s = await api.get('/status');
|
|
@@ -11,25 +18,23 @@ export async function run(_argv) {
|
|
|
11
18
|
reportAndExit(err, 'crosstalk status');
|
|
12
19
|
}
|
|
13
20
|
|
|
14
|
-
process.stdout.write(`
|
|
21
|
+
process.stdout.write(`Container: ${name}\n`);
|
|
22
|
+
process.stdout.write(`Transport host path: ${paths.transportDir}\n`);
|
|
15
23
|
process.stdout.write(`Engine alias: ${s.alias}\n`);
|
|
16
24
|
process.stdout.write(`Engine version: ${s.version}\n`);
|
|
17
25
|
process.stdout.write('\n');
|
|
18
26
|
|
|
19
27
|
process.stdout.write(`Claimed models: ${s.claimed_models.length}\n`);
|
|
20
28
|
if (s.claimed_models.length === 0) {
|
|
21
|
-
// Two failure shapes look the same in claimed_models=0: (a) no CLIs
|
|
22
|
-
// installed at all, (b) CLIs installed but data/models.yaml has no
|
|
23
|
-
// entries referencing them. Ask /agents/installed to distinguish so
|
|
24
|
-
// the operator gets a directly-actionable hint.
|
|
25
29
|
let installed = null;
|
|
26
30
|
try {
|
|
27
31
|
const r = await api.get('/agents/installed');
|
|
28
32
|
installed = r.installed;
|
|
29
33
|
} catch { /* ignore — fall back to generic message */ }
|
|
30
34
|
if (installed && installed.length > 0) {
|
|
35
|
+
const verb = installed.length === 1 ? 'references it' : 'references them';
|
|
31
36
|
process.stdout.write(
|
|
32
|
-
` (${installed.join(', ')} installed in the container but no data/models.yaml entry
|
|
37
|
+
` (${installed.join(', ')} installed in the container but no data/models.yaml entry ${verb} — add one to claim)\n`,
|
|
33
38
|
);
|
|
34
39
|
} else {
|
|
35
40
|
process.stdout.write(' (no model CLIs found inside the container — install one via `crosstalk chat --shell` and add an entry to data/models.yaml)\n');
|
|
@@ -56,9 +61,9 @@ export async function run(_argv) {
|
|
|
56
61
|
|
|
57
62
|
process.stdout.write(`Channels: ${s.channels.length}\n`);
|
|
58
63
|
for (const ch of s.channels) {
|
|
59
|
-
const
|
|
64
|
+
const chName = ch.name ?? '(unnamed)';
|
|
60
65
|
const parentSuffix = ch.parent ? ` [child of ${ch.parent.slice(0, 8)}]` : '';
|
|
61
|
-
process.stdout.write(` ${ch.uuid.slice(0, 8)}... — ${
|
|
66
|
+
process.stdout.write(` ${ch.uuid.slice(0, 8)}... — ${chName}${parentSuffix}\n`);
|
|
62
67
|
}
|
|
63
68
|
process.stdout.write('\n');
|
|
64
69
|
|
package/commands/up.js
CHANGED
|
@@ -1,68 +1,74 @@
|
|
|
1
|
-
// crosstalk up — bring up the engine container for
|
|
1
|
+
// crosstalk up — bring up the engine container for a transport.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// The compose file bind-mounts the transport root as the engine's working
|
|
6
|
-
// tree so git operations work both ways — engine sees operator's local
|
|
7
|
-
// commits and configured remotes immediately, operator sees engine
|
|
8
|
-
// commits (replies, cursor advances) immediately.
|
|
3
|
+
// alpha.6: no cwd magic. Resolves container by --containername flag
|
|
4
|
+
// (default 'crosstalk') and operates against <base>/<name>/.
|
|
9
5
|
//
|
|
10
|
-
//
|
|
6
|
+
// Three lifecycle cases:
|
|
7
|
+
// 1. Container already running → error (use chat/restart instead).
|
|
8
|
+
// 2. Storage exists, no container → resume; print one-line notice.
|
|
9
|
+
// 3. Storage doesn't exist → error pointing to `crosstalk init`.
|
|
10
|
+
//
|
|
11
|
+
// First-time up generates <base>/<name>/docker-compose.yml from a template.
|
|
12
|
+
// Subsequent ups respect operator edits — only regenerate if missing.
|
|
11
13
|
|
|
12
|
-
import { writeFileSync, existsSync,
|
|
14
|
+
import { writeFileSync, existsSync, readFileSync } from 'fs';
|
|
13
15
|
import { spawnSync } from 'child_process';
|
|
14
|
-
import { join } from 'path';
|
|
15
|
-
import { requireTransportRoot, transportName, composeFile } from '../lib/transport.js';
|
|
16
16
|
import { has } from '../lib/argv.js';
|
|
17
|
+
import {
|
|
18
|
+
requireInitialized,
|
|
19
|
+
isRunning,
|
|
20
|
+
containerExists,
|
|
21
|
+
apiPortFor,
|
|
22
|
+
DEFAULT_CONTAINER_NAME,
|
|
23
|
+
CROSSTALK_LABEL,
|
|
24
|
+
} from '../lib/resolve.js';
|
|
17
25
|
|
|
18
26
|
const DEFAULT_IMAGE = process.env.CROSSTALK_IMAGE
|
|
19
|
-
?? 'ghcr.io/cordfuse/crosstalk-server:7.0.0-alpha.
|
|
20
|
-
const DEFAULT_API_PORT = Number(process.env.CROSSTALK_API_PORT) || 7000;
|
|
27
|
+
?? 'ghcr.io/cordfuse/crosstalk-server:7.0.0-alpha.6';
|
|
21
28
|
|
|
22
29
|
function usage(exit = 0) {
|
|
23
30
|
const w = exit === 0 ? process.stdout : process.stderr;
|
|
24
31
|
w.write(
|
|
25
|
-
`Usage: crosstalk up
|
|
32
|
+
`Usage: crosstalk up [--containername <name>]
|
|
33
|
+
|
|
34
|
+
Starts the engine container for a transport that has already been
|
|
35
|
+
'crosstalk init'd. Default container is 'crosstalk'; pass --containername
|
|
36
|
+
to address a named one.
|
|
26
37
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
38
|
+
If the storage at <base>/<name>/ already exists (operator previously
|
|
39
|
+
brought it up then 'crosstalk down'd it), the container resumes against
|
|
40
|
+
that state — installed agent CLIs, OAuth tokens, dispatcher cursor all
|
|
41
|
+
carry over.
|
|
42
|
+
|
|
43
|
+
Environment:
|
|
44
|
+
CROSSTALK_IMAGE Override the engine image
|
|
45
|
+
CROSSTALK_ALIAS Override the engine's machine identity
|
|
46
|
+
(defaults to the container name)
|
|
31
47
|
`,
|
|
32
48
|
);
|
|
33
49
|
process.exit(exit);
|
|
34
50
|
}
|
|
35
51
|
|
|
36
|
-
function renderCompose({ name, image, apiPort, alias, uid, gid }) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
// files written by the engine are operator-owned on the host, so
|
|
43
|
-
// \`git remote add\` and friends work without sudo. /root stays
|
|
44
|
-
// root-owned for CLI installs.
|
|
45
|
-
return `# Generated by \`crosstalk up\`. Safe to edit; subsequent \`up\`s
|
|
46
|
-
# only regenerate when this file is missing. Compose config is machine-
|
|
47
|
-
# local — kept out of git via .gitignore by default.
|
|
52
|
+
function renderCompose({ name, image, apiPort, alias, uid, gid, paths }) {
|
|
53
|
+
return `# Generated by 'crosstalk up'. Machine-local — runtime-owned.
|
|
54
|
+
# Storage mode: ${paths.mode}
|
|
55
|
+
# transport (container /var/lib/crosstalk-transport) → ${paths.transportDir}
|
|
56
|
+
# crosstalk-root (container /crosstalk-root) → ${paths.crosstalkRoot}
|
|
57
|
+
# crosstalk-state (container /var/lib/crosstalk-state) → ${paths.crosstalkState}
|
|
48
58
|
|
|
49
59
|
services:
|
|
50
60
|
crosstalkd:
|
|
51
61
|
image: ${image}
|
|
52
|
-
container_name:
|
|
62
|
+
container_name: ${name}
|
|
53
63
|
restart: unless-stopped
|
|
64
|
+
labels:
|
|
65
|
+
${CROSSTALK_LABEL.replace('=', ': "')}"
|
|
54
66
|
ports:
|
|
55
67
|
- "127.0.0.1:${apiPort}:7000"
|
|
56
68
|
volumes:
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
-
|
|
60
|
-
# Operator-installed agent CLIs (claude, codex, agy, ...) + auth
|
|
61
|
-
# state. Root-owned, dispatcher reads only.
|
|
62
|
-
- crosstalk-root:/root
|
|
63
|
-
# Dispatcher's machine-local state: cursor, heartbeat, errors.log.
|
|
64
|
-
# Operator-UID-owned (chowned by entrypoint to CROSSTALK_UID/GID).
|
|
65
|
-
- crosstalk-state:/var/lib/crosstalkd-state
|
|
69
|
+
- ${paths.transportDir}:/var/lib/crosstalk-transport
|
|
70
|
+
- ${paths.crosstalkRoot}:/crosstalk-root
|
|
71
|
+
- ${paths.crosstalkState}:/var/lib/crosstalk-state
|
|
66
72
|
environment:
|
|
67
73
|
CROSSTALK_ALIAS: ${alias}
|
|
68
74
|
CROSSTALK_UID: "${uid}"
|
|
@@ -70,52 +76,52 @@ services:
|
|
|
70
76
|
CROSSTALKD_API_PORT: "7000"
|
|
71
77
|
DISPATCH_JSON: "true"
|
|
72
78
|
DISPATCH_POLL_SECONDS: "30"
|
|
73
|
-
|
|
74
|
-
volumes:
|
|
75
|
-
crosstalk-root:
|
|
76
|
-
crosstalk-state:
|
|
77
79
|
`;
|
|
78
80
|
}
|
|
79
81
|
|
|
80
|
-
function ensureGitignored(transportRoot) {
|
|
81
|
-
const path = join(transportRoot, '.gitignore');
|
|
82
|
-
const entry = 'docker-compose.yml\n';
|
|
83
|
-
if (!existsSync(path)) {
|
|
84
|
-
writeFileSync(path, entry);
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
const current = readFileSync(path, 'utf-8');
|
|
88
|
-
if (!current.split('\n').includes('docker-compose.yml')) {
|
|
89
|
-
appendFileSync(path, (current.endsWith('\n') ? '' : '\n') + entry);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
82
|
export async function run(argv) {
|
|
94
83
|
if (has(argv, '--help') || has(argv, '-h')) usage(0);
|
|
95
84
|
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
85
|
+
const { name, paths } = requireInitialized(argv);
|
|
86
|
+
|
|
87
|
+
// Case 1: already running → error, don't surprise the operator with
|
|
88
|
+
// a silent no-op or implicit restart.
|
|
89
|
+
if (isRunning(name)) {
|
|
90
|
+
process.stderr.write(
|
|
91
|
+
`crosstalk up: '${name}' is already running.\n` +
|
|
92
|
+
` Use 'crosstalk chat${name === DEFAULT_CONTAINER_NAME ? '' : ` --containername ${name}`}' to attach,\n` +
|
|
93
|
+
` 'crosstalk restart${name === DEFAULT_CONTAINER_NAME ? '' : ` --containername ${name}`}' to recreate,\n` +
|
|
94
|
+
` or 'crosstalk down${name === DEFAULT_CONTAINER_NAME ? '' : ` --containername ${name}`}' first.\n`,
|
|
95
|
+
);
|
|
96
|
+
return 1;
|
|
97
|
+
}
|
|
99
98
|
|
|
99
|
+
// Case "stopped but not removed" — `docker stop` without compose down.
|
|
100
|
+
// Silently remove the dead container so the recreate works.
|
|
101
|
+
if (containerExists(name)) {
|
|
102
|
+
spawnSync('docker', ['rm', '-f', name], { stdio: 'ignore' });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Compose file generation. If operator edited theirs, respect it.
|
|
106
|
+
const generated = !existsSync(paths.composeFile);
|
|
100
107
|
if (generated) {
|
|
101
|
-
const alias = process.env.CROSSTALK_ALIAS ??
|
|
108
|
+
const alias = process.env.CROSSTALK_ALIAS ?? name;
|
|
102
109
|
const uid = typeof process.getuid === 'function' ? process.getuid() : 1000;
|
|
103
110
|
const gid = typeof process.getgid === 'function' ? process.getgid() : 1000;
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
process.stdout.write(`
|
|
111
|
+
const apiPort = apiPortFor(name);
|
|
112
|
+
const image = DEFAULT_IMAGE;
|
|
113
|
+
const content = renderCompose({ name, image, apiPort, alias, uid, gid, paths });
|
|
114
|
+
writeFileSync(paths.composeFile, content);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Resume notice when storage is non-fresh (operator brought it up
|
|
118
|
+
// before, has installed CLIs / auth state in crosstalk-root/). Heuristic:
|
|
119
|
+
// crosstalk-root has more than just a .gitkeep / nothing.
|
|
120
|
+
if (!generated) {
|
|
121
|
+
process.stdout.write(`Resuming '${name}' (existing state at ${paths.storageRoot})\n`);
|
|
115
122
|
}
|
|
116
123
|
|
|
117
|
-
const r = spawnSync('docker', ['compose', '-f',
|
|
118
|
-
cwd: root,
|
|
124
|
+
const r = spawnSync('docker', ['compose', '-f', paths.composeFile, 'up', '-d'], {
|
|
119
125
|
stdio: 'inherit',
|
|
120
126
|
});
|
|
121
127
|
if (r.status !== 0) {
|
|
@@ -123,7 +129,7 @@ export async function run(argv) {
|
|
|
123
129
|
return r.status ?? 1;
|
|
124
130
|
}
|
|
125
131
|
process.stdout.write(
|
|
126
|
-
`\
|
|
132
|
+
`\n'${name}' starting. Check with 'crosstalk status${name === DEFAULT_CONTAINER_NAME ? '' : ` --containername ${name}`}' (give it ~5s).\n`,
|
|
127
133
|
);
|
|
128
134
|
return 0;
|
|
129
135
|
}
|
package/commands/version.js
CHANGED
|
@@ -7,14 +7,15 @@
|
|
|
7
7
|
import { readFileSync } from 'fs';
|
|
8
8
|
import { dirname, join } from 'path';
|
|
9
9
|
import { fileURLToPath } from 'url';
|
|
10
|
-
import {
|
|
10
|
+
import { apiFor, ConnectError } from '../lib/api-client.js';
|
|
11
11
|
|
|
12
12
|
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
13
13
|
const pkgPath = join(thisDir, '..', 'package.json');
|
|
14
14
|
const clientPkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
15
15
|
|
|
16
|
-
export async function run(
|
|
16
|
+
export async function run(argv) {
|
|
17
17
|
process.stdout.write(`crosstalk client: ${clientPkg.version}\n`);
|
|
18
|
+
const api = apiFor(argv);
|
|
18
19
|
try {
|
|
19
20
|
// Skew detection happens passively in api-client.js via the
|
|
20
21
|
// X-Crosstalk-Engine-Version response header — no duplication here.
|
package/lib/api-client.js
CHANGED
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
// api-client.js — HTTP client over loopback to crosstalkd's local API.
|
|
2
2
|
//
|
|
3
3
|
// All operator commands route through this. The engine binds 127.0.0.1
|
|
4
|
-
// inside the container; docker-compose maps that to the host loopback
|
|
5
|
-
//
|
|
4
|
+
// inside the container; docker-compose maps that to the host loopback.
|
|
5
|
+
// The host port differs per container — default container 'crosstalk'
|
|
6
|
+
// gets 7000; named containers pick a free port at init time, recorded in
|
|
7
|
+
// `<base>/<name>/api-port`. apiPortFor() resolves name → port.
|
|
6
8
|
//
|
|
7
|
-
// Port resolution
|
|
8
|
-
// 1.
|
|
9
|
-
// 2. CROSSTALK_API_PORT env var
|
|
10
|
-
// 3.
|
|
9
|
+
// Port resolution order:
|
|
10
|
+
// 1. opts.port (explicit override from caller)
|
|
11
|
+
// 2. CROSSTALK_API_PORT env var (test/dev override)
|
|
12
|
+
// 3. apiPortFor(name) — reads `<base>/<name>/api-port` file
|
|
13
|
+
// 4. DEFAULT_API_PORT (7000) as last resort
|
|
11
14
|
//
|
|
12
|
-
// No auth:
|
|
13
|
-
// model as ollama / postgres-on-localhost.
|
|
15
|
+
// No auth: engine binds 127.0.0.1 only, no token. Same model as ollama.
|
|
14
16
|
//
|
|
15
|
-
// Version skew: engine sets X-Crosstalk-Engine-Version on every
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// client and engine ship in lockstep; mismatches usually mean the
|
|
19
|
-
// operator upgraded one without the other.
|
|
17
|
+
// Version skew: engine sets X-Crosstalk-Engine-Version on every response.
|
|
18
|
+
// We compare to client version on each call, emit one warning per process
|
|
19
|
+
// if mismatched. Pre-1.0 client and engine ship in lockstep.
|
|
20
20
|
|
|
21
21
|
import { readFileSync } from 'fs';
|
|
22
22
|
import { dirname, join } from 'path';
|
|
23
23
|
import { fileURLToPath } from 'url';
|
|
24
|
+
import { apiPortFor, DEFAULT_API_PORT, parseContainerName } from './resolve.js';
|
|
24
25
|
|
|
25
26
|
const CLIENT_VERSION = (() => {
|
|
26
27
|
try {
|
|
@@ -44,12 +45,13 @@ function checkSkew(engineVersion) {
|
|
|
44
45
|
);
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
export
|
|
48
|
+
export { DEFAULT_API_PORT };
|
|
48
49
|
|
|
49
|
-
export function resolvePort(
|
|
50
|
-
if (typeof
|
|
50
|
+
export function resolvePort(opts = {}) {
|
|
51
|
+
if (typeof opts.port === 'number' && Number.isInteger(opts.port)) return opts.port;
|
|
51
52
|
const fromEnv = Number(process.env.CROSSTALK_API_PORT);
|
|
52
53
|
if (Number.isInteger(fromEnv) && fromEnv > 0) return fromEnv;
|
|
54
|
+
if (opts.name) return apiPortFor(opts.name);
|
|
53
55
|
return DEFAULT_API_PORT;
|
|
54
56
|
}
|
|
55
57
|
|
|
@@ -91,7 +93,7 @@ export class ConnectError extends Error {
|
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
async function call(method, path, body, opts = {}) {
|
|
94
|
-
const port = resolvePort(opts
|
|
96
|
+
const port = resolvePort(opts);
|
|
95
97
|
const init = {
|
|
96
98
|
method,
|
|
97
99
|
headers: body ? { 'Content-Type': 'application/json' } : undefined,
|
|
@@ -107,6 +109,23 @@ async function call(method, path, body, opts = {}) {
|
|
|
107
109
|
return parseJsonOrThrow(res);
|
|
108
110
|
}
|
|
109
111
|
|
|
112
|
+
// Bind an api client to a specific container name. All callers should
|
|
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.
|
|
115
|
+
export function apiFor(argv) {
|
|
116
|
+
const name = parseContainerName(argv);
|
|
117
|
+
return {
|
|
118
|
+
name,
|
|
119
|
+
get: (path, opts = {}) => call('GET', path, undefined, { ...opts, name }),
|
|
120
|
+
post: (path, body, opts = {}) => call('POST', path, body, { ...opts, name }),
|
|
121
|
+
patch: (path, body, opts = {}) => call('PATCH', path, body, { ...opts, name }),
|
|
122
|
+
delete: (path, opts = {}) => call('DELETE', path, undefined, { ...opts, name }),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Legacy entry point — env-driven port (CROSSTALK_API_PORT or 7000),
|
|
127
|
+
// no name resolution. Kept for harness compatibility; new code should
|
|
128
|
+
// use apiFor(argv) instead.
|
|
110
129
|
export const api = {
|
|
111
130
|
get: (path, opts) => call('GET', path, undefined, opts),
|
|
112
131
|
post: (path, body, opts) => call('POST', path, body, opts),
|
package/lib/argv.js
CHANGED
|
@@ -12,13 +12,15 @@ export function has(argv, name) {
|
|
|
12
12
|
return argv.includes(name);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
// All positional (non-flag) args, skipping flag values
|
|
15
|
+
// All positional (non-flag) args, skipping flag values for the listed
|
|
16
|
+
// flags. Handles both --long and -short forms; flag names in
|
|
17
|
+
// flagsWithValue must match the form they appear in argv.
|
|
16
18
|
export function positionals(argv, flagsWithValue) {
|
|
17
19
|
const valueFlags = new Set(flagsWithValue);
|
|
18
20
|
const out = [];
|
|
19
21
|
for (let i = 0; i < argv.length; i++) {
|
|
20
22
|
const a = argv[i];
|
|
21
|
-
if (a.startsWith('
|
|
23
|
+
if (a.startsWith('-')) {
|
|
22
24
|
if (valueFlags.has(a)) i++;
|
|
23
25
|
continue;
|
|
24
26
|
}
|