@brainjar/cli 0.6.3 → 0.6.5
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/package.json +1 -1
- package/src/client.ts +1 -0
- package/src/commands/persona.ts +5 -2
- package/src/commands/rules.ts +5 -2
- package/src/commands/soul.ts +5 -2
- package/src/daemon.ts +37 -15
package/package.json
CHANGED
package/src/client.ts
CHANGED
package/src/commands/persona.ts
CHANGED
|
@@ -307,8 +307,9 @@ export const persona = Cli.create('persona', {
|
|
|
307
307
|
const name = normalizeSlug(c.args.name, 'persona name')
|
|
308
308
|
const api = await getApi()
|
|
309
309
|
|
|
310
|
+
let result: { affected_brains?: string[] } | undefined
|
|
310
311
|
try {
|
|
311
|
-
await api.delete(`/api/v1/personas/${name}`)
|
|
312
|
+
result = await api.delete<{ affected_brains?: string[] }>(`/api/v1/personas/${name}`)
|
|
312
313
|
} catch (e) {
|
|
313
314
|
if (e instanceof IncurError && e.code === ErrorCode.NOT_FOUND) {
|
|
314
315
|
throw createError(ErrorCode.PERSONA_NOT_FOUND, { params: [name] })
|
|
@@ -320,7 +321,9 @@ export const persona = Cli.create('persona', {
|
|
|
320
321
|
const state = await getEffectiveState(api)
|
|
321
322
|
if (state.persona === name) await sync({ api })
|
|
322
323
|
|
|
323
|
-
|
|
324
|
+
const out: Record<string, unknown> = { deleted: name }
|
|
325
|
+
if (result?.affected_brains?.length) out.affected_brains = result.affected_brains
|
|
326
|
+
return out
|
|
324
327
|
},
|
|
325
328
|
})
|
|
326
329
|
.command('drop', {
|
package/src/commands/rules.ts
CHANGED
|
@@ -247,8 +247,9 @@ export const rules = Cli.create('rules', {
|
|
|
247
247
|
const name = normalizeSlug(c.args.name, 'rule name')
|
|
248
248
|
const api = await getApi()
|
|
249
249
|
|
|
250
|
+
let result: { affected_brains?: string[] } | undefined
|
|
250
251
|
try {
|
|
251
|
-
await api.delete(`/api/v1/rules/${name}`)
|
|
252
|
+
result = await api.delete<{ affected_brains?: string[] }>(`/api/v1/rules/${name}`)
|
|
252
253
|
} catch (e) {
|
|
253
254
|
if (e instanceof IncurError && e.code === ErrorCode.NOT_FOUND) {
|
|
254
255
|
throw createError(ErrorCode.RULE_NOT_FOUND, { params: [name] })
|
|
@@ -260,7 +261,9 @@ export const rules = Cli.create('rules', {
|
|
|
260
261
|
const state = await getEffectiveState(api)
|
|
261
262
|
if (state.rules.includes(name)) await sync({ api })
|
|
262
263
|
|
|
263
|
-
|
|
264
|
+
const out: Record<string, unknown> = { deleted: name }
|
|
265
|
+
if (result?.affected_brains?.length) out.affected_brains = result.affected_brains
|
|
266
|
+
return out
|
|
264
267
|
},
|
|
265
268
|
})
|
|
266
269
|
.command('drop', {
|
package/src/commands/soul.ts
CHANGED
|
@@ -264,8 +264,9 @@ export const soul = Cli.create('soul', {
|
|
|
264
264
|
const name = normalizeSlug(c.args.name, 'soul name')
|
|
265
265
|
const api = await getApi()
|
|
266
266
|
|
|
267
|
+
let result: { affected_brains?: string[] } | undefined
|
|
267
268
|
try {
|
|
268
|
-
await api.delete(`/api/v1/souls/${name}`)
|
|
269
|
+
result = await api.delete<{ affected_brains?: string[] }>(`/api/v1/souls/${name}`)
|
|
269
270
|
} catch (e) {
|
|
270
271
|
if (e instanceof IncurError && e.code === ErrorCode.NOT_FOUND) {
|
|
271
272
|
throw createError(ErrorCode.SOUL_NOT_FOUND, { params: [name] })
|
|
@@ -277,7 +278,9 @@ export const soul = Cli.create('soul', {
|
|
|
277
278
|
const state = await getEffectiveState(api)
|
|
278
279
|
if (state.soul === name) await sync({ api })
|
|
279
280
|
|
|
280
|
-
|
|
281
|
+
const out: Record<string, unknown> = { deleted: name }
|
|
282
|
+
if (result?.affected_brains?.length) out.affected_brains = result.affected_brains
|
|
283
|
+
return out
|
|
281
284
|
},
|
|
282
285
|
})
|
|
283
286
|
.command('drop', {
|
package/src/daemon.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn, execFileSync } from 'node:child_process'
|
|
2
2
|
import { createHash } from 'node:crypto'
|
|
3
|
-
import { readFile, writeFile, rm, access, open, chmod, mkdir, constants } from 'node:fs/promises'
|
|
3
|
+
import { readFile, writeFile, rm, access, open, chmod, mkdir, rename, copyFile, constants } from 'node:fs/promises'
|
|
4
4
|
import { dirname, join } from 'node:path'
|
|
5
5
|
import { tmpdir } from 'node:os'
|
|
6
6
|
import { Errors } from 'incur'
|
|
@@ -31,7 +31,7 @@ const { IncurError } = Errors
|
|
|
31
31
|
* Minimum server version this CLI is compatible with.
|
|
32
32
|
* Bump when the CLI depends on server features/API changes.
|
|
33
33
|
*/
|
|
34
|
-
export const MIN_SERVER_VERSION = '0.2.
|
|
34
|
+
export const MIN_SERVER_VERSION = '0.2.7'
|
|
35
35
|
|
|
36
36
|
export interface HealthStatus {
|
|
37
37
|
healthy: boolean
|
|
@@ -229,9 +229,17 @@ export async function downloadAndVerify(binPath: string, versionBase: string): P
|
|
|
229
229
|
})
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
await chmod(
|
|
232
|
+
// Avoid ETXTBSY: unlink the old binary first (running process
|
|
233
|
+
// keeps its inode), then move the new one into place.
|
|
234
|
+
await chmod(extractedBin, 0o755)
|
|
235
|
+
await rm(binPath, { force: true })
|
|
236
|
+
try {
|
|
237
|
+
await rename(extractedBin, binPath)
|
|
238
|
+
} catch {
|
|
239
|
+
// Cross-device rename (tmpdir on different fs) — fall back to copy
|
|
240
|
+
await copyFile(extractedBin, binPath)
|
|
241
|
+
await chmod(binPath, 0o755)
|
|
242
|
+
}
|
|
235
243
|
} finally {
|
|
236
244
|
await rm(tmpDir, { recursive: true, force: true })
|
|
237
245
|
}
|
|
@@ -275,15 +283,17 @@ export async function upgradeServer(): Promise<{ version: string; alreadyLatest:
|
|
|
275
283
|
return { version, alreadyLatest: true }
|
|
276
284
|
}
|
|
277
285
|
|
|
278
|
-
//
|
|
279
|
-
|
|
280
|
-
if (pid !== null && isAlive(pid)) {
|
|
281
|
-
await stop()
|
|
282
|
-
}
|
|
283
|
-
|
|
286
|
+
// Binary replacement uses rm + rename/copy which avoids ETXTBSY.
|
|
287
|
+
// The running server keeps its old inode; next restart picks up the new binary.
|
|
284
288
|
const versionBase = `${DIST_BASE}/${version}`
|
|
285
289
|
await downloadAndVerify(binPath, versionBase)
|
|
286
290
|
await setInstalledServerVersion(version)
|
|
291
|
+
|
|
292
|
+
// Restart the server on the new binary
|
|
293
|
+
await stop()
|
|
294
|
+
await new Promise(r => setTimeout(r, 1000))
|
|
295
|
+
await start()
|
|
296
|
+
|
|
287
297
|
return { version, alreadyLatest: false }
|
|
288
298
|
}
|
|
289
299
|
|
|
@@ -351,7 +361,14 @@ export async function stop(): Promise<{ stopped: boolean }> {
|
|
|
351
361
|
return { stopped: false }
|
|
352
362
|
}
|
|
353
363
|
|
|
354
|
-
process
|
|
364
|
+
// Kill entire process group (negative pid) so child processes
|
|
365
|
+
// like embedded postgres are also terminated.
|
|
366
|
+
try {
|
|
367
|
+
process.kill(-pid, 'SIGTERM')
|
|
368
|
+
} catch {
|
|
369
|
+
// Process group doesn't exist — try single process
|
|
370
|
+
try { process.kill(pid, 'SIGTERM') } catch {}
|
|
371
|
+
}
|
|
355
372
|
|
|
356
373
|
// Poll for exit, up to 5s
|
|
357
374
|
const deadline = Date.now() + 5000
|
|
@@ -363,10 +380,15 @@ export async function stop(): Promise<{ stopped: boolean }> {
|
|
|
363
380
|
}
|
|
364
381
|
}
|
|
365
382
|
|
|
366
|
-
// Force kill
|
|
383
|
+
// Force kill entire process group
|
|
367
384
|
try {
|
|
368
|
-
process.kill(pid, 'SIGKILL')
|
|
369
|
-
} catch {
|
|
385
|
+
process.kill(-pid, 'SIGKILL')
|
|
386
|
+
} catch {
|
|
387
|
+
try { process.kill(pid, 'SIGKILL') } catch {}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Wait briefly for SIGKILL to take effect
|
|
391
|
+
await new Promise(r => setTimeout(r, 500))
|
|
370
392
|
await rm(pid_file, { force: true })
|
|
371
393
|
return { stopped: true }
|
|
372
394
|
}
|