@getpaseo/cli 0.5.0-beta.4 → 0.5.0

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.
@@ -1,3 +1,4 @@
1
+ import type { ProviderSnapshotEntry } from "@getpaseo/protocol/agent-types";
1
2
  export interface HubStatus {
2
3
  state: string;
3
4
  daemonId: string | null;
@@ -6,6 +7,9 @@ export interface HubStatus {
6
7
  connectedAt: string | null;
7
8
  lastError: string | null;
8
9
  }
10
+ export interface HubProvidersSnapshotOptions {
11
+ cwd?: string;
12
+ }
9
13
  export interface HubDaemonClient {
10
14
  connectHub(url: string, token: string): Promise<{
11
15
  status: HubStatus;
@@ -17,6 +21,9 @@ export interface HubDaemonClient {
17
21
  status: HubStatus;
18
22
  warning?: string;
19
23
  }>;
24
+ getProvidersSnapshot(options?: HubProvidersSnapshotOptions): Promise<{
25
+ entries: ProviderSnapshotEntry[];
26
+ }>;
20
27
  close(): Promise<void>;
21
28
  }
22
29
  export interface HubDaemonConnection {
@@ -1,6 +1,6 @@
1
1
  import type { HubBundleFile } from "../deploy-bundle.js";
2
- import { type CliAuthorization, type CliAuthorizationPoll, type HubInstallResult, type HubConfigurationResources, type HubProject, type HubValidationResult } from "./internal/contracts.js";
3
- export type { CliAuthorization, CliAuthorizationPoll, HubInstallResult, HubConfigurationResources, HubProject, HubValidationResult, } from "./internal/contracts.js";
2
+ import { type CliAuthorization, type CliAuthorizationPoll, type HubInstallResult, type HubConfigurationResources, type HubSetupResources, type HubProject, type HubValidationResult } from "./internal/contracts.js";
3
+ export type { CliAuthorization, CliAuthorizationPoll, HubInstallResult, HubConfigurationResources, HubSetupResources, HubProject, HubValidationResult, } from "./internal/contracts.js";
4
4
  interface HubConfigurationInput {
5
5
  origin: string;
6
6
  apiKey: string;
@@ -12,6 +12,7 @@ export declare class HubHttpClient {
12
12
  pollCliAuthorization(origin: string, deviceCode: string, timeoutMilliseconds: number): Promise<CliAuthorizationPoll>;
13
13
  listProjects(origin: string, apiKey: string): Promise<HubProject[]>;
14
14
  listConfigurationResources(origin: string, apiKey: string): Promise<HubConfigurationResources>;
15
+ listSetupResources(origin: string, apiKey: string): Promise<HubSetupResources>;
15
16
  installConfiguration(input: HubConfigurationInput): Promise<HubInstallResult>;
16
17
  validateConfiguration(input: HubConfigurationInput): Promise<HubValidationResult>;
17
18
  issueEnrollmentToken(origin: string, apiKey: string): Promise<string>;
@@ -1,5 +1,5 @@
1
1
  import { HubCommandError } from "../error.js";
2
- import { authorizationPollSchema, authorizationSchema, configurationResourcesSchema, enrollmentTokenSchema, installResponseSchema, projectsResponseSchema, validationResponseSchema, } from "./internal/contracts.js";
2
+ import { authorizationPollSchema, authorizationSchema, configurationResourcesSchema, enrollmentTokenSchema, installResponseSchema, projectsResponseSchema, validationResponseSchema, setupResourcesSchema, } from "./internal/contracts.js";
3
3
  import { requestHub } from "./internal/transport.js";
4
4
  export class HubHttpClient {
5
5
  startCliAuthorization(origin) {
@@ -56,6 +56,17 @@ export class HubHttpClient {
56
56
  failureMessage: "Hub configuration resource listing failed",
57
57
  });
58
58
  }
59
+ listSetupResources(origin, apiKey) {
60
+ return requestHub({
61
+ origin,
62
+ path: "/api/v1/setup-resources",
63
+ method: "GET",
64
+ apiKey,
65
+ successStatus: 200,
66
+ schema: setupResourcesSchema,
67
+ failureMessage: "Hub guided setup resource listing failed",
68
+ });
69
+ }
59
70
  installConfiguration(input) {
60
71
  return requestHub({
61
72
  origin: input.origin,
@@ -62,6 +62,22 @@ export declare const configurationResourcesSchema: z.ZodObject<{
62
62
  teamName: z.ZodString;
63
63
  }, z.core.$strict>>;
64
64
  }, z.core.$strict>;
65
+ export declare const setupResourcesSchema: z.ZodObject<{
66
+ github: z.ZodArray<z.ZodObject<{
67
+ slug: z.ZodString;
68
+ accountLogin: z.ZodString;
69
+ accountType: z.ZodString;
70
+ repositories: z.ZodArray<z.ZodString>;
71
+ }, z.core.$strict>>;
72
+ discord: z.ZodArray<z.ZodObject<{
73
+ guildId: z.ZodString;
74
+ guildName: z.ZodString;
75
+ }, z.core.$strict>>;
76
+ slack: z.ZodArray<z.ZodObject<{
77
+ teamId: z.ZodString;
78
+ teamName: z.ZodString;
79
+ }, z.core.$strict>>;
80
+ }, z.core.$strict>;
65
81
  export declare const installResponseSchema: z.ZodObject<{
66
82
  projectSlug: z.ZodString;
67
83
  version: z.ZodNumber;
@@ -80,6 +96,7 @@ export type CliAuthorization = z.infer<typeof authorizationSchema>;
80
96
  export type CliAuthorizationPoll = z.infer<typeof authorizationPollSchema>;
81
97
  export type HubProject = z.infer<typeof projectSchema>;
82
98
  export type HubConfigurationResources = z.infer<typeof configurationResourcesSchema>;
99
+ export type HubSetupResources = z.infer<typeof setupResourcesSchema>;
83
100
  export type HubInstallResult = z.infer<typeof installResponseSchema>;
84
101
  export type HubValidationResult = z.infer<typeof validationResponseSchema>;
85
102
  export {};
@@ -45,6 +45,20 @@ export const configurationResourcesSchema = z
45
45
  slack: z.array(z.object({ slug: z.string().min(1), teamName: z.string().min(1) }).strict()),
46
46
  })
47
47
  .strict();
48
+ export const setupResourcesSchema = z
49
+ .object({
50
+ github: z.array(z
51
+ .object({
52
+ slug: z.string().min(1),
53
+ accountLogin: z.string().min(1),
54
+ accountType: z.string().min(1),
55
+ repositories: z.array(z.string().min(1)),
56
+ })
57
+ .strict()),
58
+ discord: z.array(z.object({ guildId: z.string().min(1), guildName: z.string().min(1) }).strict()),
59
+ slack: z.array(z.object({ teamId: z.string().min(1), teamName: z.string().min(1) }).strict()),
60
+ })
61
+ .strict();
48
62
  export const installResponseSchema = z
49
63
  .object({
50
64
  projectSlug: z.string().min(1),
@@ -14,7 +14,7 @@ import { addHubProjectsCommand } from "./projects.js";
14
14
  import { processHubReporter } from "./reporter.js";
15
15
  import { hubStatusResult } from "./status-output.js";
16
16
  import { addHubResolutionHelp } from "./help.js";
17
- import { addHubInitCommand } from "./init.js";
17
+ import { addHubInitCommand, continueHubGuidedSetup } from "./init.js";
18
18
  function productionEnvironment() {
19
19
  const env = process.env;
20
20
  const hub = new HubHttpClient();
@@ -37,6 +37,8 @@ export function createHubCommand(overrides = {}) {
37
37
  credentials: environment.credentials,
38
38
  flow: environment.login,
39
39
  reporter: environment.reporter,
40
+ isInteractive: environment.isInteractive,
41
+ continueGuidedSetup: (origin) => continueHubGuidedSetup(origin, environment),
40
42
  });
41
43
  addHubInitCommand(hub, environment);
42
44
  addHubConnectCommand(hub, {
@@ -1,6 +1,7 @@
1
1
  import type { HubDeployBundle } from "./deploy-bundle.js";
2
2
  import type { HubProject } from "./hub-client/index.js";
3
3
  import type { HubStatus } from "./daemon-client.js";
4
+ import type { HubStarterAgentRuntime } from "./starter-agent-runtime.js";
4
5
  export type HubInitProvider = "github" | "slack" | "discord";
5
6
  export type HubInitProjectResolution = {
6
7
  kind: "none";
@@ -30,6 +31,7 @@ export interface HubInitOpeningPlan {
30
31
  export interface HubInitScaffoldInput {
31
32
  cwd: string;
32
33
  daemonSlug: string;
34
+ agent: HubStarterAgentRuntime;
33
35
  provider: HubInitProvider;
34
36
  providerFilters: Readonly<Record<string, string>>;
35
37
  }
@@ -39,6 +41,7 @@ export interface HubInitScaffold {
39
41
  workflow: string;
40
42
  testAction: string;
41
43
  }
44
+ export declare function hubLoginResumeCommand(step: "connect" | "init", origin: string): string;
42
45
  export declare function planHubInitOpening(input: {
43
46
  loggedIn: boolean;
44
47
  paseoDirectoryExists: boolean;
@@ -1,5 +1,8 @@
1
1
  import path from "node:path";
2
2
  import YAML from "yaml";
3
+ export function hubLoginResumeCommand(step, origin) {
4
+ return step === "connect" ? `paseo hub connect ${origin}` : "paseo hub init";
5
+ }
3
6
  export function planHubInitOpening(input) {
4
7
  return {
5
8
  replaceExisting: input.paseoDirectoryExists,
@@ -47,9 +50,10 @@ export function createHubInitScaffold(input) {
47
50
  },
48
51
  },
49
52
  agents: {
50
- codex: {
51
- provider: "codex",
52
- mode: "full-access",
53
+ starter: {
54
+ provider: input.agent.provider,
55
+ model: input.agent.model,
56
+ ...(input.agent.mode === undefined ? {} : { mode: input.agent.mode }),
53
57
  },
54
58
  },
55
59
  }, { lineWidth: 0 });
@@ -114,7 +118,7 @@ function workflow(input) {
114
118
  environment: input.environment,
115
119
  max_runtime: "90m",
116
120
  idle_timeout: "10m",
117
- agent: "codex",
121
+ agent: "starter",
118
122
  prompt: [
119
123
  {
120
124
  text: `${replyInstruction}complete this request and call hub.finish_execution when done.\n\n<user-prompt>\n\${{ paseo.prompt }}\n</user-prompt>\n`,
@@ -1,10 +1,11 @@
1
+ import { select, text } from "@clack/prompts";
1
2
  import type { Command } from "commander";
2
3
  import type { HubCredentialStore } from "./credentials.js";
3
4
  import type { HubDaemonConnection } from "./daemon-client.js";
4
5
  import type { HubHttpClient } from "./hub-client/index.js";
5
6
  import type { CliLoginFlow } from "./login-flow.js";
6
7
  import type { HubReporter } from "./reporter.js";
7
- interface HubInitEnvironment {
8
+ export interface HubGuidedSetupEnvironment {
8
9
  env: Readonly<Record<string, string | undefined>>;
9
10
  credentials: HubCredentialStore;
10
11
  hub: HubHttpClient;
@@ -12,9 +13,23 @@ interface HubInitEnvironment {
12
13
  daemon: HubDaemonConnection;
13
14
  reporter: HubReporter;
14
15
  cwd(): string;
16
+ isInteractive?(): boolean;
17
+ prompts?: {
18
+ confirm(message: string, initialValue: boolean): Promise<boolean>;
19
+ select(options: Parameters<typeof select<string>>[0]): Promise<string>;
20
+ text(options: Parameters<typeof text>[0]): Promise<string>;
21
+ message(value: string): void;
22
+ };
15
23
  }
16
- export declare function addHubInitCommand(parent: Command, environment: HubInitEnvironment): void;
17
- export declare function runHubInit(environment: HubInitEnvironment): Promise<void>;
24
+ interface HubGuidedSetupState {
25
+ origin?: string;
26
+ daemonId?: string;
27
+ deploy?: boolean;
28
+ }
29
+ export declare function addHubInitCommand(parent: Command, environment: HubGuidedSetupEnvironment): void;
30
+ export declare function runHubInit(environment: HubGuidedSetupEnvironment): Promise<void>;
31
+ export declare function runHubGuidedSetup(environment: HubGuidedSetupEnvironment, state?: HubGuidedSetupState): Promise<void>;
32
+ export declare function continueHubGuidedSetup(origin: string, environment: HubGuidedSetupEnvironment): Promise<void>;
18
33
  export declare function githubRepositoryFromRemote(remote: string): string | undefined;
19
34
  export {};
20
35
  //# sourceMappingURL=init.d.ts.map
@@ -7,15 +7,17 @@ import { promisify } from "node:util";
7
7
  import { DEFAULT_HUB_ORIGIN, resolveHubCredential } from "./authority.js";
8
8
  import { withHubDaemon } from "./daemon-client.js";
9
9
  import { HubCommandError } from "./error.js";
10
- import { createHubInitBundle, createHubInitScaffold, planHubInitOpening, resolveHubInitConnection, resolveHubInitProjects, } from "./init-plan.js";
11
- import { runHubLogin } from "./login.js";
10
+ import { createHubInitBundle, createHubInitScaffold, hubLoginResumeCommand, planHubInitOpening, resolveHubInitConnection, resolveHubInitProjects, } from "./init-plan.js";
12
11
  import { runHubConnect } from "./connect.js";
13
12
  import { runHubDeployBundle } from "./deploy.js";
14
13
  import { runHubProjects } from "./projects.js";
15
14
  import { normalizeHubOrigin } from "./origin.js";
15
+ import { selectedStarterAgentRuntime, starterAgentProviderSnapshotState, suggestedStarterAgentChoice, } from "./starter-agent-runtime.js";
16
+ import { availableStarterTriggerConnections, } from "./starter-trigger.js";
16
17
  const execFileAsync = promisify(execFile);
17
18
  const DAEMON_READY_TIMEOUT_MS = 60000;
18
19
  const DAEMON_READY_POLL_MS = 250;
20
+ const PROVIDER_READY_TIMEOUT_MS = 60000;
19
21
  class HubInitCancelledError extends Error {
20
22
  }
21
23
  function initErrorMessage(error) {
@@ -43,7 +45,10 @@ export function addHubInitCommand(parent, environment) {
43
45
  });
44
46
  }
45
47
  export async function runHubInit(environment) {
46
- requireInteractiveTerminal();
48
+ await runHubGuidedSetup(environment);
49
+ }
50
+ export async function runHubGuidedSetup(environment, state = {}) {
51
+ requireInteractiveTerminal(environment);
47
52
  intro("Set up Paseo Hub");
48
53
  const cwd = environment.cwd();
49
54
  const activeLogin = environment.credentials.active();
@@ -52,24 +57,25 @@ export async function runHubInit(environment) {
52
57
  paseoDirectoryExists: await pathExists(path.join(cwd, ".paseo")),
53
58
  });
54
59
  if (opening.replaceExisting &&
55
- !(await requiredConfirm("Replace the existing .paseo/ Hub bundle?", false))) {
60
+ !(await requiredConfirm(environment, "Replace the existing .paseo/ Hub bundle?", false))) {
56
61
  throw new HubInitCancelledError("Existing .paseo/ bundle left unchanged.");
57
62
  }
58
- const origin = await ensureLogin(activeLogin?.origin, environment);
59
- const daemonId = await ensureDaemonConnection(origin, environment);
63
+ const origin = state.origin ?? (await ensureLogin(activeLogin?.origin, environment));
64
+ const daemonId = state.daemonId ?? (await ensureDaemonConnection(origin, environment));
60
65
  const project = await chooseProject(origin, environment);
61
- const resources = await loadConfigurationResources(origin, environment);
62
- const daemon = resources.daemons.find(({ id }) => id === daemonId);
63
- if (daemon === undefined) {
64
- throw new HubCommandError("HUB_DAEMON_RESOURCE_MISSING", "The connected daemon is not available in this Hub organization. Reconnect it and try again.");
65
- }
66
+ const resources = await loadSetupResources(origin, environment);
67
+ const triggerConnections = await resolveStarterTriggerConnections(resources, cwd);
68
+ reportStarterTriggerConnections(environment, triggerConnections);
69
+ const trigger = await chooseStarterTriggerConnection(environment, triggerConnections);
70
+ const daemon = await loadDaemonResource(origin, daemonId, environment);
66
71
  log.success(`Connected as ${daemon.slug}`);
67
- const provider = await chooseProvider();
68
- const providerFilters = await collectProviderFilters(provider, resources, cwd);
72
+ const agent = await chooseStarterAgentRuntime(environment, cwd);
73
+ const providerFilters = await collectProviderIdentity(trigger, environment);
69
74
  const scaffold = createHubInitScaffold({
70
75
  cwd,
71
76
  daemonSlug: daemon.slug,
72
- provider,
77
+ agent,
78
+ provider: trigger.provider,
73
79
  providerFilters,
74
80
  });
75
81
  const bundle = createHubInitBundle(project.slug, scaffold);
@@ -85,7 +91,7 @@ export async function runHubInit(environment) {
85
91
  log.success("Dry run passed");
86
92
  await writeScaffold(cwd, scaffold, opening.replaceExisting);
87
93
  log.success(`Created .paseo/hub.yml and ${scaffold.workflowPath}`);
88
- const deploy = await requiredConfirm("Deploy now?", true);
94
+ const deploy = state.deploy ?? (await requiredConfirm(environment, "Deploy now?", true));
89
95
  if (deploy) {
90
96
  await withSpinner("Deploying bundle", async () => {
91
97
  await runHubDeployBundle({ project: project.slug, hub: origin }, bundle, {
@@ -99,14 +105,41 @@ export async function runHubInit(environment) {
99
105
  log.success("Deployed");
100
106
  }
101
107
  else {
102
- log.message(`Skipped deployment. Run: paseo hub deploy -p ${project.slug}`);
108
+ reportMessage(environment, `Skipped deployment. Run: paseo hub deploy -p ${project.slug}`);
103
109
  }
104
110
  const activityUrl = new URL(`/projects/${project.slug}/activity`, origin).toString();
105
111
  note(`${scaffold.testAction}\nWatch it at ${activityUrl}`, "Test your workflow");
106
112
  outro(deploy ? "Hub is ready" : "Hub bundle is ready");
107
113
  }
114
+ export async function continueHubGuidedSetup(origin, environment) {
115
+ if (!(await requiredConfirm(environment, "Connect this daemon to this Hub?", true))) {
116
+ reportMessage(environment, `Skipped daemon connection. Run: ${hubLoginResumeCommand("connect", origin)}; then paseo hub init`);
117
+ return;
118
+ }
119
+ const daemonId = await ensureDaemonConnection(origin, environment, true);
120
+ if (!(await requiredConfirm(environment, "Initialize and deploy a starter workflow?", true))) {
121
+ reportMessage(environment, `Skipped starter workflow. Run: ${hubLoginResumeCommand("init", origin)}`);
122
+ return;
123
+ }
124
+ try {
125
+ await runHubGuidedSetup(environment, { origin, daemonId, deploy: true });
126
+ }
127
+ catch (error) {
128
+ if (error instanceof HubInitCancelledError) {
129
+ if (environment.prompts === undefined) {
130
+ log.message(error.message);
131
+ outro("Connect an app to continue");
132
+ }
133
+ else {
134
+ environment.prompts.message(error.message);
135
+ }
136
+ return;
137
+ }
138
+ throw error;
139
+ }
140
+ }
108
141
  async function ensureLogin(activeOrigin, environment) {
109
- const endpoint = await requiredSelect({
142
+ const endpoint = await requiredSelect(environment, {
110
143
  message: "Hub endpoint",
111
144
  initialValue: activeOrigin === undefined || activeOrigin === DEFAULT_HUB_ORIGIN ? "hosted" : "custom",
112
145
  options: [
@@ -116,7 +149,7 @@ async function ensureLogin(activeOrigin, environment) {
116
149
  });
117
150
  const origin = endpoint === "hosted"
118
151
  ? DEFAULT_HUB_ORIGIN
119
- : await requiredText({
152
+ : await requiredText(environment, {
120
153
  message: "Custom Hub URL",
121
154
  initialValue: activeOrigin === undefined || activeOrigin === DEFAULT_HUB_ORIGIN
122
155
  ? environment.env.PASEO_HUB_URL
@@ -136,16 +169,13 @@ async function ensureLogin(activeOrigin, environment) {
136
169
  log.success(`Logged in to ${normalizedOrigin}`);
137
170
  return normalizedOrigin;
138
171
  }
139
- await runHubLogin(normalizedOrigin, {}, {
140
- env: environment.env,
141
- credentials: environment.credentials,
142
- flow: environment.login,
143
- reporter: environment.reporter,
144
- });
172
+ environment.reporter.progress(`Logging in to ${normalizedOrigin}`);
173
+ const credential = await environment.login.authorize(normalizedOrigin);
174
+ environment.credentials.save({ origin: normalizedOrigin, credential });
145
175
  log.success(`Logged in to ${normalizedOrigin}`);
146
176
  return normalizedOrigin;
147
177
  }
148
- async function ensureDaemonConnection(origin, environment) {
178
+ async function ensureDaemonConnection(origin, environment, confirmed = false) {
149
179
  const status = await withHubDaemon(environment.daemon, undefined, async (daemon) => daemon.getHubStatus().then((response) => response.status));
150
180
  const connection = resolveHubInitConnection(status, origin);
151
181
  if (connection.kind === "connected") {
@@ -157,9 +187,13 @@ async function ensureDaemonConnection(origin, environment) {
157
187
  if (connection.kind === "conflict") {
158
188
  throw new HubCommandError("HUB_DAEMON_ALREADY_CONNECTED", `This daemon is connected to ${connection.origin}. Disconnect it before running Hub init for ${origin}.`);
159
189
  }
160
- if (!(await requiredConfirm(`Connect this daemon to ${origin}?`, true))) {
190
+ if (!confirmed &&
191
+ !(await requiredConfirm(environment, `Connect this daemon to ${origin}?`, true))) {
161
192
  throw new HubInitCancelledError("A connected daemon is required to create the bundle.");
162
193
  }
194
+ return connectDaemon(origin, environment);
195
+ }
196
+ async function connectDaemon(origin, environment) {
163
197
  await runHubConnect(origin, {}, {
164
198
  env: environment.env,
165
199
  credentials: environment.credentials,
@@ -205,7 +239,7 @@ async function chooseProject(origin, environment) {
205
239
  if (resolution.kind === "selected") {
206
240
  return resolution.project;
207
241
  }
208
- const slug = await requiredSelect({
242
+ const slug = await requiredSelect(environment, {
209
243
  message: "Project",
210
244
  options: resolution.projects.map((project) => ({
211
245
  value: project.slug,
@@ -219,56 +253,146 @@ async function chooseProject(origin, environment) {
219
253
  }
220
254
  return project;
221
255
  }
222
- async function chooseProvider() {
223
- return requiredSelect({
224
- message: "Trigger provider",
225
- options: [
226
- { value: "github", label: "GitHub", hint: "issue or pull request comment" },
227
- { value: "slack", label: "Slack", hint: "channel mention" },
228
- { value: "discord", label: "Discord", hint: "channel mention" },
229
- ],
256
+ async function resolveStarterTriggerConnections(resources, cwd) {
257
+ const remote = await readCommandValue("git", ["remote", "get-url", "origin"], cwd);
258
+ const repository = remote === undefined ? undefined : githubRepositoryFromRemote(remote);
259
+ const connections = availableStarterTriggerConnections(resources, repository);
260
+ if (connections.length === 0) {
261
+ throw new HubInitCancelledError("No Hub app connection is ready for this workflow.\nConnect GitHub, Slack, or Discord in Hub → Apps, then run `paseo hub init` again.");
262
+ }
263
+ return connections;
264
+ }
265
+ function reportStarterTriggerConnections(environment, connections) {
266
+ const details = `${connections.map(({ label }) => label).join("\n")}\n\nOnly configured connections are shown. To add another, open Hub → Apps, then run \`paseo hub init\` again.`;
267
+ if (environment.prompts === undefined) {
268
+ note(details, "Hub app connections ready for this workflow");
269
+ return;
270
+ }
271
+ environment.prompts.message(`Hub app connections ready for this workflow:\n${details}`);
272
+ }
273
+ async function chooseStarterTriggerConnection(environment, connections) {
274
+ if (connections.length === 1) {
275
+ const connection = connections[0];
276
+ reportMessage(environment, `Using ${connection.label}`);
277
+ return connection;
278
+ }
279
+ const id = await requiredSelect(environment, {
280
+ message: "Trigger connection",
281
+ options: connections.map((connection) => ({ value: connection.id, label: connection.label })),
230
282
  });
283
+ const connection = connections.find((candidate) => candidate.id === id);
284
+ if (connection === undefined) {
285
+ throw new HubCommandError("HUB_PROVIDER_CONNECTION_INVALID", "The selected Hub app connection is no longer available. Run paseo hub init again.");
286
+ }
287
+ return connection;
231
288
  }
232
- async function collectProviderFilters(provider, resources, cwd) {
233
- if (provider === "github") {
234
- const [login, originRemote] = await Promise.all([
235
- readGhValue(["api", "user", "--jq", ".login"]),
236
- readCommandValue("git", ["remote", "get-url", "origin"], cwd),
237
- ]);
238
- const repo = originRemote === undefined ? undefined : githubRepositoryFromRemote(originRemote);
239
- if (repo === undefined) {
240
- throw new HubCommandError("HUB_GITHUB_REPOSITORY_UNDETECTED", "Could not detect a GitHub repository from the origin remote.");
241
- }
242
- if (!resources.github.some(({ repositories }) => repositories.includes(repo))) {
243
- const connected = resources.github.flatMap(({ repositories }) => repositories);
244
- throw new HubCommandError("HUB_GITHUB_REPOSITORY_NOT_CONNECTED", `${repo} is not connected to this Hub organization. Connected repositories: ${connected.join(", ") || "none"}.`);
289
+ async function chooseStarterAgentRuntime(environment, cwd) {
290
+ const providers = await waitForStarterAgentProviders(environment, cwd);
291
+ const provider = await chooseStarterAgentProvider(environment, providers);
292
+ const model = await chooseStarterAgentModel(environment, provider);
293
+ const mode = await chooseStarterAgentMode(environment, provider);
294
+ const runtime = selectedStarterAgentRuntime(provider, model, mode);
295
+ if (runtime === undefined) {
296
+ throw new HubCommandError("HUB_AGENT_RUNTIME_SELECTION_INVALID", "The selected starter agent runtime is no longer available. Run paseo hub init again.");
297
+ }
298
+ return runtime;
299
+ }
300
+ async function waitForStarterAgentProviders(environment, cwd) {
301
+ return withSpinner("Discovering agent runtimes", async (reporter) => withHubDaemon(environment.daemon, undefined, async (daemon) => {
302
+ const deadline = Date.now() + PROVIDER_READY_TIMEOUT_MS;
303
+ while (true) {
304
+ const snapshot = await daemon.getProvidersSnapshot({ cwd });
305
+ const state = starterAgentProviderSnapshotState(snapshot.entries);
306
+ if (state.kind === "ready")
307
+ return state.providers;
308
+ if (state.kind === "unavailable") {
309
+ throw new HubCommandError("HUB_AGENT_RUNTIME_REQUIRED", "No usable agent runtime is available from this daemon. Configure an enabled provider with a selectable model, then run paseo hub init again.");
310
+ }
311
+ if (Date.now() >= deadline) {
312
+ throw new HubCommandError("HUB_AGENT_RUNTIME_TIMEOUT", "Agent runtime discovery did not finish within 60 seconds. Check the daemon's provider configuration, then run paseo hub init again.");
313
+ }
314
+ reporter.progress("Waiting for agent runtime discovery");
315
+ await delay(DAEMON_READY_POLL_MS);
245
316
  }
317
+ }));
318
+ }
319
+ async function chooseStarterAgentProvider(environment, providers) {
320
+ const selected = await requiredSelect(environment, {
321
+ message: "Starter agent provider",
322
+ options: providers.map((provider) => ({
323
+ value: provider.id,
324
+ label: provider.label,
325
+ })),
326
+ });
327
+ const provider = providers.find((candidate) => candidate.id === selected);
328
+ if (provider === undefined)
329
+ throw invalidStarterAgentSelection();
330
+ return provider;
331
+ }
332
+ async function chooseStarterAgentModel(environment, provider) {
333
+ const suggested = suggestedStarterAgentChoice(provider.models);
334
+ const selected = await requiredSelect(environment, {
335
+ message: "Starter agent model",
336
+ ...(suggested === undefined ? {} : { initialValue: suggested.id }),
337
+ options: provider.models.map((model) => ({
338
+ value: model.id,
339
+ label: model.label,
340
+ ...(model.suggested ? { hint: "suggested" } : {}),
341
+ })),
342
+ });
343
+ if (!provider.models.some((model) => model.id === selected))
344
+ throw invalidStarterAgentSelection();
345
+ return selected;
346
+ }
347
+ async function chooseStarterAgentMode(environment, provider) {
348
+ if (provider.modes.length === 0)
349
+ return undefined;
350
+ const suggested = suggestedStarterAgentChoice(provider.modes);
351
+ const selected = await requiredSelect(environment, {
352
+ message: "Starter agent mode",
353
+ ...(suggested === undefined ? {} : { initialValue: suggested.id }),
354
+ options: provider.modes.map((mode) => ({
355
+ value: mode.id,
356
+ label: mode.label,
357
+ ...(mode.suggested ? { hint: "suggested" } : {}),
358
+ })),
359
+ });
360
+ if (!provider.modes.some((mode) => mode.id === selected))
361
+ throw invalidStarterAgentSelection();
362
+ return selected;
363
+ }
364
+ function invalidStarterAgentSelection() {
365
+ return new HubCommandError("HUB_AGENT_RUNTIME_SELECTION_INVALID", "The selected starter agent runtime is no longer available. Run paseo hub init again.");
366
+ }
367
+ async function collectProviderIdentity(trigger, environment) {
368
+ if (trigger.provider === "github") {
369
+ const login = await readGhValue(["api", "user", "--jq", ".login"]);
246
370
  return {
247
- user: await requiredText({
371
+ ...trigger.filters,
372
+ user: await requiredText(environment, {
248
373
  message: "Your GitHub username (only this user can trigger the bot)",
249
374
  initialValue: login,
250
375
  }),
251
- repo,
252
376
  };
253
377
  }
254
- if (provider === "slack") {
378
+ if (trigger.provider === "slack") {
255
379
  return {
256
- workspace: await chooseConnection("Slack workspace", resources.slack.map(({ slug, teamName }) => ({ slug, label: teamName }))),
257
- user: await requiredText({
258
- message: "Your Slack username (only this user can trigger the bot)",
380
+ ...trigger.filters,
381
+ user: await requiredText(environment, {
382
+ message: "Your Slack member ID (only this user can trigger the bot)",
259
383
  }),
260
384
  };
261
385
  }
262
386
  return {
263
- guild: await chooseConnection("Discord server", resources.discord.map(({ slug, guildName }) => ({ slug, label: guildName }))),
264
- user: await requiredText({
265
- message: "Your Discord username (only this user can trigger the bot)",
387
+ ...trigger.filters,
388
+ user: await requiredText(environment, {
389
+ message: "Your Discord user ID (only this user can trigger the bot)",
266
390
  }),
267
391
  };
268
392
  }
269
- async function loadConfigurationResources(origin, environment) {
393
+ async function loadSetupResources(origin, environment) {
270
394
  try {
271
- return await withSpinner("Loading Hub connections", () => environment.hub.listConfigurationResources(origin, resolveHubCredential({
395
+ return await withSpinner("Loading Hub connections", () => environment.hub.listSetupResources(origin, resolveHubCredential({
272
396
  options: { origin },
273
397
  env: environment.env,
274
398
  credentials: environment.credentials,
@@ -277,21 +401,23 @@ async function loadConfigurationResources(origin, environment) {
277
401
  }
278
402
  catch (error) {
279
403
  if (error instanceof HubCommandError && error.code === "HUB_NOT_FOUND") {
280
- throw new HubCommandError("HUB_UPDATE_REQUIRED", "This Hub does not support guided setup. Update the Hub and try again.");
404
+ throw new HubCommandError("HUB_UPDATE_REQUIRED", "This Hub needs an update before guided setup can use provider IDs. Update Hub and try again.");
281
405
  }
282
406
  throw error;
283
407
  }
284
408
  }
285
- async function chooseConnection(message, connections) {
286
- if (connections.length === 0) {
287
- throw new HubCommandError("HUB_PROVIDER_CONNECTION_REQUIRED", `No ${message.toLowerCase()} is connected in this Hub organization. Connect one and try again.`);
409
+ async function loadDaemonResource(origin, daemonId, environment) {
410
+ const resources = await environment.hub.listConfigurationResources(origin, resolveHubCredential({
411
+ options: { origin },
412
+ env: environment.env,
413
+ credentials: environment.credentials,
414
+ origin,
415
+ }));
416
+ const daemon = resources.daemons.find(({ id }) => id === daemonId);
417
+ if (daemon === undefined) {
418
+ throw new HubCommandError("HUB_DAEMON_RESOURCE_MISSING", "The connected daemon is not available in this Hub organization. Reconnect it and try again.");
288
419
  }
289
- if (connections.length === 1)
290
- return connections[0].slug;
291
- return requiredSelect({
292
- message,
293
- options: connections.map(({ slug, label }) => ({ value: slug, label })),
294
- });
420
+ return daemon;
295
421
  }
296
422
  async function writeScaffold(cwd, scaffold, replaceExisting) {
297
423
  const root = path.join(cwd, ".paseo");
@@ -380,8 +506,8 @@ async function withSpinner(message, action) {
380
506
  throw error;
381
507
  }
382
508
  }
383
- async function requiredText(options) {
384
- const answer = await text({
509
+ async function requiredText(environment, options) {
510
+ const request = {
385
511
  ...options,
386
512
  validate(value) {
387
513
  const input = value ?? "";
@@ -390,26 +516,40 @@ async function requiredText(options) {
390
516
  return customError;
391
517
  return input.trim().length === 0 ? "A value is required" : undefined;
392
518
  },
393
- });
519
+ };
520
+ const answer = environment.prompts === undefined
521
+ ? await text(request)
522
+ : await environment.prompts.text(request);
394
523
  if (isCancel(answer))
395
524
  throw new HubInitCancelledError("Hub init cancelled.");
396
525
  return answer.trim();
397
526
  }
398
- async function requiredConfirm(message, initialValue) {
399
- const answer = await confirm({ message, initialValue });
527
+ async function requiredConfirm(environment, message, initialValue) {
528
+ const answer = environment.prompts === undefined
529
+ ? await confirm({ message, initialValue })
530
+ : await environment.prompts.confirm(message, initialValue);
400
531
  if (isCancel(answer))
401
532
  throw new HubInitCancelledError("Hub init cancelled.");
402
533
  return answer;
403
534
  }
404
- async function requiredSelect(options) {
405
- const answer = await select(options);
535
+ async function requiredSelect(environment, options) {
536
+ const answer = environment.prompts === undefined
537
+ ? await select(options)
538
+ : (await environment.prompts.select(options));
406
539
  if (isCancel(answer))
407
540
  throw new HubInitCancelledError("Hub init cancelled.");
408
541
  return answer;
409
542
  }
410
- function requireInteractiveTerminal() {
411
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
543
+ function requireInteractiveTerminal(environment) {
544
+ if (!(environment.isInteractive?.() ?? (process.stdin.isTTY && process.stdout.isTTY))) {
412
545
  throw new HubCommandError("HUB_INIT_INTERACTIVE_REQUIRED", "paseo hub init requires a TTY.");
413
546
  }
414
547
  }
548
+ function reportMessage(environment, message) {
549
+ if (environment.prompts === undefined) {
550
+ log.message(message);
551
+ return;
552
+ }
553
+ environment.prompts.message(message);
554
+ }
415
555
  //# sourceMappingURL=init.js.map
@@ -12,6 +12,8 @@ interface HubLoginDependencies {
12
12
  credentials: HubCredentialStore;
13
13
  flow: Pick<CliLoginFlow, "authorize">;
14
14
  reporter: HubReporter;
15
+ isInteractive?(): boolean;
16
+ continueGuidedSetup?(origin: string): Promise<void>;
15
17
  }
16
18
  export declare function runHubLogin(originInput: string | undefined, options: {
17
19
  json?: boolean;
@@ -19,6 +19,12 @@ export async function runHubLogin(originInput, options, dependencies) {
19
19
  reportHubProgress(dependencies.reporter, options, `Logging in to ${origin}`);
20
20
  const credential = await dependencies.flow.authorize(origin);
21
21
  dependencies.credentials.save({ origin, credential });
22
+ reportHubProgress(dependencies.reporter, options, "Logged in");
23
+ if (!options.json &&
24
+ dependencies.isInteractive?.() &&
25
+ dependencies.continueGuidedSetup !== undefined) {
26
+ await dependencies.continueGuidedSetup(origin);
27
+ }
22
28
  return { type: "single", data: { origin, status: "logged_in" }, schema };
23
29
  }
24
30
  export function addHubLoginCommand(parent, dependencies) {
@@ -0,0 +1,37 @@
1
+ import type { ProviderSnapshotEntry } from "@getpaseo/protocol/agent-types";
2
+ export interface HubStarterAgentRuntime {
3
+ provider: string;
4
+ model: string;
5
+ mode?: string;
6
+ }
7
+ export interface HubStarterAgentProvider {
8
+ id: string;
9
+ label: string;
10
+ models: readonly HubStarterAgentModel[];
11
+ modes: readonly HubStarterAgentMode[];
12
+ }
13
+ export interface HubStarterAgentModel {
14
+ id: string;
15
+ label: string;
16
+ suggested: boolean;
17
+ }
18
+ export interface HubStarterAgentMode {
19
+ id: string;
20
+ label: string;
21
+ suggested: boolean;
22
+ }
23
+ export type HubStarterAgentProviderSnapshotState = {
24
+ kind: "ready";
25
+ providers: readonly HubStarterAgentProvider[];
26
+ } | {
27
+ kind: "loading";
28
+ } | {
29
+ kind: "unavailable";
30
+ };
31
+ export declare function availableStarterAgentProviders(entries: readonly ProviderSnapshotEntry[]): HubStarterAgentProvider[];
32
+ export declare function starterAgentProviderSnapshotState(entries: readonly ProviderSnapshotEntry[]): HubStarterAgentProviderSnapshotState;
33
+ export declare function suggestedStarterAgentChoice<T extends {
34
+ suggested: boolean;
35
+ }>(choices: readonly T[]): T | undefined;
36
+ export declare function selectedStarterAgentRuntime(provider: HubStarterAgentProvider, modelId: string, modeId?: string): HubStarterAgentRuntime | undefined;
37
+ //# sourceMappingURL=starter-agent-runtime.d.ts.map
@@ -0,0 +1,49 @@
1
+ export function availableStarterAgentProviders(entries) {
2
+ return entries.flatMap((entry) => {
3
+ if (entry.status !== "ready" || !entry.enabled)
4
+ return [];
5
+ const models = (entry.models ?? [])
6
+ .filter((model) => model.isSelectable !== false)
7
+ .map((model) => ({ id: model.id, label: model.label, suggested: model.isDefault === true }));
8
+ if (models.length === 0)
9
+ return [];
10
+ const modes = (entry.modes ?? []).map((mode) => ({
11
+ id: mode.id,
12
+ label: mode.label,
13
+ suggested: mode.id === entry.defaultModeId,
14
+ }));
15
+ return [
16
+ {
17
+ id: entry.provider,
18
+ label: entry.label ?? entry.provider,
19
+ models,
20
+ modes,
21
+ },
22
+ ];
23
+ });
24
+ }
25
+ export function starterAgentProviderSnapshotState(entries) {
26
+ const providers = availableStarterAgentProviders(entries);
27
+ if (providers.length > 0)
28
+ return { kind: "ready", providers };
29
+ if (entries.length === 0)
30
+ return { kind: "loading" };
31
+ if (entries.some((entry) => entry.enabled && entry.status === "loading")) {
32
+ return { kind: "loading" };
33
+ }
34
+ return { kind: "unavailable" };
35
+ }
36
+ export function suggestedStarterAgentChoice(choices) {
37
+ return choices.find((choice) => choice.suggested);
38
+ }
39
+ export function selectedStarterAgentRuntime(provider, modelId, modeId) {
40
+ if (!provider.models.some((model) => model.id === modelId))
41
+ return undefined;
42
+ if (provider.modes.length === 0) {
43
+ return modeId === undefined ? { provider: provider.id, model: modelId } : undefined;
44
+ }
45
+ if (modeId === undefined || !provider.modes.some((mode) => mode.id === modeId))
46
+ return undefined;
47
+ return { provider: provider.id, model: modelId, mode: modeId };
48
+ }
49
+ //# sourceMappingURL=starter-agent-runtime.js.map
@@ -0,0 +1,10 @@
1
+ import type { HubSetupResources } from "./hub-client/index.js";
2
+ import type { HubInitProvider } from "./init-plan.js";
3
+ export interface HubStarterTriggerConnection {
4
+ id: string;
5
+ label: string;
6
+ provider: HubInitProvider;
7
+ filters: Readonly<Record<string, string>>;
8
+ }
9
+ export declare function availableStarterTriggerConnections(resources: HubSetupResources, githubRepository?: string): HubStarterTriggerConnection[];
10
+ //# sourceMappingURL=starter-trigger.d.ts.map
@@ -0,0 +1,28 @@
1
+ export function availableStarterTriggerConnections(resources, githubRepository) {
2
+ return [
3
+ ...(githubRepository !== undefined &&
4
+ resources.github.some(({ repositories }) => repositories.includes(githubRepository))
5
+ ? [
6
+ {
7
+ id: `github:${githubRepository}`,
8
+ label: `GitHub — ${githubRepository}`,
9
+ provider: "github",
10
+ filters: { repo: githubRepository },
11
+ },
12
+ ]
13
+ : []),
14
+ ...resources.slack.map(({ teamId, teamName }) => ({
15
+ id: `slack:${teamId}`,
16
+ label: `Slack — ${teamName}`,
17
+ provider: "slack",
18
+ filters: { workspace: teamId },
19
+ })),
20
+ ...resources.discord.map(({ guildId, guildName }) => ({
21
+ id: `discord:${guildId}`,
22
+ label: `Discord — ${guildName}`,
23
+ provider: "discord",
24
+ filters: { guild: guildId },
25
+ })),
26
+ ];
27
+ }
28
+ //# sourceMappingURL=starter-trigger.js.map
@@ -19,6 +19,6 @@ function formatQr(qr, columns) {
19
19
  return qr;
20
20
  }
21
21
  export function formatPairingInstructions({ url, qr, columns }) {
22
- return `\nScan to pair:\n${formatQr(qr, columns)}\n\nPairing link:\n${url}\n`;
22
+ return `\nScan to pair:\n${formatQr(qr, columns)}\n\nPairing link:\n${url}\n\nTreat this pairing link like a password. Anyone with it can access this daemon.\n`;
23
23
  }
24
24
  //# sourceMappingURL=pairing.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getpaseo/cli",
3
- "version": "0.5.0-beta.4",
3
+ "version": "0.5.0",
4
4
  "description": "Paseo CLI - control your AI coding agents from the command line",
5
5
  "bin": {
6
6
  "paseo": "bin/paseo"
@@ -28,9 +28,9 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@clack/prompts": "^1.0.0",
31
- "@getpaseo/client": "0.5.0-beta.4",
32
- "@getpaseo/protocol": "0.5.0-beta.4",
33
- "@getpaseo/server": "0.5.0-beta.4",
31
+ "@getpaseo/client": "0.5.0",
32
+ "@getpaseo/protocol": "0.5.0",
33
+ "@getpaseo/server": "0.5.0",
34
34
  "chalk": "^5.3.0",
35
35
  "commander": "^12.0.0",
36
36
  "mime-types": "^2.1.35",