@brainjar/cli 0.6.3 → 0.6.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainjar/cli",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "description": "Shape how your AI thinks — composable soul, persona, and rules for AI agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/client.ts CHANGED
@@ -153,6 +153,7 @@ export async function createClient(options?: ClientOptions): Promise<BrainjarCli
153
153
  throw new IncurError({ code, message, hint })
154
154
  }
155
155
 
156
+ if (response.status === 204) return undefined as T
156
157
  return response.json() as Promise<T>
157
158
  }
158
159
 
@@ -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
- return { deleted: name }
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', {
@@ -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
- return { deleted: name }
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', {
@@ -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
- return { deleted: name }
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
@@ -279,6 +279,18 @@ export async function upgradeServer(): Promise<{ version: string; alreadyLatest:
279
279
  const pid = await readPid(localContext(config).pid_file)
280
280
  if (pid !== null && isAlive(pid)) {
281
281
  await stop()
282
+
283
+ // Verify process is actually dead before replacing binary
284
+ const deadline = Date.now() + 3000
285
+ while (Date.now() < deadline && isAlive(pid)) {
286
+ await new Promise(r => setTimeout(r, 100))
287
+ }
288
+ if (isAlive(pid)) {
289
+ throw createError(ErrorCode.SERVER_START_FAILED, {
290
+ message: `Server process (PID ${pid}) is still running. Cannot replace binary.`,
291
+ hint: `Kill it manually: kill -9 ${pid}`,
292
+ })
293
+ }
282
294
  }
283
295
 
284
296
  const versionBase = `${DIST_BASE}/${version}`
@@ -351,7 +363,14 @@ export async function stop(): Promise<{ stopped: boolean }> {
351
363
  return { stopped: false }
352
364
  }
353
365
 
354
- process.kill(pid, 'SIGTERM')
366
+ // Kill entire process group (negative pid) so child processes
367
+ // like embedded postgres are also terminated.
368
+ try {
369
+ process.kill(-pid, 'SIGTERM')
370
+ } catch {
371
+ // Process group doesn't exist — try single process
372
+ try { process.kill(pid, 'SIGTERM') } catch {}
373
+ }
355
374
 
356
375
  // Poll for exit, up to 5s
357
376
  const deadline = Date.now() + 5000
@@ -363,10 +382,15 @@ export async function stop(): Promise<{ stopped: boolean }> {
363
382
  }
364
383
  }
365
384
 
366
- // Force kill
385
+ // Force kill entire process group
367
386
  try {
368
- process.kill(pid, 'SIGKILL')
369
- } catch {}
387
+ process.kill(-pid, 'SIGKILL')
388
+ } catch {
389
+ try { process.kill(pid, 'SIGKILL') } catch {}
390
+ }
391
+
392
+ // Wait briefly for SIGKILL to take effect
393
+ await new Promise(r => setTimeout(r, 500))
370
394
  await rm(pid_file, { force: true })
371
395
  return { stopped: true }
372
396
  }