@plosson/agentio 0.8.0 → 0.8.1

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": "@plosson/agentio",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "CLI for LLM agents to interact with communication and tracking services",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -394,21 +394,44 @@ describe('runTeleport — preflight failures', () => {
394
394
  );
395
395
  });
396
396
 
397
- test('app already exists → CliError + warning', async () => {
397
+ test('app already exists → rebuild in place (no create, key preserved)', async () => {
398
398
  const deps = makeDeps({
399
399
  existingApp: { name: 'mcp', url: 'https://mcp.existing.com' },
400
400
  });
401
- await expect(runTeleport({ name: 'mcp' }, deps)).rejects.toThrow(
402
- /already exists/
403
- );
404
- // A warning was emitted before the throw so the user has context.
405
- expect(deps.warnLines.some((l) => l.includes('siteio apps rm'))).toBe(
406
- true
407
- );
408
- // Must not have attempted to create/set/deploy.
401
+ const result = await runTeleport({ name: 'mcp' }, deps);
402
+
409
403
  const methods = deps.calls.map((c) => c.method);
404
+ // Rebuild MUST NOT create the app — it already exists.
410
405
  expect(methods).not.toContain('createApp');
411
- expect(methods).not.toContain('deploy');
406
+ // But it MUST push env + redeploy so the image is rebuilt.
407
+ expect(methods).toContain('setApp');
408
+ expect(methods).toContain('deploy');
409
+
410
+ // setApp on rebuild preserves AGENTIO_SERVER_API_KEY by omitting it
411
+ // from the env map (siteio merges env vars).
412
+ const setCall = deps.calls.find((c) => c.method === 'setApp');
413
+ expect(setCall).toBeDefined();
414
+ const envVars = (setCall!.args as { envVars?: Record<string, string> })
415
+ .envVars;
416
+ expect(envVars).toBeDefined();
417
+ expect(Object.keys(envVars!).sort()).toEqual([
418
+ 'AGENTIO_CONFIG',
419
+ 'AGENTIO_KEY',
420
+ ]);
421
+ expect(envVars!.AGENTIO_SERVER_API_KEY).toBeUndefined();
422
+
423
+ // Result signals a rebuild: no new server API key emitted.
424
+ expect(result.serverApiKey).toBe('');
425
+
426
+ // Log messages surface the rebuild intent, and the success banner
427
+ // is distinct from a fresh deploy.
428
+ const logs = deps.logLines.join('\n');
429
+ expect(logs).toMatch(/rebuild/i);
430
+ expect(logs).toContain('Rebuild complete!');
431
+ expect(logs).not.toContain('Teleport complete!');
432
+ // We do NOT print the onboarding snippet on rebuild — clients
433
+ // already have their bearer.
434
+ expect(logs).not.toContain('To add to Claude Code:');
412
435
  });
413
436
  });
414
437
 
@@ -513,27 +513,52 @@ export async function runTeleport(
513
513
  }
514
514
  deps.log(`Found ${profileCount} local profile(s).`);
515
515
 
516
- // App must not already exist.
516
+ // Check whether the app already exists on siteio. If it does, we
517
+ // REBUILD in place: skip createApp, preserve the existing
518
+ // AGENTIO_SERVER_API_KEY (so Claude clients keep their /authorize PIN
519
+ // and their issued bearers), backfill /data if needed, and redeploy
520
+ // the freshly generated image. If it doesn't exist, fall through to
521
+ // the fresh-deploy path.
517
522
  deps.log(`Checking if siteio app "${opts.name}" already exists…`);
518
523
  const existing = await deps.runner.findApp(opts.name);
