feat(hub): guide starter setup (#3651)

* feat(hub): guide setup with provider IDs

* fix(hub): preserve login setup state

* feat(hub): select starter agent runtime

Use the connected daemon's provider snapshot so generated Hub bundles only
contain complete provider, model, and mode combinations.

* fix(hub): stage starter runtime choices

Keep providers with valid modes selectable when the daemon has no default mode, so users explicitly choose the complete runtime.

* fix(hub): scope runtime choices to workspace

Provider capabilities can vary by repository, so request the snapshot for the bundle directory that guided setup will deploy.
This commit is contained in:
Mohamed Boudra
2026-08-21 23:11:55 +02:00
committed by GitHub
parent 956f6ea7ad
commit 8f0c35e924
13 changed files with 1027 additions and 73 deletions
@@ -91,6 +91,56 @@ describe("Hub commands", () => {
assert.equal(result.data.origin, "https://hub.paseo.sh");
});
it("interactive login continues through the injected guided setup coordinator without invoking a CLI command", async () => {
const credentials = new MemoryCredentials();
const events: string[] = [];
await runHubLogin(
"https://hub.test",
{},
{
env: {},
credentials,
flow: {
authorize: async () => {
events.push("login");
return "paseo_cli_prefix_durable-secret";
},
},
isInteractive: () => true,
continueGuidedSetup: async (origin) => {
events.push(`connect:${origin}`);
events.push("init");
events.push("deploy");
},
reporter: quietReporter,
},
);
assert.deepEqual(events, ["login", "connect:https://hub.test", "init", "deploy"]);
});
it("JSON and noninteractive login remain login-only", async () => {
for (const [options, interactive] of [
[{ json: true }, true],
[{}, false],
] as const) {
const credentials = new MemoryCredentials();
let continuationCount = 0;
await runHubLogin(undefined, options, {
env: {},
credentials,
flow: { authorize: async () => "paseo_cli_prefix_durable-secret" },
isInteractive: () => interactive,
continueGuidedSetup: async () => {
continuationCount += 1;
},
reporter: quietReporter,
});
assert.equal(continuationCount, 0);
}
});
it("connect exchanges authority once and gives only the enrollment token to the daemon", async () => {
const credentials = new MemoryCredentials();
credentials.save({ origin: "https://hub.test", credential: "stored-human-secret" });
@@ -484,6 +534,10 @@ class FakeDaemon implements HubDaemonClient {
return { status: hubStatus("connected", this.origin) };
}
async getProvidersSnapshot() {
return { entries: [] };
}
async disconnectHub(force: boolean) {
this.disconnects += 1;
this.disconnectForces.push(force);