@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 +1 -1
- package/src/commands/teleport.test.ts +33 -10
- package/src/commands/teleport.ts +121 -55
package/package.json
CHANGED
|
@@ -394,21 +394,44 @@ describe('runTeleport — preflight failures', () => {
|
|
|
394
394
|
);
|
|
395
395
|
});
|
|
396
396
|
|
|
397
|
-
test('app already exists →
|
|
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
|
|
402
|
-
|
|
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
|
-
|
|
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
|
|
package/src/commands/teleport.ts
CHANGED
|
@@ -513,27 +513,52 @@ export async function runTeleport(
|
|
|
513
513
|
}
|
|
514
514
|
deps.log(`Found ${profileCount} local profile(s).`);
|
|
515
515
|
|
|
516
|
-
//
|
|
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
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
-
|
|
525
|
-
|
|
526
|
-
`
|
|
529
|
+
} else {
|
|
530
|
+
deps.log(
|
|
531
|
+
`No existing siteio app "${opts.name}" — will create a fresh one.`
|
|
527
532
|
);
|
|
528
533
|
}
|
|
529
534
|
|
|
530
|
-
//
|
|
531
|
-
|
|
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
|
-
|
|
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 (
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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
|
-
|
|
575
|
-
`siteio apps set ${opts.name}
|
|
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
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
-
|
|
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
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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(
|
|
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
|
-
|
|
707
|
-
|
|
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(
|