@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/lib/resolve.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// resolve.js — name-based resolution for the runtime-owned-transport
|
|
2
|
+
// model (alpha.6+).
|
|
3
|
+
//
|
|
4
|
+
// Operator addresses transports by container name, picked at `crosstalk
|
|
5
|
+
// init`. Default container is literally `crosstalk`; named containers
|
|
6
|
+
// are literally whatever the operator typed into --containername.
|
|
7
|
+
// Identity lives with the container; storage lives at <base>/<name>/.
|
|
8
|
+
//
|
|
9
|
+
// Replaces the cwd-walking transport.js from alpha.5 (which derived
|
|
10
|
+
// identity from the operator's filesystem position). The new model is
|
|
11
|
+
// position-independent — operator can be anywhere on disk.
|
|
12
|
+
|
|
13
|
+
import { existsSync, readFileSync, mkdirSync, readdirSync } from 'fs';
|
|
14
|
+
import { spawnSync } from 'child_process';
|
|
15
|
+
import { join } from 'path';
|
|
16
|
+
import { homedir } from 'os';
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_CONTAINER_NAME = 'crosstalk';
|
|
19
|
+
export const DEFAULT_API_PORT = 7000;
|
|
20
|
+
export const CROSSTALK_LABEL = 'crosstalk.transport=true';
|
|
21
|
+
|
|
22
|
+
// Per-OS storage bases. User-mode (default) keeps everything under the
|
|
23
|
+
// operator's home — no sudo/UAC, no Docker Desktop file-sharing allowlist
|
|
24
|
+
// gymnastics. System-mode is for headless multi-operator deployments.
|
|
25
|
+
export function resolveBase(mode) {
|
|
26
|
+
const platform = process.platform;
|
|
27
|
+
const bases = (platform === 'darwin')
|
|
28
|
+
? {
|
|
29
|
+
user: join(homedir(), 'Library', 'Application Support', 'crosstalk'),
|
|
30
|
+
system: '/Library/Application Support/crosstalk',
|
|
31
|
+
}
|
|
32
|
+
: (platform === 'win32')
|
|
33
|
+
? {
|
|
34
|
+
user: process.env['LOCALAPPDATA']
|
|
35
|
+
? join(process.env['LOCALAPPDATA'], 'crosstalk')
|
|
36
|
+
: join(homedir(), 'AppData', 'Local', 'crosstalk'),
|
|
37
|
+
system: 'C:\\ProgramData\\crosstalk',
|
|
38
|
+
}
|
|
39
|
+
: {
|
|
40
|
+
user: join(process.env['XDG_DATA_HOME'] ?? join(homedir(), '.local', 'share'), 'crosstalk'),
|
|
41
|
+
system: '/var/lib/crosstalk',
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
if (mode !== 'user' && mode !== 'system') {
|
|
45
|
+
throw new Error(`CROSSTALK_STORAGE_MODE='${mode}' invalid — must be 'user' or 'system'.`);
|
|
46
|
+
}
|
|
47
|
+
return bases[mode];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function storageMode() {
|
|
51
|
+
return (process.env.CROSSTALK_STORAGE_MODE ?? 'user').toLowerCase();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// All storage paths for a given container name, derived from the resolved
|
|
55
|
+
// base. None of these are validated for existence here — callers check
|
|
56
|
+
// based on context (init pre-creates, up verifies, etc).
|
|
57
|
+
export function storagePaths(name, mode = storageMode()) {
|
|
58
|
+
const base = resolveBase(mode);
|
|
59
|
+
const root = join(base, name);
|
|
60
|
+
return {
|
|
61
|
+
base,
|
|
62
|
+
mode,
|
|
63
|
+
storageRoot: root,
|
|
64
|
+
transportDir: join(root, 'transport'),
|
|
65
|
+
crosstalkRoot: join(root, 'crosstalk-root'),
|
|
66
|
+
crosstalkState: join(root, 'crosstalk-state'),
|
|
67
|
+
composeFile: join(root, 'docker-compose.yml'),
|
|
68
|
+
portFile: join(root, 'api-port'),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Container name from --containername flag, defaulting to `crosstalk`.
|
|
73
|
+
// Validates the name is a sane Docker container name + filesystem dir
|
|
74
|
+
// name: lowercase alphanumeric, `_.-`, no leading dot/hyphen.
|
|
75
|
+
const NAME_RE = /^[a-z0-9][a-z0-9_.-]*$/;
|
|
76
|
+
export function parseContainerName(argv) {
|
|
77
|
+
let name = DEFAULT_CONTAINER_NAME;
|
|
78
|
+
for (let i = 0; i < argv.length; i++) {
|
|
79
|
+
if (argv[i] === '--containername' || argv[i] === '-c') {
|
|
80
|
+
const val = argv[i + 1];
|
|
81
|
+
if (!val) throw new Error('--containername requires a value');
|
|
82
|
+
name = val;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (!NAME_RE.test(name)) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`--containername '${name}' invalid — must be lowercase alphanumeric ` +
|
|
89
|
+
`with optional ._- (no leading . or -).`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return name;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Read the API port for a given container. Source-of-truth lookup:
|
|
96
|
+
// 1. `<base>/<name>/api-port` file (written at init time)
|
|
97
|
+
// 2. Fallback: DEFAULT_API_PORT
|
|
98
|
+
// We use a file rather than `docker inspect` because the resolver runs
|
|
99
|
+
// before the container exists (`init`) and after it's gone (`rm` cleanup).
|
|
100
|
+
// Docker is authoritative for what's running; the port file is
|
|
101
|
+
// authoritative for which port a given container WOULD use.
|
|
102
|
+
export function apiPortFor(name) {
|
|
103
|
+
const paths = storagePaths(name);
|
|
104
|
+
if (existsSync(paths.portFile)) {
|
|
105
|
+
const v = Number(readFileSync(paths.portFile, 'utf-8').trim());
|
|
106
|
+
if (Number.isInteger(v) && v > 0 && v < 65536) return v;
|
|
107
|
+
}
|
|
108
|
+
return DEFAULT_API_PORT;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Pick a free TCP port. Default container always gets DEFAULT_API_PORT
|
|
112
|
+
// (7000) so operators have a predictable target. Named containers search
|
|
113
|
+
// upward from 7001 until they find one that doesn't collide with already-
|
|
114
|
+
// allocated containers (any `<base>/*/api-port` file).
|
|
115
|
+
export function pickFreePort(name, mode = storageMode()) {
|
|
116
|
+
if (name === DEFAULT_CONTAINER_NAME) return DEFAULT_API_PORT;
|
|
117
|
+
const base = resolveBase(mode);
|
|
118
|
+
const taken = new Set([DEFAULT_API_PORT]);
|
|
119
|
+
try {
|
|
120
|
+
const dirs = readdirSync(base);
|
|
121
|
+
for (const d of dirs) {
|
|
122
|
+
const portFile = join(base, d, 'api-port');
|
|
123
|
+
if (existsSync(portFile)) {
|
|
124
|
+
const v = Number(readFileSync(portFile, 'utf-8').trim());
|
|
125
|
+
if (Number.isInteger(v)) taken.add(v);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} catch { /* base may not exist yet */ }
|
|
129
|
+
for (let p = 7001; p < 8000; p++) {
|
|
130
|
+
if (!taken.has(p)) return p;
|
|
131
|
+
}
|
|
132
|
+
throw new Error('Could not find a free port between 7001 and 7999.');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// True if `<base>/<name>/transport/.git` exists. The cheapest signal that
|
|
136
|
+
// a transport has been initialized for this name.
|
|
137
|
+
export function isInitialized(name) {
|
|
138
|
+
const paths = storagePaths(name);
|
|
139
|
+
return existsSync(join(paths.transportDir, '.git'));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// True if a running container exists for this name. Uses Docker as the
|
|
143
|
+
// source of truth, filtered by our label to avoid confusing
|
|
144
|
+
// operator-named-`crosstalk` containers from other workloads.
|
|
145
|
+
export function isRunning(name) {
|
|
146
|
+
const r = spawnSync(
|
|
147
|
+
'docker',
|
|
148
|
+
['ps', '--filter', `label=${CROSSTALK_LABEL}`, '--filter', `name=^${name}$`, '--format', '{{.Names}}'],
|
|
149
|
+
{ encoding: 'utf-8' },
|
|
150
|
+
);
|
|
151
|
+
if (r.status !== 0) return false;
|
|
152
|
+
return r.stdout.trim().split('\n').includes(name);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// True if a stopped container exists (less common path; usually `down`
|
|
156
|
+
// removes the container). Checked separately so `up` can decide whether
|
|
157
|
+
// to `docker rm` and recreate.
|
|
158
|
+
export function containerExists(name) {
|
|
159
|
+
const r = spawnSync(
|
|
160
|
+
'docker',
|
|
161
|
+
['ps', '-a', '--filter', `label=${CROSSTALK_LABEL}`, '--filter', `name=^${name}$`, '--format', '{{.Names}}'],
|
|
162
|
+
{ encoding: 'utf-8' },
|
|
163
|
+
);
|
|
164
|
+
if (r.status !== 0) return false;
|
|
165
|
+
return r.stdout.trim().split('\n').includes(name);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Look up the resolved name+paths from argv, validate that init has run,
|
|
169
|
+
// and exit with a clear error if not. For verbs that operate on existing
|
|
170
|
+
// transports (up after init, status, chat, down, rm, etc).
|
|
171
|
+
export function requireInitialized(argv) {
|
|
172
|
+
const name = parseContainerName(argv);
|
|
173
|
+
if (!isInitialized(name)) {
|
|
174
|
+
process.stderr.write(
|
|
175
|
+
`crosstalk: no transport '${name}' on this machine.\n` +
|
|
176
|
+
` Run 'crosstalk init${name === DEFAULT_CONTAINER_NAME ? '' : ` --containername ${name}`}' to set one up.\n`,
|
|
177
|
+
);
|
|
178
|
+
process.exit(2);
|
|
179
|
+
}
|
|
180
|
+
return { name, paths: storagePaths(name) };
|
|
181
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cordfuse/crosstalk",
|
|
3
|
-
"version": "7.0.0-alpha.
|
|
3
|
+
"version": "7.0.0-alpha.6",
|
|
4
4
|
"description": "Crosstalk client — host-side CLI that talks to the crosstalkd daemon running inside the crosstalk-server container.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/lib/transport.js
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
// transport.js — locate the current Crosstalk transport on the host,
|
|
2
|
-
// derive its conventional names (compose project, container name, etc.).
|
|
3
|
-
//
|
|
4
|
-
// A transport is identified by a CROSSTALK-VERSION file at its root.
|
|
5
|
-
// `crosstalk` walks up from the operator's cwd to find it — same pattern
|
|
6
|
-
// as `git`, `npm`, etc. discovering their root directories.
|
|
7
|
-
|
|
8
|
-
import { existsSync, statSync } from 'fs';
|
|
9
|
-
import { resolve, basename, dirname, join } from 'path';
|
|
10
|
-
|
|
11
|
-
export function findTransportRoot(startDir = process.cwd()) {
|
|
12
|
-
let dir = resolve(startDir);
|
|
13
|
-
for (;;) {
|
|
14
|
-
if (existsSync(join(dir, 'CROSSTALK-VERSION'))) {
|
|
15
|
-
try {
|
|
16
|
-
if (statSync(join(dir, 'CROSSTALK-VERSION')).isFile()) return dir;
|
|
17
|
-
} catch { /* fall through */ }
|
|
18
|
-
}
|
|
19
|
-
const parent = dirname(dir);
|
|
20
|
-
if (parent === dir) return null;
|
|
21
|
-
dir = parent;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function requireTransportRoot(startDir = process.cwd()) {
|
|
26
|
-
const root = findTransportRoot(startDir);
|
|
27
|
-
if (!root) {
|
|
28
|
-
process.stderr.write(
|
|
29
|
-
`crosstalk: not inside a Crosstalk transport ` +
|
|
30
|
-
`(no CROSSTALK-VERSION found from ${resolve(startDir)} upward).\n` +
|
|
31
|
-
`Create one with 'crosstalk init <dir>' or cd into an existing transport.\n`,
|
|
32
|
-
);
|
|
33
|
-
process.exit(2);
|
|
34
|
-
}
|
|
35
|
-
return root;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// transport-name is the basename of the transport root. Used for the
|
|
39
|
-
// compose project, container name, volume names — gives each transport
|
|
40
|
-
// on a host distinct docker objects without an explicit registry.
|
|
41
|
-
export function transportName(transportRoot) {
|
|
42
|
-
return basename(transportRoot);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function containerName(transportRoot) {
|
|
46
|
-
return `crosstalk-${transportName(transportRoot)}`;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function composeFile(transportRoot) {
|
|
50
|
-
return join(transportRoot, 'docker-compose.yml');
|
|
51
|
-
}
|