@pome-sh/cli 0.43.0 → 0.44.0
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 +237 -117
- package/dist/build-info.json +3 -3
- package/dist/{checks-QYUPKBPT.js → checks-BOOQNOGJ.js} +4 -3
- package/dist/chunk-26UAPLHK.js +191 -0
- package/dist/{chunk-5A6HHA54.js → chunk-3T4FUF43.js} +2 -2
- package/dist/chunk-4LQ4IJOC.js +1 -0
- package/dist/chunk-7ULGZG7X.js +132 -0
- package/dist/{chunk-5KFDRR53.js → chunk-7VZBAHQ2.js} +2 -2
- package/dist/{chunk-OBFHOACQ.js → chunk-G3YRDMBE.js} +12 -71
- package/dist/{chunk-CS7O2ZXB.js → chunk-M7ATJ423.js} +5 -190
- package/dist/{chunk-ELIEDNF3.js → chunk-RYAQ2ZYA.js} +7 -7
- package/dist/{chunk-HRAD7MRX.js → chunk-TWURH7YM.js} +34 -10
- package/dist/{chunk-3YCX3KUL.js → chunk-ZVKPZHFQ.js} +1 -1
- package/dist/{runTrialGroup-4QL6S7QO.js → runTrialGroup-I3PC2AST.js} +8 -6
- package/dist/{server-KC57K5AC.js → server-GECKG2H6.js} +2 -2
- package/dist/src/cli/main.js +71 -140
- package/dist/{src-E7NTZL2F.js → src-252X4AEZ.js} +2 -2
- package/dist/{src-Z63IOSCJ.js → src-CV7QDV5E.js} +2 -2
- package/dist/{src-5F5OTVYT.js → src-D3PKLT54.js} +2 -2
- package/dist/{src-7X62AGW7.js → src-LPTWNBO5.js} +2 -2
- package/dist/{src-ESRY2MC7.js → src-P5MEPJY4.js} +2 -2
- package/dist/twinHarness-YDGIGB4P.js +6 -0
- package/dist/{twinSeed-4VQ7RRCL.js → twinSeed-ADWQ62CA.js} +1 -1
- package/dist/twinStart-E5R26SF4.js +422 -0
- package/dist/twinTape-3XNU3GRW.js +418 -0
- package/examples/agents/mcp-loop-agent.ts +0 -6
- package/package.json +1 -1
- package/dist/twinHarness-WJFDLEAO.js +0 -6
- package/dist/twinStart-QQ64EV3P.js +0 -183
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { snapshotStandaloneInitialState, updateStandaloneStatusFile } from './chunk-7ULGZG7X.js';
|
|
2
|
+
export { STANDALONE_STATUS_PATH, mergeStandaloneStatus, readStandaloneStatusFile, standaloneStatusEntries, updateStandaloneStatusFile, writeStandaloneStatusFile } from './chunk-7ULGZG7X.js';
|
|
3
|
+
import { readSeedFileText, seedForTwin, parseSeedFileText, soleTwinOf, twinsNamedBy } from './chunk-ZVKPZHFQ.js';
|
|
4
|
+
import { bootTwin } from './chunk-3T4FUF43.js';
|
|
5
|
+
import { defaultPortFor, TWIN_REGISTRY, isTwinName, TWIN_NAMES } from './chunk-RYAQ2ZYA.js';
|
|
6
|
+
import './chunk-TWURH7YM.js';
|
|
7
|
+
import './chunk-7VZBAHQ2.js';
|
|
8
|
+
import './chunk-SG6ZTIMT.js';
|
|
9
|
+
import './chunk-FBSA5L36.js';
|
|
10
|
+
import { randomBytes } from 'node:crypto';
|
|
11
|
+
import { readFileSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { serve } from '@hono/node-server';
|
|
14
|
+
import { sign } from 'hono/jwt';
|
|
15
|
+
import { createServer } from 'node:net';
|
|
16
|
+
|
|
17
|
+
// src/twin/connectSnippets.ts
|
|
18
|
+
function mcpServerName(name) {
|
|
19
|
+
return `pome-${name}`;
|
|
20
|
+
}
|
|
21
|
+
function claudeCodeCommand(input) {
|
|
22
|
+
return `claude mcp add --transport http ${mcpServerName(input.name)} ${input.mcpUrl} --header "Authorization: Bearer ${input.token}"`;
|
|
23
|
+
}
|
|
24
|
+
function codexConfigBlock(input) {
|
|
25
|
+
return [
|
|
26
|
+
`[mcp_servers.${mcpServerName(input.name)}]`,
|
|
27
|
+
`url = "${input.mcpUrl}"`,
|
|
28
|
+
`bearer_token_env_var = "POME_AUTH_TOKEN"`
|
|
29
|
+
].join("\n");
|
|
30
|
+
}
|
|
31
|
+
function mcpJsonStanzaFor(inputs) {
|
|
32
|
+
const mcpServers = {};
|
|
33
|
+
for (const input of inputs) {
|
|
34
|
+
mcpServers[mcpServerName(input.name)] = {
|
|
35
|
+
type: "http",
|
|
36
|
+
url: input.mcpUrl,
|
|
37
|
+
headers: { Authorization: "Bearer ${POME_AUTH_TOKEN}" }
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return JSON.stringify({ mcpServers }, null, 2);
|
|
41
|
+
}
|
|
42
|
+
function sdkLines(input) {
|
|
43
|
+
const rest = `process.env.POME_${input.envName}_REST_URL`;
|
|
44
|
+
switch (input.name) {
|
|
45
|
+
case "github":
|
|
46
|
+
return {
|
|
47
|
+
label: "GitHub's SDK (Octokit)",
|
|
48
|
+
lines: [`new Octokit({ baseUrl: ${rest}, auth: process.env.POME_AUTH_TOKEN })`]
|
|
49
|
+
};
|
|
50
|
+
case "slack":
|
|
51
|
+
return {
|
|
52
|
+
label: "Slack's SDK (WebClient)",
|
|
53
|
+
lines: [`new WebClient(process.env.POME_AUTH_TOKEN, { slackApiUrl: ${rest} + "/" })`]
|
|
54
|
+
};
|
|
55
|
+
case "stripe":
|
|
56
|
+
return {
|
|
57
|
+
label: "Stripe's SDK",
|
|
58
|
+
lines: [
|
|
59
|
+
`new Stripe(process.env.POME_AUTH_TOKEN, { host: "127.0.0.1", port: Number(new URL(${rest}).port), protocol: "http" })`
|
|
60
|
+
]
|
|
61
|
+
};
|
|
62
|
+
case "gmail":
|
|
63
|
+
return {
|
|
64
|
+
label: "googleapis (Gmail)",
|
|
65
|
+
lines: [
|
|
66
|
+
`const gmail = google.gmail({ version: "v1", auth }); // auth: an OAuth2 client with access_token = ${input.tokenEnvName ?? "POME_AUTH_TOKEN"}`,
|
|
67
|
+
`gmail.users.messages.list({ userId: "me" }, { rootUrl: ${rest} + "/" }); // rootUrl goes on each call`
|
|
68
|
+
]
|
|
69
|
+
};
|
|
70
|
+
case "linear":
|
|
71
|
+
return {
|
|
72
|
+
label: "Linear's SDK",
|
|
73
|
+
lines: [
|
|
74
|
+
`new LinearClient({ apiKey: process.env.${input.tokenEnvName ?? "POME_AUTH_TOKEN"}, apiUrl: ${rest} + "/graphql" })`
|
|
75
|
+
]
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function exportLineFor(inputs) {
|
|
80
|
+
const first = inputs[0];
|
|
81
|
+
if (first === void 0) throw new Error("exportLineFor: no twins");
|
|
82
|
+
const pairs = inputs.flatMap((input) => [
|
|
83
|
+
`POME_${input.envName}_REST_URL=${input.restUrl}`,
|
|
84
|
+
`POME_${input.envName}_MCP_URL=${input.mcpUrl}`
|
|
85
|
+
]);
|
|
86
|
+
pairs.push(`POME_AUTH_TOKEN=${first.token}`);
|
|
87
|
+
for (const input of inputs) {
|
|
88
|
+
if (input.tokenEnvName) pairs.push(`${input.tokenEnvName}=${input.token}`);
|
|
89
|
+
}
|
|
90
|
+
return `export ${pairs.join(" ")}`;
|
|
91
|
+
}
|
|
92
|
+
function indent(text) {
|
|
93
|
+
return text.split("\n").map((line) => ` ${line}`).join("\n");
|
|
94
|
+
}
|
|
95
|
+
function renderConnectSnippets(input) {
|
|
96
|
+
const inputs = Array.isArray(input) ? input : [input];
|
|
97
|
+
const sdkSections = inputs.map((twin) => {
|
|
98
|
+
const sdk = sdkLines(twin);
|
|
99
|
+
return [`Your own code, through ${sdk.label}:`, indent(sdk.lines.join("\n"))].join("\n");
|
|
100
|
+
});
|
|
101
|
+
return [
|
|
102
|
+
"Connect your agent (pick one):",
|
|
103
|
+
"",
|
|
104
|
+
"Claude Code:",
|
|
105
|
+
indent(inputs.map(claudeCodeCommand).join("\n")),
|
|
106
|
+
"",
|
|
107
|
+
"Codex, .mcp.json and the SDK line below read the token from your shell. Export it once:",
|
|
108
|
+
indent(exportLineFor(inputs)),
|
|
109
|
+
"",
|
|
110
|
+
"Codex (append to ~/.codex/config.toml):",
|
|
111
|
+
indent(inputs.map(codexConfigBlock).join("\n\n")),
|
|
112
|
+
"",
|
|
113
|
+
".mcp.json (Claude Code project scope, in the repo):",
|
|
114
|
+
indent(mcpJsonStanzaFor(inputs)),
|
|
115
|
+
"",
|
|
116
|
+
sdkSections.join("\n\n")
|
|
117
|
+
].join("\n");
|
|
118
|
+
}
|
|
119
|
+
async function isLoopbackPortFree(port) {
|
|
120
|
+
return await new Promise((resolve) => {
|
|
121
|
+
const probe = createServer();
|
|
122
|
+
probe.once("error", () => resolve(false));
|
|
123
|
+
probe.listen(port, "127.0.0.1", () => probe.close(() => resolve(true)));
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
async function chooseStandalonePorts(twins, portOption, env = process.env, isFree = isLoopbackPortFree) {
|
|
127
|
+
const parse = (raw) => {
|
|
128
|
+
const port = Number(raw);
|
|
129
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
130
|
+
throw new Error(`pome twin start: invalid --port "${raw}"`);
|
|
131
|
+
}
|
|
132
|
+
return port;
|
|
133
|
+
};
|
|
134
|
+
if (twins.length === 1) return [parse(portOption ?? defaultPortFor(twins[0], env))];
|
|
135
|
+
const chosen = [];
|
|
136
|
+
for (const [index, twin] of twins.entries()) {
|
|
137
|
+
if (index === 0 && portOption !== void 0) {
|
|
138
|
+
chosen.push(parse(portOption));
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const start = portOption !== void 0 ? chosen[index - 1] + 1 : parse(defaultPortFor(twin, env));
|
|
142
|
+
let candidate = start;
|
|
143
|
+
while (chosen.includes(candidate) || !await isFree(candidate)) {
|
|
144
|
+
candidate += 1;
|
|
145
|
+
if (candidate > 65535 || candidate - start > 200) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`pome twin start: no free port for the ${twin} twin within 200 of ${start} \u2014 pass --port <port>.`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
chosen.push(candidate);
|
|
152
|
+
}
|
|
153
|
+
return chosen;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/twin/twinStart.ts
|
|
157
|
+
var STANDALONE_SID = "standalone";
|
|
158
|
+
function resolveStandaloneAuthSecret(twin, env = process.env) {
|
|
159
|
+
const injected = env.TWIN_AUTH_SECRET;
|
|
160
|
+
if (injected) return { secret: injected, source: "env" };
|
|
161
|
+
const dataDir = env.POME_TWIN_DATA_DIR || join(".pome-data", twin);
|
|
162
|
+
const secretPath = join(dataDir, "secret");
|
|
163
|
+
let raw;
|
|
164
|
+
try {
|
|
165
|
+
raw = readFileSync(secretPath, "utf8");
|
|
166
|
+
} catch (err) {
|
|
167
|
+
if (err.code !== "ENOENT") throw err;
|
|
168
|
+
}
|
|
169
|
+
const persisted = raw?.trim() ?? "";
|
|
170
|
+
if (persisted.length >= 32) {
|
|
171
|
+
return { secret: persisted, source: "persisted", path: secretPath };
|
|
172
|
+
}
|
|
173
|
+
if (persisted.length > 0) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`The persisted secret at ${secretPath} is shorter than 32 chars \u2014 fix or delete the file, or inject TWIN_AUTH_SECRET.`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
return { secret: randomBytes(32).toString("hex"), source: "ephemeral" };
|
|
179
|
+
}
|
|
180
|
+
async function resolveStandaloneSeed(twin, seedPath, env = process.env, seedText) {
|
|
181
|
+
if (seedPath !== void 0) {
|
|
182
|
+
const raw = seedText ?? readSeedFileText(seedPath, "pome twin start --seed");
|
|
183
|
+
const origin = `--seed ${seedPath}`;
|
|
184
|
+
return {
|
|
185
|
+
seedState: await seedForTwin(parseSeedFileText(raw, origin), twin, origin),
|
|
186
|
+
source: "file",
|
|
187
|
+
path: seedPath
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const fromEnv = env.POME_SEED_JSON;
|
|
191
|
+
if (fromEnv !== void 0 && fromEnv !== "") {
|
|
192
|
+
return {
|
|
193
|
+
seedState: await seedForTwin(
|
|
194
|
+
parseSeedFileText(fromEnv, "POME_SEED_JSON"),
|
|
195
|
+
twin,
|
|
196
|
+
"POME_SEED_JSON"
|
|
197
|
+
),
|
|
198
|
+
source: "env"
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
return { seedState: await TWIN_REGISTRY[twin].defaultSeed(), source: "default" };
|
|
202
|
+
}
|
|
203
|
+
function resolveStandaloneTwin(name, seedPath, seedText) {
|
|
204
|
+
if (name !== void 0) {
|
|
205
|
+
if (!isTwinName(name)) {
|
|
206
|
+
throw new Error(`Unknown twin '${name}'. Supported: ${TWIN_NAMES.join(", ")}.`);
|
|
207
|
+
}
|
|
208
|
+
return name;
|
|
209
|
+
}
|
|
210
|
+
if (seedPath === void 0 || seedText === void 0) {
|
|
211
|
+
throw new Error(
|
|
212
|
+
`pome twin start: name a twin (${TWIN_NAMES.join(", ")}), or pass a --seed file whose envelope names exactly one.`
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
const origin = `--seed ${seedPath}`;
|
|
216
|
+
const file = parseSeedFileText(seedText, origin);
|
|
217
|
+
const sole = soleTwinOf(file);
|
|
218
|
+
if (sole !== void 0) return sole;
|
|
219
|
+
const named = twinsNamedBy(file);
|
|
220
|
+
throw new Error(
|
|
221
|
+
named.length === 0 ? `${origin} is a flat seed, so it does not name a twin. Pass the name: pome twin start <${TWIN_NAMES.join("|")}> --seed ${seedPath}` : `${origin} names ${named.length} twins (${named.join(", ")}), so it does not say which one to start. Pass the name: pome twin start ${named[0]} --seed ${seedPath}`
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
function resolveStandaloneTwins(names, seedPath, seedText) {
|
|
225
|
+
if (names.length === 0) return [resolveStandaloneTwin(void 0, seedPath, seedText)];
|
|
226
|
+
const twins = [];
|
|
227
|
+
for (const name of names) {
|
|
228
|
+
if (!isTwinName(name)) {
|
|
229
|
+
throw new Error(`Unknown twin '${name}'. Supported: ${TWIN_NAMES.join(", ")}.`);
|
|
230
|
+
}
|
|
231
|
+
if (twins.includes(name)) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`pome twin start: '${name}' is named twice. Each twin boots once per command; name it once.`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
twins.push(name);
|
|
237
|
+
}
|
|
238
|
+
return twins;
|
|
239
|
+
}
|
|
240
|
+
async function resolveStandaloneSeeds(twins, seedPath, env = process.env, seedText) {
|
|
241
|
+
const out = /* @__PURE__ */ new Map();
|
|
242
|
+
if (twins.length === 1) {
|
|
243
|
+
const only = twins[0];
|
|
244
|
+
out.set(only, await resolveStandaloneSeed(only, seedPath, env, seedText));
|
|
245
|
+
return out;
|
|
246
|
+
}
|
|
247
|
+
let authored;
|
|
248
|
+
if (seedPath !== void 0) {
|
|
249
|
+
authored = {
|
|
250
|
+
raw: seedText ?? readSeedFileText(seedPath, "pome twin start --seed"),
|
|
251
|
+
origin: `--seed ${seedPath}`,
|
|
252
|
+
source: "file",
|
|
253
|
+
path: seedPath
|
|
254
|
+
};
|
|
255
|
+
} else if (env.POME_SEED_JSON !== void 0 && env.POME_SEED_JSON !== "") {
|
|
256
|
+
authored = { raw: env.POME_SEED_JSON, origin: "POME_SEED_JSON", source: "env" };
|
|
257
|
+
}
|
|
258
|
+
if (authored === void 0) {
|
|
259
|
+
for (const twin of twins) {
|
|
260
|
+
out.set(twin, { seedState: await TWIN_REGISTRY[twin].defaultSeed(), source: "default" });
|
|
261
|
+
}
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
264
|
+
const file = parseSeedFileText(authored.raw, authored.origin);
|
|
265
|
+
if (file.shape === "flat") {
|
|
266
|
+
throw new Error(
|
|
267
|
+
`${authored.origin} is a flat seed for one twin, and pome twin start was given ${twins.length} names (${twins.join(", ")}), so nothing says which one it is for. Name one twin, or wrap it in a per-twin envelope: { "${twins[0]}": { \u2026 } }.`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
for (const twin of twins) {
|
|
271
|
+
out.set(twin, {
|
|
272
|
+
seedState: await seedForTwin(file, twin, authored.origin),
|
|
273
|
+
source: authored.source,
|
|
274
|
+
...authored.path !== void 0 ? { path: authored.path } : {}
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
return out;
|
|
278
|
+
}
|
|
279
|
+
function resolveStandaloneAuthSecretFor(twins, env = process.env, resolveOne = resolveStandaloneAuthSecret) {
|
|
280
|
+
if (twins.length === 1) return resolveOne(twins[0], env);
|
|
281
|
+
if (env.TWIN_AUTH_SECRET) return { secret: env.TWIN_AUTH_SECRET, source: "env" };
|
|
282
|
+
const persisted = twins.map((twin) => resolveOne(twin, env)).filter((resolved) => resolved.source === "persisted");
|
|
283
|
+
const distinct = new Set(persisted.map((resolved) => resolved.secret));
|
|
284
|
+
if (distinct.size > 1) {
|
|
285
|
+
throw new Error(
|
|
286
|
+
`pome twin start: ${persisted.map((resolved) => resolved.path).join(" and ")} hold different secrets, and one process serves one secret. Inject TWIN_AUTH_SECRET, or make the files agree.`
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
const shared = persisted[0];
|
|
290
|
+
if (shared !== void 0) return { secret: shared.secret, source: "persisted", path: shared.path };
|
|
291
|
+
return { secret: randomBytes(32).toString("hex"), source: "ephemeral" };
|
|
292
|
+
}
|
|
293
|
+
async function runTwinStartCommand(namesArg, options) {
|
|
294
|
+
const names = namesArg === void 0 ? [] : typeof namesArg === "string" ? [namesArg] : [...namesArg];
|
|
295
|
+
const seedText = options.seed === void 0 ? void 0 : readSeedFileText(options.seed, "pome twin start --seed");
|
|
296
|
+
const twins = resolveStandaloneTwins(names, options.seed, seedText);
|
|
297
|
+
const ports = await chooseStandalonePorts(twins, options.port, process.env);
|
|
298
|
+
const seeds = await resolveStandaloneSeeds(twins, options.seed, process.env, seedText);
|
|
299
|
+
const resolved = resolveStandaloneAuthSecretFor(twins);
|
|
300
|
+
process.env.TWIN_AUTH_SECRET = resolved.secret;
|
|
301
|
+
const booted = [];
|
|
302
|
+
try {
|
|
303
|
+
for (const [index, twin] of twins.entries()) {
|
|
304
|
+
const port = ports[index];
|
|
305
|
+
const baseUrl = `http://127.0.0.1:${port}`;
|
|
306
|
+
const harness = await bootTwin({
|
|
307
|
+
twin,
|
|
308
|
+
seedState: seeds.get(twin).seedState,
|
|
309
|
+
runId: STANDALONE_SID,
|
|
310
|
+
twinBaseUrl: baseUrl
|
|
311
|
+
});
|
|
312
|
+
booted.push({ twin, port, baseUrl, harness });
|
|
313
|
+
await snapshotStandaloneInitialState(twin, () => harness.exportState());
|
|
314
|
+
}
|
|
315
|
+
} catch (err) {
|
|
316
|
+
for (const entry of booted) await entry.harness.close();
|
|
317
|
+
throw err;
|
|
318
|
+
}
|
|
319
|
+
let extraClaims = {};
|
|
320
|
+
for (const entry of booted) extraClaims = { ...extraClaims, ...entry.harness.extraClaims ?? {} };
|
|
321
|
+
const token = await sign(
|
|
322
|
+
{
|
|
323
|
+
sid: STANDALONE_SID,
|
|
324
|
+
team_id: "tm_local",
|
|
325
|
+
login: "pome-agent",
|
|
326
|
+
...extraClaims,
|
|
327
|
+
exp: Math.floor(Date.now() / 1e3) + 60 * 60 * 24
|
|
328
|
+
},
|
|
329
|
+
resolved.secret
|
|
330
|
+
);
|
|
331
|
+
const servers = [];
|
|
332
|
+
const closeAll = async () => {
|
|
333
|
+
for (const server of servers) {
|
|
334
|
+
server.closeAllConnections?.();
|
|
335
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
336
|
+
}
|
|
337
|
+
for (const entry of booted) await entry.harness.close();
|
|
338
|
+
};
|
|
339
|
+
try {
|
|
340
|
+
for (const entry of booted) {
|
|
341
|
+
const server = serve({ fetch: entry.harness.app.fetch, port: entry.port, hostname: "127.0.0.1" });
|
|
342
|
+
servers.push(server);
|
|
343
|
+
await new Promise((resolve, reject) => {
|
|
344
|
+
const onError = (err) => {
|
|
345
|
+
reject(
|
|
346
|
+
err.code === "EADDRINUSE" ? new Error(
|
|
347
|
+
`pome twin start: port ${entry.port} is already in use \u2014 pass --port <port>, or stop the twin using it.`
|
|
348
|
+
) : err
|
|
349
|
+
);
|
|
350
|
+
};
|
|
351
|
+
server.once("error", onError);
|
|
352
|
+
server.once("listening", () => {
|
|
353
|
+
server.off("error", onError);
|
|
354
|
+
server.on("error", (err) => console.error(`pome twin start: server error: ${err}`));
|
|
355
|
+
resolve();
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
const entries = booted.map((entry) => {
|
|
360
|
+
const restUrl = `${entry.baseUrl}/s/${STANDALONE_SID}`;
|
|
361
|
+
return { name: entry.twin, url: restUrl, rest_url: restUrl, mcp_url: `${restUrl}/mcp`, auth_token: token };
|
|
362
|
+
});
|
|
363
|
+
await updateStandaloneStatusFile(entries);
|
|
364
|
+
} catch (err) {
|
|
365
|
+
await closeAll();
|
|
366
|
+
throw err;
|
|
367
|
+
}
|
|
368
|
+
const connect = [];
|
|
369
|
+
for (const [index, entry] of booted.entries()) {
|
|
370
|
+
const { twin: name, port, baseUrl, harness } = entry;
|
|
371
|
+
const restUrl = `${baseUrl}/s/${STANDALONE_SID}`;
|
|
372
|
+
const mcpUrl = `${restUrl}/mcp`;
|
|
373
|
+
const world = seeds.get(name);
|
|
374
|
+
console.log(`Pome ${name} twin listening at ${restUrl}`);
|
|
375
|
+
if (world.source === "file") {
|
|
376
|
+
console.log(`Seed: ${world.path} (replaces the ${name} twin's default).`);
|
|
377
|
+
} else if (world.source === "env") {
|
|
378
|
+
console.log(
|
|
379
|
+
`Seed: POME_SEED_JSON (replaces the ${name} twin's default; --seed <path> overrides it).`
|
|
380
|
+
);
|
|
381
|
+
} else {
|
|
382
|
+
console.log(
|
|
383
|
+
`Seed: the ${name} twin's default (pass --seed <path>, or write one with \`pome twin new-seed ${name}\`).`
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
if (index === 0 && resolved.source === "persisted") {
|
|
387
|
+
console.log(
|
|
388
|
+
`Auth: using the persisted secret from ${resolved.path} (an env-injected TWIN_AUTH_SECRET overrides it).`
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
console.log(`POME_${harness.envName}_REST_URL=${restUrl}`);
|
|
392
|
+
console.log(`POME_${harness.envName}_MCP_URL=${mcpUrl}`);
|
|
393
|
+
console.log(`POME_AUTH_TOKEN=${token}`);
|
|
394
|
+
if (harness.tokenEnvName) console.log(`${harness.tokenEnvName}=${token}`);
|
|
395
|
+
console.log(`Health check (no auth): curl ${baseUrl}/healthz`);
|
|
396
|
+
connect.push({
|
|
397
|
+
name,
|
|
398
|
+
envName: harness.envName,
|
|
399
|
+
port,
|
|
400
|
+
restUrl,
|
|
401
|
+
mcpUrl,
|
|
402
|
+
token,
|
|
403
|
+
...harness.tokenEnvName ? { tokenEnvName: harness.tokenEnvName } : {}
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
console.log("");
|
|
407
|
+
console.log(renderConnectSnippets(connect));
|
|
408
|
+
console.log("");
|
|
409
|
+
console.log("Ctrl-C to stop.");
|
|
410
|
+
const shutdown = () => {
|
|
411
|
+
void (async () => {
|
|
412
|
+
const hardExit = setTimeout(() => process.exit(1), 1e4);
|
|
413
|
+
hardExit.unref();
|
|
414
|
+
await closeAll();
|
|
415
|
+
process.exit(0);
|
|
416
|
+
})();
|
|
417
|
+
};
|
|
418
|
+
process.once("SIGINT", shutdown);
|
|
419
|
+
process.once("SIGTERM", shutdown);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export { chooseStandalonePorts, isLoopbackPortFree, resolveStandaloneAuthSecret, resolveStandaloneAuthSecretFor, resolveStandaloneSeed, resolveStandaloneSeeds, resolveStandaloneTwin, resolveStandaloneTwins, runTwinStartCommand };
|