@vellumai/cli 0.8.6 → 0.8.7-dev.202606052135.3e62c5a
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/bun.lock +8 -0
- package/knip.json +5 -1
- package/node_modules/@vellumai/environments/bun.lock +24 -0
- package/node_modules/@vellumai/environments/package.json +18 -0
- package/node_modules/@vellumai/environments/src/__tests__/package-boundary.test.ts +95 -0
- package/node_modules/@vellumai/environments/src/index.ts +11 -0
- package/{src/lib/environments → node_modules/@vellumai/environments/src}/seeds.ts +5 -9
- package/node_modules/@vellumai/environments/tsconfig.json +20 -0
- package/node_modules/@vellumai/local-mode/bun.lock +29 -0
- package/node_modules/@vellumai/local-mode/package.json +22 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/environment.test.ts +116 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/gateway-proxy.test.ts +79 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/hatch.test.ts +108 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/package-boundary.test.ts +104 -0
- package/node_modules/@vellumai/local-mode/src/__tests__/wake.test.ts +66 -0
- package/node_modules/@vellumai/local-mode/src/config.ts +66 -0
- package/node_modules/@vellumai/local-mode/src/environment.ts +62 -0
- package/node_modules/@vellumai/local-mode/src/gateway-proxy.ts +109 -0
- package/node_modules/@vellumai/local-mode/src/guardian-token.ts +122 -0
- package/node_modules/@vellumai/local-mode/src/hatch.ts +92 -0
- package/node_modules/@vellumai/local-mode/src/index.ts +48 -0
- package/node_modules/@vellumai/local-mode/src/lockfile-contract.test.ts +173 -0
- package/node_modules/@vellumai/local-mode/src/lockfile-contract.ts +114 -0
- package/node_modules/@vellumai/local-mode/src/lockfile.test.ts +235 -0
- package/node_modules/@vellumai/local-mode/src/lockfile.ts +133 -0
- package/node_modules/@vellumai/local-mode/src/retire.ts +58 -0
- package/node_modules/@vellumai/local-mode/src/util.ts +102 -0
- package/node_modules/@vellumai/local-mode/src/wake.ts +78 -0
- package/node_modules/@vellumai/local-mode/tsconfig.json +16 -0
- package/package.json +12 -1
- package/src/__tests__/assistant-client-refresh.test.ts +182 -0
- package/src/__tests__/clean.test.ts +179 -0
- package/src/__tests__/client-token.test.ts +87 -0
- package/src/__tests__/client-tui-refresh.test.ts +170 -0
- package/src/__tests__/cloudflare-tunnel.test.ts +137 -0
- package/src/__tests__/connect-import.test.ts +317 -0
- package/src/__tests__/devices.test.ts +272 -0
- package/src/__tests__/env-drift.test.ts +32 -44
- package/src/__tests__/flags.test.ts +248 -0
- package/src/__tests__/guardian-token.test.ts +126 -2
- package/src/__tests__/multi-local.test.ts +1 -1
- package/src/__tests__/orphan-detection.test.ts +8 -6
- package/src/__tests__/pair.test.ts +271 -0
- package/src/__tests__/paired-lifecycle.test.ts +116 -0
- package/src/__tests__/segments-to-plain-text.test.ts +37 -0
- package/src/__tests__/tui-midsession-refresh.test.ts +166 -0
- package/src/__tests__/unpair.test.ts +163 -0
- package/src/commands/client.ts +511 -11
- package/src/commands/connect/import.ts +217 -0
- package/src/commands/connect.ts +31 -0
- package/src/commands/devices.ts +247 -0
- package/src/commands/env.ts +1 -1
- package/src/commands/flags.ts +89 -17
- package/src/commands/pair.ts +222 -0
- package/src/commands/ps.ts +16 -0
- package/src/commands/retire.ts +20 -47
- package/src/commands/sleep.ts +7 -0
- package/src/commands/tunnel.ts +46 -2
- package/src/commands/unpair.ts +118 -0
- package/src/commands/wake.ts +7 -0
- package/src/components/DefaultMainScreen.tsx +100 -14
- package/src/index.ts +16 -0
- package/src/lib/__tests__/lifecycle-reporter.test.ts +59 -0
- package/src/lib/assistant-client.ts +58 -37
- package/src/lib/assistant-config.ts +15 -3
- package/src/lib/cloudflare-tunnel.ts +276 -0
- package/src/lib/confirm-action.ts +57 -0
- package/src/lib/docker.ts +25 -1
- package/src/lib/environments/__tests__/paths.test.ts +2 -1
- package/src/lib/environments/__tests__/seeds.test.ts +2 -1
- package/src/lib/environments/paths.ts +1 -1
- package/src/lib/environments/resolve.ts +11 -35
- package/src/lib/guardian-token.ts +132 -9
- package/src/lib/hatch-local.ts +73 -33
- package/src/lib/lifecycle-reporter.ts +31 -0
- package/src/lib/local.ts +20 -6
- package/src/lib/retire-local.ts +28 -14
- package/src/lib/segments-to-plain-text.ts +35 -0
- /package/{src/lib/environments → node_modules/@vellumai/environments/src}/types.ts +0 -0
package/src/lib/hatch-local.ts
CHANGED
|
@@ -33,7 +33,10 @@ import {
|
|
|
33
33
|
import { generateInstanceName } from "./random-name.js";
|
|
34
34
|
import { leaseGuardianToken } from "./guardian-token.js";
|
|
35
35
|
import { archiveLogFile, resetLogFile } from "./xdg-log.js";
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
consoleLifecycleReporter,
|
|
38
|
+
type LifecycleReporter,
|
|
39
|
+
} from "./lifecycle-reporter.js";
|
|
37
40
|
import {
|
|
38
41
|
configureHatchProviderApiKey,
|
|
39
42
|
formatProviderName,
|
|
@@ -134,6 +137,25 @@ function installCLISymlink(): void {
|
|
|
134
137
|
|
|
135
138
|
export interface HatchLocalOptions {
|
|
136
139
|
setupProviderCredentials?: boolean;
|
|
140
|
+
/**
|
|
141
|
+
* Sink for progress and log output. Defaults to the console reporter so CLI
|
|
142
|
+
* callers keep their existing terminal output; in-process callers can inject
|
|
143
|
+
* their own reporter to consume progress without writing to stdout.
|
|
144
|
+
*/
|
|
145
|
+
reporter?: LifecycleReporter;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface HatchLocalResult {
|
|
149
|
+
assistantId: string;
|
|
150
|
+
runtimeUrl: string;
|
|
151
|
+
localUrl: string;
|
|
152
|
+
species: Species;
|
|
153
|
+
/**
|
|
154
|
+
* Guardian access token leased during hatch, when the lease succeeded. The
|
|
155
|
+
* full token pair is persisted to disk regardless; this is surfaced so an
|
|
156
|
+
* in-process caller can prime a connection without re-reading the file.
|
|
157
|
+
*/
|
|
158
|
+
guardianAccessToken?: string;
|
|
137
159
|
}
|
|
138
160
|
|
|
139
161
|
export async function hatchLocal(
|
|
@@ -143,7 +165,8 @@ export async function hatchLocal(
|
|
|
143
165
|
keepAlive: boolean = false,
|
|
144
166
|
configValues: Record<string, string> = {},
|
|
145
167
|
options: HatchLocalOptions = {},
|
|
146
|
-
): Promise<
|
|
168
|
+
): Promise<HatchLocalResult> {
|
|
169
|
+
const reporter = options.reporter ?? consoleLifecycleReporter;
|
|
147
170
|
const provider =
|
|
148
171
|
options.setupProviderCredentials === false
|
|
149
172
|
? undefined
|
|
@@ -153,7 +176,7 @@ export async function hatchLocal(
|
|
|
153
176
|
name ?? process.env.VELLUM_ASSISTANT_NAME,
|
|
154
177
|
);
|
|
155
178
|
|
|
156
|
-
|
|
179
|
+
reporter.progress(1, 6, "Allocating resources...");
|
|
157
180
|
|
|
158
181
|
const existing = findAssistantByName(instanceName);
|
|
159
182
|
if (existing && (!existing.cloud || existing.cloud === "local")) {
|
|
@@ -175,29 +198,29 @@ export async function hatchLocal(
|
|
|
175
198
|
archiveLogFile("hatch.log", logsDir);
|
|
176
199
|
resetLogFile("hatch.log");
|
|
177
200
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
201
|
+
reporter.log(`🥚 Hatching local assistant: ${instanceName}`);
|
|
202
|
+
reporter.log(` Species: ${species}`);
|
|
203
|
+
reporter.log("");
|
|
181
204
|
|
|
182
205
|
const apiKeyCheck = checkProviderApiKey();
|
|
183
206
|
if (!apiKeyCheck.hasKey) {
|
|
184
|
-
|
|
207
|
+
reporter.warn(
|
|
185
208
|
"Warning: No LLM provider API key is configured. The assistant will fail when you try to send a message.",
|
|
186
209
|
);
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
210
|
+
reporter.warn(" To fix, export your key before running vellum hatch:");
|
|
211
|
+
reporter.warn(" export ANTHROPIC_API_KEY=<your-key>");
|
|
212
|
+
reporter.warn("");
|
|
190
213
|
}
|
|
191
214
|
|
|
192
215
|
if (!process.env.APP_VERSION) {
|
|
193
216
|
process.env.APP_VERSION = cliPkg.version;
|
|
194
217
|
}
|
|
195
218
|
|
|
196
|
-
|
|
219
|
+
reporter.progress(2, 6, "Writing configuration...");
|
|
197
220
|
const hatchConfigValues = buildHatchConfigValues(configValues, provider);
|
|
198
221
|
const defaultWorkspaceConfigPath = writeInitialConfig(hatchConfigValues);
|
|
199
222
|
|
|
200
|
-
|
|
223
|
+
reporter.progress(3, 6, "Starting assistant...");
|
|
201
224
|
const signingKey = generateLocalSigningKey();
|
|
202
225
|
const bootstrapSecret = generateLocalSigningKey();
|
|
203
226
|
await startLocalDaemon(watch, resources, {
|
|
@@ -205,14 +228,17 @@ export async function hatchLocal(
|
|
|
205
228
|
signingKey,
|
|
206
229
|
});
|
|
207
230
|
|
|
208
|
-
|
|
231
|
+
reporter.progress(4, 6, "Starting gateway...");
|
|
209
232
|
let runtimeUrl = `http://127.0.0.1:${resources.gatewayPort}`;
|
|
210
233
|
try {
|
|
211
|
-
runtimeUrl = await startGateway(watch, resources, {
|
|
234
|
+
runtimeUrl = await startGateway(watch, resources, {
|
|
235
|
+
signingKey,
|
|
236
|
+
bootstrapSecret,
|
|
237
|
+
});
|
|
212
238
|
} catch (error) {
|
|
213
239
|
// Gateway failed — stop the daemon we just started so we don't leave
|
|
214
240
|
// orphaned processes with no lock file entry.
|
|
215
|
-
|
|
241
|
+
reporter.error(
|
|
216
242
|
`\n❌ Gateway startup failed — stopping assistant to avoid orphaned processes.`,
|
|
217
243
|
);
|
|
218
244
|
await stopLocalProcesses(resources);
|
|
@@ -223,24 +249,28 @@ export async function hatchLocal(
|
|
|
223
249
|
// instead of hitting /v1/guardian/init itself. Use loopback to satisfy
|
|
224
250
|
// the daemon's local-only check — the mDNS runtimeUrl resolves to a LAN
|
|
225
251
|
// IP which the daemon rejects as non-loopback.
|
|
226
|
-
|
|
252
|
+
reporter.progress(5, 6, "Securing connection...");
|
|
227
253
|
const loopbackUrl = `http://127.0.0.1:${resources.gatewayPort}`;
|
|
228
254
|
const maxLeaseAttempts = 3;
|
|
229
255
|
let guardianAccessToken: string | undefined;
|
|
230
256
|
for (let attempt = 1; attempt <= maxLeaseAttempts; attempt++) {
|
|
231
257
|
try {
|
|
232
|
-
const tokenData = await leaseGuardianToken(
|
|
258
|
+
const tokenData = await leaseGuardianToken(
|
|
259
|
+
loopbackUrl,
|
|
260
|
+
instanceName,
|
|
261
|
+
bootstrapSecret,
|
|
262
|
+
);
|
|
233
263
|
guardianAccessToken = tokenData.accessToken;
|
|
234
264
|
break;
|
|
235
265
|
} catch (err) {
|
|
236
266
|
if (attempt < maxLeaseAttempts) {
|
|
237
267
|
const delayMs = 2000 * 2 ** (attempt - 1);
|
|
238
|
-
|
|
268
|
+
reporter.error(
|
|
239
269
|
`⚠️ Guardian token lease attempt ${attempt}/${maxLeaseAttempts} failed — retrying in ${delayMs / 1000}s: ${err}`,
|
|
240
270
|
);
|
|
241
271
|
await new Promise((r) => setTimeout(r, delayMs));
|
|
242
272
|
} else {
|
|
243
|
-
|
|
273
|
+
reporter.error(
|
|
244
274
|
`⚠️ Guardian token lease failed after ${maxLeaseAttempts} attempts: ${err}\n` +
|
|
245
275
|
` The assistant is running but guardian-token.json was not written.\n` +
|
|
246
276
|
` If the desktop app loses its stored credentials, re-hatch to recover.`,
|
|
@@ -261,7 +291,7 @@ export async function hatchLocal(
|
|
|
261
291
|
guardianBootstrapSecret: bootstrapSecret,
|
|
262
292
|
};
|
|
263
293
|
|
|
264
|
-
|
|
294
|
+
reporter.progress(6, 6, "Saving configuration...");
|
|
265
295
|
saveAssistantEntry(localEntry);
|
|
266
296
|
setActiveAssistant(instanceName);
|
|
267
297
|
|
|
@@ -270,13 +300,13 @@ export async function hatchLocal(
|
|
|
270
300
|
}
|
|
271
301
|
|
|
272
302
|
if (provider !== undefined && provider !== null && !guardianAccessToken) {
|
|
273
|
-
|
|
303
|
+
reporter.error(
|
|
274
304
|
`⚠️ Provider credential setup skipped because the guardian token was not leased.\n` +
|
|
275
305
|
` The assistant is still hatched. Run \`vellum setup --provider ${provider}\` after fixing the connection.`,
|
|
276
306
|
);
|
|
277
307
|
} else if (provider !== undefined) {
|
|
278
|
-
|
|
279
|
-
|
|
308
|
+
reporter.log("");
|
|
309
|
+
reporter.log(
|
|
280
310
|
provider === null
|
|
281
311
|
? "Checking provider credentials..."
|
|
282
312
|
: `Checking ${formatProviderName(provider)} credentials...`,
|
|
@@ -289,14 +319,22 @@ export async function hatchLocal(
|
|
|
289
319
|
});
|
|
290
320
|
}
|
|
291
321
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
logHatchNextSteps(
|
|
322
|
+
reporter.log("");
|
|
323
|
+
reporter.log(`✅ Local assistant hatched!`);
|
|
324
|
+
reporter.log("");
|
|
325
|
+
reporter.log("Instance details:");
|
|
326
|
+
reporter.log(` Name: ${instanceName}`);
|
|
327
|
+
reporter.log(` Runtime: ${runtimeUrl}`);
|
|
328
|
+
reporter.log("");
|
|
329
|
+
logHatchNextSteps((message) => reporter.log(message), instanceName);
|
|
330
|
+
|
|
331
|
+
const result: HatchLocalResult = {
|
|
332
|
+
assistantId: instanceName,
|
|
333
|
+
runtimeUrl,
|
|
334
|
+
localUrl: `http://127.0.0.1:${resources.gatewayPort}`,
|
|
335
|
+
species,
|
|
336
|
+
guardianAccessToken,
|
|
337
|
+
};
|
|
300
338
|
|
|
301
339
|
if (keepAlive) {
|
|
302
340
|
const healthUrl = `http://127.0.0.1:${resources.gatewayPort}/healthz`;
|
|
@@ -306,7 +344,7 @@ export async function hatchLocal(
|
|
|
306
344
|
let consecutiveFailures = 0;
|
|
307
345
|
|
|
308
346
|
const shutdown = async (): Promise<void> => {
|
|
309
|
-
|
|
347
|
+
reporter.log("\nShutting down local processes...");
|
|
310
348
|
await stopLocalProcesses(resources);
|
|
311
349
|
process.exit(0);
|
|
312
350
|
};
|
|
@@ -330,7 +368,7 @@ export async function hatchLocal(
|
|
|
330
368
|
consecutiveFailures++;
|
|
331
369
|
}
|
|
332
370
|
if (consecutiveFailures >= MAX_FAILURES) {
|
|
333
|
-
|
|
371
|
+
reporter.log(
|
|
334
372
|
`\n⚠️ ${healthTarget} stopped responding — shutting down.`,
|
|
335
373
|
);
|
|
336
374
|
await stopLocalProcesses(resources);
|
|
@@ -338,4 +376,6 @@ export async function hatchLocal(
|
|
|
338
376
|
}
|
|
339
377
|
}
|
|
340
378
|
}
|
|
379
|
+
|
|
380
|
+
return result;
|
|
341
381
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { emitProgress } from "./desktop-progress.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sink for the human-facing and structured output of long-running lifecycle
|
|
5
|
+
* operations (hatch, retire). Injecting it lets an in-process caller (e.g. a
|
|
6
|
+
* desktop main process embedding these functions) observe progress without the
|
|
7
|
+
* operation writing to the terminal, while the CLI keeps its existing stdout.
|
|
8
|
+
*/
|
|
9
|
+
export interface LifecycleReporter {
|
|
10
|
+
/**
|
|
11
|
+
* Coarse step progress. The CLI reporter mirrors this to the desktop
|
|
12
|
+
* `HATCH_PROGRESS:` stdout channel.
|
|
13
|
+
*/
|
|
14
|
+
progress(step: number, total: number, label: string): void;
|
|
15
|
+
log(message: string): void;
|
|
16
|
+
warn(message: string): void;
|
|
17
|
+
error(message: string): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Reporter used by the CLI commands: human-readable lines to the console plus
|
|
22
|
+
* structured step events on the desktop progress channel. Reproduces the exact
|
|
23
|
+
* terminal output — and the `HATCH_PROGRESS:` lines under `VELLUM_DESKTOP_APP` —
|
|
24
|
+
* that existing subprocess consumers parse.
|
|
25
|
+
*/
|
|
26
|
+
export const consoleLifecycleReporter: LifecycleReporter = {
|
|
27
|
+
progress: (step, total, label) => emitProgress(step, total, label),
|
|
28
|
+
log: (message) => console.log(message),
|
|
29
|
+
warn: (message) => console.warn(message),
|
|
30
|
+
error: (message) => console.error(message),
|
|
31
|
+
};
|
package/src/lib/local.ts
CHANGED
|
@@ -230,8 +230,10 @@ function resolveAssistantIndexPath(): string | undefined {
|
|
|
230
230
|
}
|
|
231
231
|
|
|
232
232
|
try {
|
|
233
|
-
const
|
|
234
|
-
|
|
233
|
+
const assistantPkgPath = _require.resolve(
|
|
234
|
+
"@vellumai/assistant/package.json",
|
|
235
|
+
);
|
|
236
|
+
const resolved = join(dirname(assistantPkgPath), "src", "index.ts");
|
|
235
237
|
if (existsSync(resolved)) {
|
|
236
238
|
return resolved;
|
|
237
239
|
}
|
|
@@ -416,13 +418,13 @@ async function startDaemonFromSource(
|
|
|
416
418
|
writeFileSync(pidFile, "starting", "utf-8");
|
|
417
419
|
|
|
418
420
|
const child = foreground
|
|
419
|
-
? spawn(
|
|
421
|
+
? spawn(process.execPath, ["run", daemonMainPath], {
|
|
420
422
|
stdio: "inherit",
|
|
421
423
|
env,
|
|
422
424
|
})
|
|
423
425
|
: (() => {
|
|
424
426
|
const daemonLogFd = openLogFile("hatch.log");
|
|
425
|
-
const c = spawn(
|
|
427
|
+
const c = spawn(process.execPath, ["run", daemonMainPath], {
|
|
426
428
|
detached: true,
|
|
427
429
|
stdio: ["ignore", "pipe", "pipe"],
|
|
428
430
|
env,
|
|
@@ -486,7 +488,7 @@ async function startDaemonWatchFromSource(
|
|
|
486
488
|
writeFileSync(pidFile, "starting", "utf-8");
|
|
487
489
|
|
|
488
490
|
const daemonLogFd = openLogFile("hatch.log");
|
|
489
|
-
const child = spawn(
|
|
491
|
+
const child = spawn(process.execPath, ["--watch", "run", mainPath], {
|
|
490
492
|
detached: true,
|
|
491
493
|
stdio: ["ignore", "pipe", "pipe"],
|
|
492
494
|
env,
|
|
@@ -514,6 +516,18 @@ function resolveGatewayDir(): string {
|
|
|
514
516
|
return sourceDir;
|
|
515
517
|
}
|
|
516
518
|
|
|
519
|
+
// npm-installed: @vellumai/cli and @vellumai/vellum-gateway are siblings
|
|
520
|
+
const npmGatewayDir = join(
|
|
521
|
+
import.meta.dir,
|
|
522
|
+
"..",
|
|
523
|
+
"..",
|
|
524
|
+
"..",
|
|
525
|
+
"vellum-gateway",
|
|
526
|
+
);
|
|
527
|
+
if (isGatewaySourceDir(npmGatewayDir)) {
|
|
528
|
+
return npmGatewayDir;
|
|
529
|
+
}
|
|
530
|
+
|
|
517
531
|
// Compiled binary: gateway/ bundled adjacent to the CLI executable.
|
|
518
532
|
const binGateway = join(dirname(process.execPath), "gateway");
|
|
519
533
|
if (isGatewaySourceDir(binGateway)) {
|
|
@@ -1135,7 +1149,7 @@ export async function startGateway(
|
|
|
1135
1149
|
? ["--watch", "run", "src/index.ts", "--vellum-gateway"]
|
|
1136
1150
|
: ["run", "src/index.ts", "--vellum-gateway"];
|
|
1137
1151
|
const gwLogFd = openLogFile("hatch.log");
|
|
1138
|
-
gateway = spawn(
|
|
1152
|
+
gateway = spawn(process.execPath, bunArgs, {
|
|
1139
1153
|
cwd: gatewayDir,
|
|
1140
1154
|
detached: true,
|
|
1141
1155
|
stdio: ["ignore", "pipe", "pipe"],
|
package/src/lib/retire-local.ts
CHANGED
|
@@ -3,22 +3,34 @@ import { homedir } from "os";
|
|
|
3
3
|
import { existsSync, mkdirSync, renameSync, writeFileSync } from "fs";
|
|
4
4
|
import { basename, dirname, join } from "path";
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
getDaemonPidPath,
|
|
8
|
-
loadAllAssistants,
|
|
9
|
-
} from "./assistant-config.js";
|
|
6
|
+
import { getDaemonPidPath, loadAllAssistants } from "./assistant-config.js";
|
|
10
7
|
import type { AssistantEntry } from "./assistant-config.js";
|
|
11
8
|
import {
|
|
12
9
|
stopOrphanedDaemonProcesses,
|
|
13
10
|
stopProcessByPidFile,
|
|
14
11
|
} from "./process.js";
|
|
15
12
|
import { getArchivePath, getMetadataPath } from "./retire-archive.js";
|
|
13
|
+
import {
|
|
14
|
+
consoleLifecycleReporter,
|
|
15
|
+
type LifecycleReporter,
|
|
16
|
+
} from "./lifecycle-reporter.js";
|
|
17
|
+
|
|
18
|
+
export interface RetireLocalResult {
|
|
19
|
+
assistantId: string;
|
|
20
|
+
/** Whether the instance data directory was archived (false when skipped). */
|
|
21
|
+
archived: boolean;
|
|
22
|
+
/** Path to the background tar archive, when archiving was started. */
|
|
23
|
+
archivePath?: string;
|
|
24
|
+
/** True when another local assistant shared the data dir, so it was kept. */
|
|
25
|
+
sharedDataDir?: boolean;
|
|
26
|
+
}
|
|
16
27
|
|
|
17
28
|
export async function retireLocal(
|
|
18
29
|
name: string,
|
|
19
30
|
entry: AssistantEntry,
|
|
20
|
-
|
|
21
|
-
|
|
31
|
+
reporter: LifecycleReporter = consoleLifecycleReporter,
|
|
32
|
+
): Promise<RetireLocalResult> {
|
|
33
|
+
reporter.log("\u{1F5D1}\ufe0f Stopping local assistant...\n");
|
|
22
34
|
|
|
23
35
|
if (!entry.resources) {
|
|
24
36
|
throw new Error(
|
|
@@ -38,11 +50,11 @@ export async function retireLocal(
|
|
|
38
50
|
});
|
|
39
51
|
|
|
40
52
|
if (otherSharesDir) {
|
|
41
|
-
|
|
53
|
+
reporter.log(
|
|
42
54
|
` Skipping process stop and archive — another local assistant shares ${vellumDir}.`,
|
|
43
55
|
);
|
|
44
|
-
|
|
45
|
-
return;
|
|
56
|
+
reporter.log("\u2705 Local instance retired (config entry removed only).");
|
|
57
|
+
return { assistantId: name, archived: false, sharedDataDir: true };
|
|
46
58
|
}
|
|
47
59
|
|
|
48
60
|
const daemonPidFile = getDaemonPidPath(resources);
|
|
@@ -87,11 +99,11 @@ export async function retireLocal(
|
|
|
87
99
|
const stagingDir = `${archivePath}.staging`;
|
|
88
100
|
|
|
89
101
|
if (!existsSync(dirToArchive)) {
|
|
90
|
-
|
|
102
|
+
reporter.log(
|
|
91
103
|
` No data directory at ${dirToArchive} — nothing to archive.`,
|
|
92
104
|
);
|
|
93
|
-
|
|
94
|
-
return;
|
|
105
|
+
reporter.log("\u2705 Local instance retired.");
|
|
106
|
+
return { assistantId: name, archived: false };
|
|
95
107
|
}
|
|
96
108
|
|
|
97
109
|
// Ensure the retired archive directory exists before attempting the rename
|
|
@@ -123,6 +135,8 @@ export async function retireLocal(
|
|
|
123
135
|
});
|
|
124
136
|
child.unref();
|
|
125
137
|
|
|
126
|
-
|
|
127
|
-
|
|
138
|
+
reporter.log(`📦 Archiving to ${archivePath} in the background.`);
|
|
139
|
+
reporter.log("\u2705 Local instance retired.");
|
|
140
|
+
|
|
141
|
+
return { assistantId: name, archived: true, archivePath };
|
|
128
142
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derive a message's flat plain-text body from its ordered text segments.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the daemon's `joinWithSpacing` (assistant `daemon/handlers/shared.ts`):
|
|
5
|
+
* adjacent segments are concatenated, inserting a single space between two
|
|
6
|
+
* segments only when neither the end of the left nor the start of the right is
|
|
7
|
+
* already whitespace. Keeping these byte-identical means CLI-rendered text
|
|
8
|
+
* matches what the daemon would have produced for the now-removed flat
|
|
9
|
+
* `content` field.
|
|
10
|
+
*/
|
|
11
|
+
export function segmentsToPlainText(segments: string[] | undefined): string {
|
|
12
|
+
if (!segments || segments.length === 0) {
|
|
13
|
+
return "";
|
|
14
|
+
}
|
|
15
|
+
let result = segments[0] ?? "";
|
|
16
|
+
for (let i = 1; i < segments.length; i++) {
|
|
17
|
+
const prev = result[result.length - 1];
|
|
18
|
+
const next = segments[i]![0];
|
|
19
|
+
// Only insert a space when neither side already has whitespace.
|
|
20
|
+
if (
|
|
21
|
+
prev &&
|
|
22
|
+
next &&
|
|
23
|
+
prev !== " " &&
|
|
24
|
+
prev !== "\n" &&
|
|
25
|
+
prev !== "\t" &&
|
|
26
|
+
next !== " " &&
|
|
27
|
+
next !== "\n" &&
|
|
28
|
+
next !== "\t"
|
|
29
|
+
) {
|
|
30
|
+
result += " ";
|
|
31
|
+
}
|
|
32
|
+
result += segments[i];
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
File without changes
|