@cordfuse/crosstalk 7.0.0-alpha.8 → 7.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GUIDE-CLI.md +322 -0
- package/GUIDE-PROMPTS.md +118 -0
- package/LICENSE +21 -0
- package/README.md +402 -61
- package/bin/crosstalk.js +88 -54
- package/commands/agent.js +69 -0
- package/commands/auth.js +273 -0
- package/commands/channel.js +54 -44
- package/commands/chat.js +107 -71
- package/commands/daemon.js +120 -0
- package/commands/logs.js +108 -19
- package/commands/message.js +125 -0
- package/commands/server.js +153 -0
- package/commands/settings.js +49 -0
- package/commands/status.js +37 -13
- package/commands/token.js +136 -0
- package/commands/transport.js +270 -0
- package/commands/version.js +3 -3
- package/commands/workflow.js +234 -0
- package/deploy/crosstalk@.service +62 -0
- package/deploy/install.sh +82 -0
- package/lib/api-client.js +77 -22
- package/lib/credentials.js +207 -0
- package/lib/nativeServer.js +173 -0
- package/lib/resolve.js +101 -34
- package/package.json +27 -4
- package/src/activation.ts +104 -0
- package/src/api.ts +1716 -0
- package/src/auth/enforce.ts +68 -0
- package/src/auth/handlers.ts +266 -0
- package/src/auth/middleware.ts +132 -0
- package/src/auth/setup.ts +263 -0
- package/src/auth/tokens.ts +285 -0
- package/src/auth/users.ts +267 -0
- package/src/dispatch.ts +492 -0
- package/src/dispatchers.ts +91 -0
- package/src/filenames.ts +28 -0
- package/src/frontmatter.ts +26 -0
- package/src/init.ts +116 -0
- package/src/invoke.ts +201 -0
- package/src/log-buffer.ts +67 -0
- package/src/models.ts +283 -0
- package/src/resolve.ts +100 -0
- package/src/state.ts +190 -0
- package/src/stop.ts +37 -0
- package/src/transport.ts +243 -0
- package/src/web/auth-pages.ts +160 -0
- package/src/web/channels.ts +395 -0
- package/src/web/chat-page.ts +636 -0
- package/src/web/chat-pty.ts +254 -0
- package/src/web/dashboard.ts +129 -0
- package/src/web/layout.ts +237 -0
- package/src/web/stubs.ts +510 -0
- package/src/web/workflows.ts +490 -0
- package/src/workflow.ts +470 -0
- package/template/CLAUDE.md +10 -0
- package/template/CROSSTALK-VERSION +1 -0
- package/template/CROSSTALK.md +262 -0
- package/template/PROTOCOL.md +70 -0
- package/template/README.md +64 -0
- package/template/auth/.gitkeep +0 -0
- package/template/auth/README.md +224 -0
- package/template/data/crosstalk.yaml +196 -0
- package/template/gitignore +4 -0
- package/commands/down.js +0 -40
- package/commands/init.js +0 -243
- package/commands/pull.js +0 -22
- package/commands/replies.js +0 -40
- package/commands/restart.js +0 -29
- package/commands/rm.js +0 -109
- package/commands/run.js +0 -122
- package/commands/up.js +0 -135
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Crosstalk transport config — the operator-edited file.
|
|
2
|
+
#
|
|
3
|
+
# Schema:
|
|
4
|
+
#
|
|
5
|
+
# providers:
|
|
6
|
+
# <provider-name>:
|
|
7
|
+
# env_file: <relpath> # optional — dotenv file under transport
|
|
8
|
+
# env: # optional — inline ${VAR} interpolation
|
|
9
|
+
# <KEY>: ${HOST_VAR}
|
|
10
|
+
# models:
|
|
11
|
+
# <model-name>: <cli command>
|
|
12
|
+
#
|
|
13
|
+
# Models are addressed in `<provider>/<model>` form everywhere (--to flag,
|
|
14
|
+
# message `to:` field, workflow plan targets). A bare `<model>` is also
|
|
15
|
+
# accepted when exactly one claimed model has that name; ambiguous bare
|
|
16
|
+
# names fail with a pick-list.
|
|
17
|
+
#
|
|
18
|
+
# Dispatchers (machines) read this file, take the first token of each
|
|
19
|
+
# model's command, and check PATH. Only models whose CLI is installed
|
|
20
|
+
# locally are claimed by that dispatcher.
|
|
21
|
+
#
|
|
22
|
+
# COMMAND CONTRACT — what `<cli command>` means
|
|
23
|
+
# ---------------------------------------------
|
|
24
|
+
# The command is a whitespace-tokenized argv list, NOT a shell pipeline.
|
|
25
|
+
#
|
|
26
|
+
# - First token = the binary the dispatcher checks PATH for.
|
|
27
|
+
# - Remaining tokens = static argv passed to that binary.
|
|
28
|
+
# - The runtime APPENDS the message body as the FINAL positional arg
|
|
29
|
+
# (stdin fallback when the prompt exceeds 64 KB).
|
|
30
|
+
# - Quoting (`'…'`, `"…"`), command substitution (`$(…)`), pipes (`|`),
|
|
31
|
+
# and redirection (`>`, `<`) are NOT interpreted — they pass through
|
|
32
|
+
# as literal argv characters. `bash -c 'echo X'` will NOT work as
|
|
33
|
+
# written; the quotes become part of argv.
|
|
34
|
+
#
|
|
35
|
+
# If you need shell features (pipelines, substitution, env expansion in
|
|
36
|
+
# the command itself), write a wrapper script that reads the prompt
|
|
37
|
+
# from $1, drop it inside the container, then reference it:
|
|
38
|
+
#
|
|
39
|
+
# crosstalk chat --shell
|
|
40
|
+
# cat > /crosstalk-root/.local/bin/myagent-wrapper <<'WRAP'
|
|
41
|
+
# #!/usr/bin/env bash
|
|
42
|
+
# /path/to/real-agent --some-flag "$1"
|
|
43
|
+
# WRAP
|
|
44
|
+
# chmod +x /crosstalk-root/.local/bin/myagent-wrapper
|
|
45
|
+
# exit
|
|
46
|
+
# crosstalk restart
|
|
47
|
+
#
|
|
48
|
+
# Then in this file:
|
|
49
|
+
# providers:
|
|
50
|
+
# custom:
|
|
51
|
+
# models:
|
|
52
|
+
# myagent: myagent-wrapper
|
|
53
|
+
#
|
|
54
|
+
# Why `/crosstalk-root/.local/bin/`: it's already on $PATH and lives
|
|
55
|
+
# under the persistent /crosstalk-root volume, so the wrapper survives
|
|
56
|
+
# `crosstalk restart`. /usr/local/bin/ does NOT work — it's in the
|
|
57
|
+
# container's writable layer and gets wiped on every restart.
|
|
58
|
+
# `crosstalk restart` is required after dropping the wrapper so the
|
|
59
|
+
# dispatcher rescans PATH and claims the new model.
|
|
60
|
+
#
|
|
61
|
+
# Absolute paths also work (alpha.12+): an operator can place the
|
|
62
|
+
# wrapper anywhere under /crosstalk-root and reference it as
|
|
63
|
+
# myagent: /crosstalk-root/wrappers/myagent-wrapper
|
|
64
|
+
# The dispatcher resolves absolute first tokens directly (skipping
|
|
65
|
+
# the PATH walk).
|
|
66
|
+
#
|
|
67
|
+
# Skip-bad-provider (alpha.11+) / skip-bad-model (alpha.12+): typo'd
|
|
68
|
+
# providers and unreachable model binaries are both logged to stderr
|
|
69
|
+
# and skipped — the rest of the registry loads. Check `crosstalk logs`
|
|
70
|
+
# (or `docker logs <container>`) to see which providers / models were
|
|
71
|
+
# rejected and why.
|
|
72
|
+
#
|
|
73
|
+
# Auth: two layouts, pick whichever fits — they coexist per provider.
|
|
74
|
+
#
|
|
75
|
+
# env_file: <relpath>
|
|
76
|
+
# Points at a dotenv file under the transport. Engine reads it at
|
|
77
|
+
# agent spawn and merges KEY=VALUE pairs into the subprocess env.
|
|
78
|
+
# Operator-edited file holds the raw secrets.
|
|
79
|
+
#
|
|
80
|
+
# env: { KEY: ${HOST_VAR}, ... }
|
|
81
|
+
# Inline map. Values MUST be `${HOST_VAR}` references — raw secrets
|
|
82
|
+
# are refused so the yaml stays committable. Engine resolves each
|
|
83
|
+
# ${HOST_VAR} from process.env at spawn time. Missing host vars are
|
|
84
|
+
# dropped silently (the agent CLI surfaces its own auth error). Use
|
|
85
|
+
# this when you want the provider→credential mapping visible in one
|
|
86
|
+
# file.
|
|
87
|
+
#
|
|
88
|
+
# Precedence at spawn (last-wins): process.env → env_file → env →
|
|
89
|
+
# dispatchEnv. So `env` overrides `env_file` per-key if both set, and
|
|
90
|
+
# dispatch metadata wins over everything.
|
|
91
|
+
#
|
|
92
|
+
# Tip: agent CLIs accept aliases (sonnet, opus, haiku, etc.) that auto-
|
|
93
|
+
# track the latest model version. Prefer aliases over pinned full names
|
|
94
|
+
# unless you need a specific version.
|
|
95
|
+
|
|
96
|
+
providers:
|
|
97
|
+
|
|
98
|
+
# ── Anthropic (Claude) ─────────────────────────────────────────────
|
|
99
|
+
# Headless auth env (pick one): CLAUDE_CODE_OAUTH_TOKEN (subscription
|
|
100
|
+
# token from `claude setup-token`), or ANTHROPIC_API_KEY (Console key).
|
|
101
|
+
# OAuth token takes precedence when both are set.
|
|
102
|
+
#
|
|
103
|
+
# anthropic-personal:
|
|
104
|
+
# env_file: auth/anthropic-personal.env
|
|
105
|
+
# models:
|
|
106
|
+
# sonnet: claude --print --dangerously-skip-permissions --model sonnet
|
|
107
|
+
# haiku: claude --print --dangerously-skip-permissions --model haiku
|
|
108
|
+
# opus: claude --print --dangerously-skip-permissions --model opus
|
|
109
|
+
|
|
110
|
+
# ── Google (Gemini CLI) ────────────────────────────────────────────
|
|
111
|
+
# Headless auth env: GEMINI_API_KEY (from aistudio.google.com/apikey).
|
|
112
|
+
# Vertex AI / GCA are alternatives — see the gemini-cli docs.
|
|
113
|
+
#
|
|
114
|
+
# google-personal:
|
|
115
|
+
# env_file: auth/google-personal.env
|
|
116
|
+
# models:
|
|
117
|
+
# gemini-pro: gemini --skip-trust --yolo -p --model gemini-1.5-pro
|
|
118
|
+
# gemini-flash: gemini --skip-trust --yolo -p --model gemini-1.5-flash
|
|
119
|
+
|
|
120
|
+
# ── Inline `env:` block alternative (alpha.10) ─────────────────────
|
|
121
|
+
# Same per-provider scoping as `env_file:`, but the provider→credential
|
|
122
|
+
# mapping is visible in this one file. Values are `${VAR}` references
|
|
123
|
+
# only — raw secrets are rejected at parse time. Resolve from the host
|
|
124
|
+
# environment (export the var before `crosstalk up`, or set it in your
|
|
125
|
+
# shell rc / a sourced .env). Useful when running multiple Gemini
|
|
126
|
+
# accounts so it's obvious which key feeds which provider.
|
|
127
|
+
#
|
|
128
|
+
# google-work:
|
|
129
|
+
# env:
|
|
130
|
+
# GEMINI_API_KEY: ${GOOGLE_WORK_KEY}
|
|
131
|
+
# models:
|
|
132
|
+
# gemini-work: gemini --skip-trust --yolo -p
|
|
133
|
+
|
|
134
|
+
# ── Google (Antigravity CLI — agy) ─────────────────────────────────
|
|
135
|
+
# Reads GEMINI_API_KEY too. Can share its env_file with the google-*
|
|
136
|
+
# provider above, or live under a separate provider if you want
|
|
137
|
+
# different keys for agy vs gemini-cli.
|
|
138
|
+
#
|
|
139
|
+
# google-agy:
|
|
140
|
+
# env_file: auth/google-personal.env
|
|
141
|
+
# models:
|
|
142
|
+
# agy: agy --print
|
|
143
|
+
|
|
144
|
+
# ── OpenAI (Codex CLI) ─────────────────────────────────────────────
|
|
145
|
+
# Codex auths via its own credential store at ~/.codex/auth.json, NOT
|
|
146
|
+
# via env var. Run once via `crosstalk chat --shell`:
|
|
147
|
+
# echo "$OPENAI_API_KEY" | codex login --with-api-key
|
|
148
|
+
# The store persists in /crosstalk-root across container restarts.
|
|
149
|
+
# No env_file is needed (declare one if you want isolated codex
|
|
150
|
+
# accounts via HOME swap — express in the cli string itself).
|
|
151
|
+
#
|
|
152
|
+
# openai-codex:
|
|
153
|
+
# models:
|
|
154
|
+
# codex: codex --skip-git-repo-check
|
|
155
|
+
|
|
156
|
+
# ── OpenRouter (gateway → many models) ─────────────────────────────
|
|
157
|
+
# OpenAI-compatible gateway. Use with codex, qwen, opencode (native),
|
|
158
|
+
# or any other OpenAI-SDK-shaped client.
|
|
159
|
+
# OPENAI_API_KEY=sk-or-v1-...
|
|
160
|
+
# OPENAI_BASE_URL=https://openrouter.ai/api/v1
|
|
161
|
+
#
|
|
162
|
+
# openrouter:
|
|
163
|
+
# env_file: auth/openrouter.env
|
|
164
|
+
# models:
|
|
165
|
+
# qwen3-coder-or: qwen --auth-type openai --yolo --model qwen/qwen3-coder
|
|
166
|
+
# opencode-or: opencode --model qwen/qwen3-coder
|
|
167
|
+
|
|
168
|
+
# ── opencode (multi-provider via own config) ───────────────────────
|
|
169
|
+
# opencode reads native OPENROUTER_API_KEY, or per-provider env
|
|
170
|
+
# (ANTHROPIC_API_KEY, OPENAI_API_KEY, ...), or its own config at
|
|
171
|
+
# ~/.config/opencode/. The provider you put it under here decides
|
|
172
|
+
# which env file it sees at spawn.
|
|
173
|
+
#
|
|
174
|
+
# opencode-native:
|
|
175
|
+
# env_file: auth/openrouter.env
|
|
176
|
+
# models:
|
|
177
|
+
# opencode: opencode -p
|
|
178
|
+
|
|
179
|
+
# ── Local Ollama (no auth) ─────────────────────────────────────────
|
|
180
|
+
# OpenAI-compatible endpoint for whatever models are pulled on the
|
|
181
|
+
# ollama host. Auth is `OPENAI_API_KEY=ollama` (any non-empty string)
|
|
182
|
+
# + OPENAI_BASE_URL pointing at the ollama instance.
|
|
183
|
+
#
|
|
184
|
+
# ollama-local:
|
|
185
|
+
# env_file: auth/ollama-local.env
|
|
186
|
+
# models:
|
|
187
|
+
# qwen-coder-local: qwen --auth-type openai --yolo --model qwen2.5-coder:14b
|
|
188
|
+
|
|
189
|
+
# ── Local Ollama via Tailscale (one GPU box serves the swarm) ──────
|
|
190
|
+
# Same as ollama-local but pointed at a remote ollama via Tailscale.
|
|
191
|
+
# Operators on multiple machines all share one GPU host.
|
|
192
|
+
#
|
|
193
|
+
# ollama-tailnet:
|
|
194
|
+
# env_file: auth/ollama-tailnet.env
|
|
195
|
+
# models:
|
|
196
|
+
# qwen-coder-tailnet: qwen --auth-type openai --yolo --model qwen2.5-coder:14b
|
package/commands/down.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
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.
|
|
6
|
-
|
|
7
|
-
import { existsSync } from 'fs';
|
|
8
|
-
import { spawnSync } from 'child_process';
|
|
9
|
-
import { has } from '../lib/argv.js';
|
|
10
|
-
import { requireInitialized, DEFAULT_CONTAINER_NAME } from '../lib/resolve.js';
|
|
11
|
-
|
|
12
|
-
function usage(exit = 0) {
|
|
13
|
-
const w = exit === 0 ? process.stdout : process.stderr;
|
|
14
|
-
w.write(
|
|
15
|
-
`Usage: crosstalk down [--containername <name>]
|
|
16
|
-
|
|
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.
|
|
20
|
-
`,
|
|
21
|
-
);
|
|
22
|
-
process.exit(exit);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export async function run(argv) {
|
|
26
|
-
if (has(argv, '--help') || has(argv, '-h')) usage(0);
|
|
27
|
-
|
|
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
|
-
);
|
|
33
|
-
return 0;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const r = spawnSync('docker', ['compose', '-f', paths.composeFile, 'down'], {
|
|
37
|
-
stdio: 'inherit',
|
|
38
|
-
});
|
|
39
|
-
return r.status ?? 1;
|
|
40
|
-
}
|
package/commands/init.js
DELETED
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
// crosstalk init — scaffold a transport into <base>/<name>/.
|
|
2
|
-
//
|
|
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.
|
|
9
|
-
//
|
|
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.
|
|
21
|
-
//
|
|
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
|
-
import { spawnSync } from 'child_process';
|
|
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';
|
|
35
|
-
|
|
36
|
-
const DEFAULT_IMAGE = process.env.CROSSTALK_IMAGE
|
|
37
|
-
?? 'ghcr.io/cordfuse/crosstalk-server:7.0.0-alpha.8';
|
|
38
|
-
|
|
39
|
-
function usage(exit = 0) {
|
|
40
|
-
const w = exit === 0 ? process.stdout : process.stderr;
|
|
41
|
-
w.write(
|
|
42
|
-
`Usage:
|
|
43
|
-
crosstalk init [--containername <name>] [--remote <url>]
|
|
44
|
-
|
|
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:
|
|
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
|
|
71
|
-
`,
|
|
72
|
-
);
|
|
73
|
-
process.exit(exit);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export async function run(argv) {
|
|
77
|
-
if (has(argv, '--help') || has(argv, '-h')) usage(0);
|
|
78
|
-
|
|
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;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const gitSteps = [
|
|
184
|
-
['init', '--quiet', '--initial-branch=main'],
|
|
185
|
-
['add', '-A'],
|
|
186
|
-
['commit', '--quiet', '-m', 'initial transport'],
|
|
187
|
-
];
|
|
188
|
-
let gitOk = true;
|
|
189
|
-
for (const args of gitSteps) {
|
|
190
|
-
const gr = spawnSync('git', args, { cwd: paths.transportDir, stdio: 'pipe' });
|
|
191
|
-
if (gr.status !== 0) {
|
|
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 {
|
|
225
|
-
process.stderr.write(
|
|
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`,
|
|
228
|
-
);
|
|
229
|
-
return 1;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
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
|
-
);
|
|
242
|
-
return 0;
|
|
243
|
-
}
|
package/commands/pull.js
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
// crosstalk pull — refresh the engine image for a transport.
|
|
2
|
-
|
|
3
|
-
import { existsSync } from 'fs';
|
|
4
|
-
import { spawnSync } from 'child_process';
|
|
5
|
-
import { has } from '../lib/argv.js';
|
|
6
|
-
import { requireInitialized } from '../lib/resolve.js';
|
|
7
|
-
|
|
8
|
-
export async function run(argv) {
|
|
9
|
-
if (has(argv, '--help') || has(argv, '-h')) {
|
|
10
|
-
process.stdout.write('Usage: crosstalk pull [--containername <name>]\n');
|
|
11
|
-
return 0;
|
|
12
|
-
}
|
|
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`);
|
|
16
|
-
return 1;
|
|
17
|
-
}
|
|
18
|
-
const r = spawnSync('docker', ['compose', '-f', paths.composeFile, 'pull'], {
|
|
19
|
-
stdio: 'inherit',
|
|
20
|
-
});
|
|
21
|
-
return r.status ?? 1;
|
|
22
|
-
}
|
package/commands/replies.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
// crosstalk replies <relPath> [<relPath>...] — poll reply status.
|
|
2
|
-
//
|
|
3
|
-
// Exit 0 if all targets have REPLIED or FAILED, exit 2 while any PENDING
|
|
4
|
-
// — agents can poll cheaply in a loop. Mirrors crosstalkd replies.
|
|
5
|
-
|
|
6
|
-
import { apiFor } from '../lib/api-client.js';
|
|
7
|
-
import { reportAndExit } from '../lib/errors.js';
|
|
8
|
-
import { positionals, has } from '../lib/argv.js';
|
|
9
|
-
|
|
10
|
-
function usage(exit = 0) {
|
|
11
|
-
const w = exit === 0 ? process.stdout : process.stderr;
|
|
12
|
-
w.write('Usage: crosstalk replies [--containername <name>] <relPath> [<relPath>...]\n');
|
|
13
|
-
process.exit(exit);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export async function run(argv) {
|
|
17
|
-
if (has(argv, '--help') || has(argv, '-h')) usage(0);
|
|
18
|
-
const api = apiFor(argv);
|
|
19
|
-
const targets = positionals(argv, ['--containername', '-c']);
|
|
20
|
-
if (targets.length === 0) usage(1);
|
|
21
|
-
|
|
22
|
-
let resp;
|
|
23
|
-
try {
|
|
24
|
-
resp = await api.get(`/replies?relPaths=${encodeURIComponent(targets.join(','))}`);
|
|
25
|
-
} catch (err) {
|
|
26
|
-
reportAndExit(err, 'crosstalk replies');
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
let pending = 0;
|
|
30
|
-
for (const r of resp.replies) {
|
|
31
|
-
if (r.status === 'PENDING') {
|
|
32
|
-
process.stdout.write(`PENDING ${r.target}\n`);
|
|
33
|
-
pending++;
|
|
34
|
-
} else {
|
|
35
|
-
const tag = r.status === 'FAILED' ? 'FAILED ' : 'REPLIED ';
|
|
36
|
-
process.stdout.write(`${tag} ${r.target} <- ${r.from} (${r.replyRelPath})\n`);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return pending > 0 ? 2 : 0;
|
|
40
|
-
}
|
package/commands/restart.js
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
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.
|
|
7
|
-
|
|
8
|
-
import { existsSync } from 'fs';
|
|
9
|
-
import { spawnSync } from 'child_process';
|
|
10
|
-
import { has } from '../lib/argv.js';
|
|
11
|
-
import { requireInitialized } from '../lib/resolve.js';
|
|
12
|
-
|
|
13
|
-
export async function run(argv) {
|
|
14
|
-
if (has(argv, '--help') || has(argv, '-h')) {
|
|
15
|
-
process.stdout.write('Usage: crosstalk restart [--containername <name>]\n');
|
|
16
|
-
return 0;
|
|
17
|
-
}
|
|
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`);
|
|
21
|
-
return 1;
|
|
22
|
-
}
|
|
23
|
-
const r = spawnSync(
|
|
24
|
-
'docker',
|
|
25
|
-
['compose', '-f', paths.composeFile, 'up', '-d', '--force-recreate'],
|
|
26
|
-
{ stdio: 'inherit' },
|
|
27
|
-
);
|
|
28
|
-
return r.status ?? 1;
|
|
29
|
-
}
|