519
- if (existing) {
520
- deps.warn(
521
- `A siteio app named "${opts.name}" already exists. ` +
522
- `Run \`siteio apps rm ${opts.name}\` if you want to redeploy from scratch.`
524
+ const isRebuild = Boolean(existing);
525
+ if (isRebuild) {
526
+ deps.log(
527
+ `Found existing siteio app "${opts.name}" will rebuild image in place (API key and clients preserved).`
523
528
  );
524
- throw new CliError(
525
- 'INVALID_PARAMS',
526
- `App "${opts.name}" already exists on siteio`
529
+ } else {
530
+ deps.log(
531
+ `No existing siteio app "${opts.name}" will create a fresh one.`
527
532
  );
528
533
  }
529
534
 
530
- // Generate a fresh server API key for the remote.
531
- const serverApiKey = deps.generateServerApiKey();
535
+ // Only generate a new operator API key on fresh deploys. On rebuild
536
+ // the remote's AGENTIO_SERVER_API_KEY is left untouched (siteio's
537
+ // `apps set -e` merges env vars, so omitting a key preserves it).
538
+ const serverApiKey = isRebuild ? '' : deps.generateServerApiKey();
532
539
 
533
- // Export the local config.
534
- deps.log('Exporting local configuration…');
540
+ // Export the local config (always — we want rebuild to also pick up
541
+ // any profile additions since the last deploy).
542
+ deps.log(
543
+ isRebuild
544
+ ? 'Re-exporting local configuration…'
545
+ : 'Exporting local configuration…'
546
+ );
535
547
  const exported = await deps.generateExportData();
536
548
 
549
+ // On rebuild, detect whether /data is already mounted so we backfill
550
+ // the persistent volume if it's missing (same logic as --sync).
551
+ let needsVolumeBackfill = false;
552
+ if (isRebuild) {
553
+ const detail = await deps.runner.appInfo(opts.name);
554
+ needsVolumeBackfill = !hasDataVolumeMount(detail);
555
+ if (needsVolumeBackfill) {
556
+ deps.log(
557
+ `No persistent volume mounted at ${DATA_VOLUME_PATH} — will attach ${volumeNameFor(opts.name)}:${DATA_VOLUME_PATH} as part of this rebuild.`
558
+ );
559
+ }
560
+ }
561
+
537
562
  // Resolve git mode settings up front so dry-run can show the same
538
563
  // command shape the real run would use.
539
564
  const isGitMode = Boolean(opts.gitBranch);
@@ -562,21 +587,37 @@ export async function runTeleport(
562
587
  // Dry-run: report what would happen and exit.
563
588
  if (opts.dryRun) {
564
589
  deps.log('--- Dry run: the following commands would be executed ---');
565
- if (gitSettings) {
566
- deps.log(
567
- `siteio apps create ${opts.name} -g ${gitSettings.repoUrl} --branch ${gitSettings.branch} --dockerfile ${TELEPORT_DOCKERFILE_PATH} -p 9999`
568
- );
569
- } else {
570
- deps.log(
571
- `siteio apps create ${opts.name} -f <tempfile> -p 9999`
572
- );
590
+ if (!isRebuild) {
591
+ if (gitSettings) {
592
+ deps.log(
593
+ `siteio apps create ${opts.name} -g ${gitSettings.repoUrl} --branch ${gitSettings.branch} --dockerfile ${TELEPORT_DOCKERFILE_PATH} -p 9999`
594
+ );
595
+ } else {
596
+ deps.log(
597
+ `siteio apps create ${opts.name} -f <tempfile> -p 9999`
598
+ );
599
+ }
573
600
  }
574
- deps.log(
575
- `siteio apps set ${opts.name} -e AGENTIO_KEY=<redacted> -e AGENTIO_CONFIG=<${exported.config.length} chars> -e AGENTIO_SERVER_API_KEY=${serverApiKey}`
576
- );
601
+ const setParts = [
602
+ `siteio apps set ${opts.name}`,
603
+ '-e AGENTIO_KEY=<redacted>',
604
+ `-e AGENTIO_CONFIG=<${exported.config.length} chars>`,
605
+ ];
606
+ if (!isRebuild) {
607
+ setParts.push(`-e AGENTIO_SERVER_API_KEY=${serverApiKey}`);
608
+ }
609
+ if (!isRebuild || needsVolumeBackfill) {
610
+ setParts.push(`-v ${volumeNameFor(opts.name)}:${DATA_VOLUME_PATH}`);
611
+ }
612
+ deps.log(setParts.join(' '));
577
613
  deps.log(
578
614
  `siteio apps deploy ${opts.name}${opts.noCache ? ' --no-cache' : ''}`
579
615
  );
616
+ if (isRebuild) {
617
+ deps.log(
618
+ '(AGENTIO_SERVER_API_KEY is intentionally NOT touched — operator key on the remote stays the same.)'
619
+ );
620
+ }
580
621
  if (!gitSettings) {
581
622
  const dockerfile = deps.generateDockerfile();
582
623
  deps.log('--- Dockerfile that would be uploaded ---');
@@ -601,41 +642,62 @@ export async function runTeleport(
601
642
  : await deps.writeTempFile(deps.generateDockerfile());
602
643
 
603
644
  try {
604
- deps.log(`Creating siteio app "${opts.name}"…`);
605
- if (gitSettings) {
606
- await deps.runner.createApp({
607
- name: opts.name,
608
- port: 9999,
609
- git: {
610
- repoUrl: gitSettings.repoUrl,
611
- branch: gitSettings.branch,
612
- dockerfilePath: TELEPORT_DOCKERFILE_PATH,
613
- },
614
- });
645
+ if (!isRebuild) {
646
+ deps.log(`Creating siteio app "${opts.name}"…`);
647
+ if (gitSettings) {
648
+ await deps.runner.createApp({
649
+ name: opts.name,
650
+ port: 9999,
651
+ git: {
652
+ repoUrl: gitSettings.repoUrl,
653
+ branch: gitSettings.branch,
654
+ dockerfilePath: TELEPORT_DOCKERFILE_PATH,
655
+ },
656
+ });
657
+ } else {
658
+ await deps.runner.createApp({
659
+ name: opts.name,
660
+ dockerfilePath: tempPath!,
661
+ port: 9999,
662
+ });
663
+ }
664
+ }
665
+
666
+ // On rebuild, omit AGENTIO_SERVER_API_KEY so siteio's env-merge
667
+ // preserves the existing value (clients keep their bearer). Only
668
+ // attach /data when not already mounted — siteio REPLACES the
669
+ // volumes list on update, so blindly passing it would clobber
670
+ // other mounts the operator added.
671
+ const envVars: Record<string, string> = {
672
+ AGENTIO_KEY: exported.key,
673
+ AGENTIO_CONFIG: exported.config,
674
+ ...(isRebuild ? {} : { AGENTIO_SERVER_API_KEY: serverApiKey }),
675
+ };
676
+ const attachVolume = !isRebuild || needsVolumeBackfill;
677
+
678
+ if (isRebuild) {
679
+ deps.log(
680
+ attachVolume
681
+ ? 'Updating env vars + attaching persistent volume on siteio…'
682
+ : 'Updating environment variables on siteio…'
683
+ );
615
684
  } else {
616
- await deps.runner.createApp({
617
- name: opts.name,
618
- dockerfilePath: tempPath!,
619
- port: 9999,
620
- });
685
+ deps.log('Setting environment variables and persistent volume…');
621
686
  }
622
687
 
623
- deps.log('Setting environment variables and persistent volume…');
624
688
  await deps.runner.setApp({
625
689
  name: opts.name,
626
- envVars: {
627
- AGENTIO_KEY: exported.key,
628
- AGENTIO_CONFIG: exported.config,
629
- AGENTIO_SERVER_API_KEY: serverApiKey,
630
- },
631
- // Persistent named volume mounted at /data so config.server.tokens
632
- // (issued OAuth bearers) survive container restarts. Without this
633
- // mount, every restart wipes the bearer and connected clients
634
- // would re-run the OAuth flow.
635
- volumes: { [volumeNameFor(opts.name)]: DATA_VOLUME_PATH },
690
+ envVars,
691
+ ...(attachVolume
692
+ ? { volumes: { [volumeNameFor(opts.name)]: DATA_VOLUME_PATH } }
693
+ : {}),
636
694
  });
637
695
 
638
- deps.log('Deploying (this may take a minute — Docker is building your image)…');
696
+ deps.log(
697
+ isRebuild
698
+ ? 'Rebuilding (this may take a minute — Docker is rebuilding your image)…'
699
+ : 'Deploying (this may take a minute — Docker is building your image)…'
700
+ );
639
701
  await deps.runner.deploy({
640
702
  name: opts.name,
641
703
  // In git mode, there's no -f to re-pass on deploy — siteio uses
@@ -693,7 +755,7 @@ export async function runTeleport(
693
755
  : null;
694
756
 
695
757
  deps.log('');
696
- deps.log('Teleport complete!');
758
+ deps.log(isRebuild ? 'Rebuild complete!' : 'Teleport complete!');
697
759
  if (url) {
698
760
  deps.log(` URL: ${url}`);
699
761
  deps.log(` Health: ${url}/health`);
@@ -703,10 +765,14 @@ export async function runTeleport(
703
765
  ` URL: (siteio did not return a URL — run \`siteio apps info ${opts.name}\` to look it up)`
704
766
  );
705
767
  }
706
- deps.log(` API key: ${serverApiKey}`);
707
- deps.log(' (you will type this into the Authorize page when Claude Code first connects)');
768
+ if (isRebuild) {
769
+ deps.log(' API key: (unchanged existing clients keep their bearer)');
770
+ } else {
771
+ deps.log(` API key: ${serverApiKey}`);
772
+ deps.log(' (you will type this into the Authorize page when Claude Code first connects)');
773
+ }
708
774
  deps.log('');
709
- if (claudeCmd) {
775
+ if (claudeCmd && !isRebuild) {
710
776
  deps.log('To add to Claude Code:');
711
777
  deps.log(` ${claudeCmd}`);
712
778
  deps.log(