@agentuity/cli 0.0.110 → 0.0.111
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/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +19 -4
- package/dist/cli.js.map +1 -1
- package/dist/cmd/build/vite/agent-discovery.d.ts +1 -1
- package/dist/cmd/build/vite/agent-discovery.d.ts.map +1 -1
- package/dist/cmd/build/vite/agent-discovery.js +3 -3
- package/dist/cmd/build/vite/agent-discovery.js.map +1 -1
- package/dist/cmd/build/vite/index.js +1 -1
- package/dist/cmd/build/vite/index.js.map +1 -1
- package/dist/cmd/build/vite/metadata-generator.js +1 -1
- package/dist/cmd/build/vite/metadata-generator.js.map +1 -1
- package/dist/cmd/build/vite/registry-generator.d.ts +1 -1
- package/dist/cmd/build/vite/registry-generator.d.ts.map +1 -1
- package/dist/cmd/build/vite/registry-generator.js +70 -23
- package/dist/cmd/build/vite/registry-generator.js.map +1 -1
- package/dist/cmd/build/vite/route-discovery.d.ts +6 -0
- package/dist/cmd/build/vite/route-discovery.d.ts.map +1 -1
- package/dist/cmd/build/vite/route-discovery.js +19 -0
- package/dist/cmd/build/vite/route-discovery.js.map +1 -1
- package/dist/cmd/build/vite/vite-builder.js +1 -1
- package/dist/cmd/build/vite/vite-builder.js.map +1 -1
- package/dist/cmd/cloud/deploy.d.ts.map +1 -1
- package/dist/cmd/cloud/deploy.js +63 -1
- package/dist/cmd/cloud/deploy.js.map +1 -1
- package/dist/cmd/cloud/sandbox/create.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/create.js +18 -0
- package/dist/cmd/cloud/sandbox/create.js.map +1 -1
- package/dist/cmd/cloud/sandbox/delete.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/delete.js +2 -6
- package/dist/cmd/cloud/sandbox/delete.js.map +1 -1
- package/dist/cmd/cloud/sandbox/download.d.ts +3 -0
- package/dist/cmd/cloud/sandbox/download.d.ts.map +1 -0
- package/dist/cmd/cloud/sandbox/download.js +89 -0
- package/dist/cmd/cloud/sandbox/download.js.map +1 -0
- package/dist/cmd/cloud/sandbox/env.d.ts +3 -0
- package/dist/cmd/cloud/sandbox/env.d.ts.map +1 -0
- package/dist/cmd/cloud/sandbox/env.js +90 -0
- package/dist/cmd/cloud/sandbox/env.js.map +1 -0
- package/dist/cmd/cloud/sandbox/get.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/get.js +5 -0
- package/dist/cmd/cloud/sandbox/get.js.map +1 -1
- package/dist/cmd/cloud/sandbox/index.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/index.js +14 -0
- package/dist/cmd/cloud/sandbox/index.js.map +1 -1
- package/dist/cmd/cloud/sandbox/ls.d.ts +3 -0
- package/dist/cmd/cloud/sandbox/ls.d.ts.map +1 -0
- package/dist/cmd/cloud/sandbox/ls.js +119 -0
- package/dist/cmd/cloud/sandbox/ls.js.map +1 -0
- package/dist/cmd/cloud/sandbox/mkdir.d.ts +3 -0
- package/dist/cmd/cloud/sandbox/mkdir.d.ts.map +1 -0
- package/dist/cmd/cloud/sandbox/mkdir.js +59 -0
- package/dist/cmd/cloud/sandbox/mkdir.js.map +1 -0
- package/dist/cmd/cloud/sandbox/rm.d.ts +3 -0
- package/dist/cmd/cloud/sandbox/rm.d.ts.map +1 -0
- package/dist/cmd/cloud/sandbox/rm.js +45 -0
- package/dist/cmd/cloud/sandbox/rm.js.map +1 -0
- package/dist/cmd/cloud/sandbox/rmdir.d.ts +3 -0
- package/dist/cmd/cloud/sandbox/rmdir.d.ts.map +1 -0
- package/dist/cmd/cloud/sandbox/rmdir.js +59 -0
- package/dist/cmd/cloud/sandbox/rmdir.js.map +1 -0
- package/dist/cmd/cloud/sandbox/snapshot/create.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/snapshot/create.js +0 -2
- package/dist/cmd/cloud/sandbox/snapshot/create.js.map +1 -1
- package/dist/cmd/cloud/sandbox/snapshot/get.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/snapshot/get.js +0 -2
- package/dist/cmd/cloud/sandbox/snapshot/get.js.map +1 -1
- package/dist/cmd/cloud/sandbox/snapshot/list.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/snapshot/list.js +0 -3
- package/dist/cmd/cloud/sandbox/snapshot/list.js.map +1 -1
- package/dist/cmd/cloud/sandbox/upload.d.ts +3 -0
- package/dist/cmd/cloud/sandbox/upload.d.ts.map +1 -0
- package/dist/cmd/cloud/sandbox/upload.js +77 -0
- package/dist/cmd/cloud/sandbox/upload.js.map +1 -0
- package/dist/cmd/dev/index.d.ts.map +1 -1
- package/dist/cmd/dev/index.js +17 -8
- package/dist/cmd/dev/index.js.map +1 -1
- package/dist/cmd/dev/sync.d.ts.map +1 -1
- package/dist/cmd/dev/sync.js +8 -14
- package/dist/cmd/dev/sync.js.map +1 -1
- package/dist/cmd/git/account/add.d.ts +17 -0
- package/dist/cmd/git/account/add.d.ts.map +1 -0
- package/dist/cmd/git/account/add.js +244 -0
- package/dist/cmd/git/account/add.js.map +1 -0
- package/dist/cmd/git/account/index.d.ts +3 -0
- package/dist/cmd/git/account/index.d.ts.map +1 -0
- package/dist/cmd/git/account/index.js +11 -0
- package/dist/cmd/git/account/index.js.map +1 -0
- package/dist/cmd/git/account/list.d.ts +2 -0
- package/dist/cmd/git/account/list.d.ts.map +1 -0
- package/dist/cmd/git/account/list.js +111 -0
- package/dist/cmd/git/account/list.js.map +1 -0
- package/dist/cmd/git/account/remove.d.ts +2 -0
- package/dist/cmd/git/account/remove.d.ts.map +1 -0
- package/dist/cmd/git/account/remove.js +171 -0
- package/dist/cmd/git/account/remove.js.map +1 -0
- package/dist/cmd/git/index.d.ts +3 -0
- package/dist/cmd/git/index.d.ts.map +1 -0
- package/dist/cmd/git/index.js +19 -0
- package/dist/cmd/git/index.js.map +1 -0
- package/dist/cmd/git/link.d.ts +32 -0
- package/dist/cmd/git/link.d.ts.map +1 -0
- package/dist/cmd/git/link.js +357 -0
- package/dist/cmd/git/link.js.map +1 -0
- package/dist/cmd/git/list.d.ts +2 -0
- package/dist/cmd/git/list.d.ts.map +1 -0
- package/dist/cmd/git/list.js +137 -0
- package/dist/cmd/git/list.js.map +1 -0
- package/dist/cmd/git/status.d.ts +2 -0
- package/dist/cmd/git/status.d.ts.map +1 -0
- package/dist/cmd/git/status.js +119 -0
- package/dist/cmd/git/status.js.map +1 -0
- package/dist/cmd/git/unlink.d.ts +2 -0
- package/dist/cmd/git/unlink.d.ts.map +1 -0
- package/dist/cmd/git/unlink.js +98 -0
- package/dist/cmd/git/unlink.js.map +1 -0
- package/dist/cmd/index.d.ts.map +1 -1
- package/dist/cmd/index.js +2 -0
- package/dist/cmd/index.js.map +1 -1
- package/dist/cmd/integration/api.d.ts +61 -0
- package/dist/cmd/integration/api.d.ts.map +1 -0
- package/dist/cmd/integration/api.js +176 -0
- package/dist/cmd/integration/api.js.map +1 -0
- package/dist/cmd/integration/github/connect.d.ts +2 -0
- package/dist/cmd/integration/github/connect.d.ts.map +1 -0
- package/dist/cmd/integration/github/connect.js +197 -0
- package/dist/cmd/integration/github/connect.js.map +1 -0
- package/dist/cmd/integration/github/disconnect.d.ts +2 -0
- package/dist/cmd/integration/github/disconnect.d.ts.map +1 -0
- package/dist/cmd/integration/github/disconnect.js +121 -0
- package/dist/cmd/integration/github/disconnect.js.map +1 -0
- package/dist/cmd/integration/github/index.d.ts +2 -0
- package/dist/cmd/integration/github/index.d.ts.map +1 -0
- package/dist/cmd/integration/github/index.js +21 -0
- package/dist/cmd/integration/github/index.js.map +1 -0
- package/dist/cmd/integration/index.d.ts +2 -0
- package/dist/cmd/integration/index.d.ts.map +1 -0
- package/dist/cmd/integration/index.js +16 -0
- package/dist/cmd/integration/index.js.map +1 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +24 -0
- package/dist/config.js.map +1 -1
- package/dist/errors.d.ts +2 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +5 -0
- package/dist/errors.js.map +1 -1
- package/dist/types.d.ts +2 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -1
- package/package.json +6 -6
- package/src/cli.ts +20 -4
- package/src/cmd/build/vite/agent-discovery.ts +4 -4
- package/src/cmd/build/vite/index.ts +1 -1
- package/src/cmd/build/vite/metadata-generator.ts +1 -1
- package/src/cmd/build/vite/registry-generator.ts +78 -24
- package/src/cmd/build/vite/route-discovery.ts +20 -0
- package/src/cmd/build/vite/vite-builder.ts +1 -1
- package/src/cmd/cloud/deploy.ts +78 -1
- package/src/cmd/cloud/sandbox/create.ts +22 -0
- package/src/cmd/cloud/sandbox/delete.ts +2 -6
- package/src/cmd/cloud/sandbox/download.ts +96 -0
- package/src/cmd/cloud/sandbox/env.ts +104 -0
- package/src/cmd/cloud/sandbox/get.ts +5 -0
- package/src/cmd/cloud/sandbox/index.ts +14 -0
- package/src/cmd/cloud/sandbox/ls.ts +126 -0
- package/src/cmd/cloud/sandbox/mkdir.ts +65 -0
- package/src/cmd/cloud/sandbox/rm.ts +51 -0
- package/src/cmd/cloud/sandbox/rmdir.ts +65 -0
- package/src/cmd/cloud/sandbox/snapshot/create.ts +0 -2
- package/src/cmd/cloud/sandbox/snapshot/get.ts +0 -2
- package/src/cmd/cloud/sandbox/snapshot/list.ts +0 -3
- package/src/cmd/cloud/sandbox/upload.ts +83 -0
- package/src/cmd/dev/index.ts +32 -19
- package/src/cmd/dev/sync.ts +26 -30
- package/src/cmd/git/account/add.ts +317 -0
- package/src/cmd/git/account/index.ts +12 -0
- package/src/cmd/git/account/list.ts +139 -0
- package/src/cmd/git/account/remove.ts +212 -0
- package/src/cmd/git/index.ts +20 -0
- package/src/cmd/git/link.ts +468 -0
- package/src/cmd/git/list.ts +161 -0
- package/src/cmd/git/status.ts +144 -0
- package/src/cmd/git/unlink.ts +117 -0
- package/src/cmd/index.ts +2 -0
- package/src/cmd/integration/api.ts +379 -0
- package/src/cmd/integration/github/connect.ts +242 -0
- package/src/cmd/integration/github/disconnect.ts +149 -0
- package/src/cmd/integration/github/index.ts +21 -0
- package/src/cmd/integration/index.ts +16 -0
- package/src/config.ts +34 -0
- package/src/errors.ts +7 -0
- package/src/types.ts +4 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { createSubcommand } from '../../../types';
|
|
2
|
+
import * as tui from '../../../tui';
|
|
3
|
+
import { getCommand } from '../../../command-prefix';
|
|
4
|
+
import { ErrorCode } from '../../../errors';
|
|
5
|
+
import { listOrganizations } from '@agentuity/server';
|
|
6
|
+
import enquirer from 'enquirer';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import {
|
|
9
|
+
getGithubIntegrationStatus,
|
|
10
|
+
disconnectGithubIntegration,
|
|
11
|
+
type GithubIntegration,
|
|
12
|
+
} from '../../integration/api';
|
|
13
|
+
|
|
14
|
+
const RemoveOptionsSchema = z.object({
|
|
15
|
+
org: z.string().optional().describe('Organization ID'),
|
|
16
|
+
account: z.string().optional().describe('GitHub integration ID to remove'),
|
|
17
|
+
confirm: z.boolean().optional().describe('Skip confirmation prompt'),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const RemoveResponseSchema = z.object({
|
|
21
|
+
removed: z.boolean().describe('Whether the account was removed'),
|
|
22
|
+
orgId: z.string().optional().describe('Organization ID'),
|
|
23
|
+
integrationId: z.string().optional().describe('Integration ID that was removed'),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export const removeSubcommand = createSubcommand({
|
|
27
|
+
name: 'remove',
|
|
28
|
+
description: 'Remove a GitHub account from your organization',
|
|
29
|
+
tags: ['mutating', 'destructive', 'slow'],
|
|
30
|
+
idempotent: false,
|
|
31
|
+
requires: { auth: true, apiClient: true },
|
|
32
|
+
schema: {
|
|
33
|
+
options: RemoveOptionsSchema,
|
|
34
|
+
response: RemoveResponseSchema,
|
|
35
|
+
},
|
|
36
|
+
examples: [
|
|
37
|
+
{
|
|
38
|
+
command: getCommand('git account remove'),
|
|
39
|
+
description: 'Remove a GitHub account from your organization',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
command: getCommand('git account remove --org org_abc --account int_xyz --confirm'),
|
|
43
|
+
description: 'Remove a specific account without prompts',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
command: getCommand('--json git account remove --org org_abc --account int_xyz --confirm'),
|
|
47
|
+
description: 'Remove and return JSON result',
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
|
|
51
|
+
async handler(ctx) {
|
|
52
|
+
const { logger, apiClient, opts, options } = ctx;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
// If both org and account provided, skip interactive flow
|
|
56
|
+
if (opts.org && opts.account) {
|
|
57
|
+
if (!opts.confirm) {
|
|
58
|
+
const confirmed = await tui.confirm(
|
|
59
|
+
`Are you sure you want to remove this GitHub account?`
|
|
60
|
+
);
|
|
61
|
+
if (!confirmed) {
|
|
62
|
+
tui.info('Cancelled');
|
|
63
|
+
return { removed: false };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
await tui.spinner({
|
|
68
|
+
message: 'Removing GitHub account...',
|
|
69
|
+
clearOnSuccess: true,
|
|
70
|
+
callback: () => disconnectGithubIntegration(apiClient, opts.org!, opts.account!),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (!options.json) {
|
|
74
|
+
tui.newline();
|
|
75
|
+
tui.success('Removed GitHub account');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { removed: true, orgId: opts.org, integrationId: opts.account };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Fetch organizations
|
|
82
|
+
const orgs = await tui.spinner({
|
|
83
|
+
message: 'Fetching organizations...',
|
|
84
|
+
clearOnSuccess: true,
|
|
85
|
+
callback: () => listOrganizations(apiClient),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (orgs.length === 0) {
|
|
89
|
+
tui.fatal('No organizations found for your account');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check GitHub status for each org
|
|
93
|
+
const orgStatuses = await tui.spinner({
|
|
94
|
+
message: 'Checking GitHub integration status...',
|
|
95
|
+
clearOnSuccess: true,
|
|
96
|
+
callback: async () => {
|
|
97
|
+
const statuses = await Promise.all(
|
|
98
|
+
orgs.map(async (org) => {
|
|
99
|
+
const status = await getGithubIntegrationStatus(apiClient, org.id);
|
|
100
|
+
return {
|
|
101
|
+
...org,
|
|
102
|
+
connected: status.connected,
|
|
103
|
+
integrations: status.integrations,
|
|
104
|
+
};
|
|
105
|
+
})
|
|
106
|
+
);
|
|
107
|
+
return statuses;
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Flatten all integrations across orgs
|
|
112
|
+
const allIntegrations: Array<{
|
|
113
|
+
orgId: string;
|
|
114
|
+
orgName: string;
|
|
115
|
+
integration: GithubIntegration;
|
|
116
|
+
}> = [];
|
|
117
|
+
|
|
118
|
+
for (const org of orgStatuses) {
|
|
119
|
+
for (const integration of org.integrations) {
|
|
120
|
+
allIntegrations.push({
|
|
121
|
+
orgId: org.id,
|
|
122
|
+
orgName: org.name,
|
|
123
|
+
integration,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (allIntegrations.length === 0) {
|
|
129
|
+
if (!options.json) {
|
|
130
|
+
tui.newline();
|
|
131
|
+
tui.info('No GitHub accounts are connected.');
|
|
132
|
+
}
|
|
133
|
+
return { removed: false };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Build choices showing GitHub account and org
|
|
137
|
+
const choices = allIntegrations.map((item) => ({
|
|
138
|
+
name: `${tui.bold(item.integration.githubAccountName)} ${tui.muted(`(${item.integration.githubAccountType})`)} → ${tui.bold(item.orgName)}`,
|
|
139
|
+
value: `${item.orgId}:${item.integration.id}`,
|
|
140
|
+
}));
|
|
141
|
+
|
|
142
|
+
// Show picker
|
|
143
|
+
const response = await enquirer.prompt<{ selection: string }>({
|
|
144
|
+
type: 'select',
|
|
145
|
+
name: 'selection',
|
|
146
|
+
message: 'Select a GitHub account to remove',
|
|
147
|
+
choices,
|
|
148
|
+
result(name: string) {
|
|
149
|
+
// Return the value (IDs) instead of the display name
|
|
150
|
+
const choice = choices.find((c) => c.name === name);
|
|
151
|
+
return choice?.value ?? name;
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const colonIdx = response.selection.indexOf(':');
|
|
156
|
+
if (colonIdx === -1) {
|
|
157
|
+
logger.fatal('Invalid selection format');
|
|
158
|
+
}
|
|
159
|
+
const orgId = response.selection.slice(0, colonIdx);
|
|
160
|
+
const integrationId = response.selection.slice(colonIdx + 1);
|
|
161
|
+
const selected = allIntegrations.find(
|
|
162
|
+
(i) => i.orgId === orgId && i.integration.id === integrationId
|
|
163
|
+
);
|
|
164
|
+
const displayName = selected
|
|
165
|
+
? `${tui.bold(selected.integration.githubAccountName)} from ${tui.bold(selected.orgName)}`
|
|
166
|
+
: response.selection;
|
|
167
|
+
|
|
168
|
+
// Confirm
|
|
169
|
+
if (!opts.confirm) {
|
|
170
|
+
const confirmed = await tui.confirm(`Are you sure you want to remove ${displayName}?`);
|
|
171
|
+
|
|
172
|
+
if (!confirmed) {
|
|
173
|
+
tui.newline();
|
|
174
|
+
tui.info('Cancelled');
|
|
175
|
+
return { removed: false };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
await tui.spinner({
|
|
180
|
+
message: 'Removing GitHub account...',
|
|
181
|
+
clearOnSuccess: true,
|
|
182
|
+
callback: () => disconnectGithubIntegration(apiClient, orgId, integrationId),
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
if (!options.json) {
|
|
186
|
+
tui.newline();
|
|
187
|
+
tui.success(`Removed ${displayName}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return { removed: true, orgId, integrationId };
|
|
191
|
+
} catch (error) {
|
|
192
|
+
// Handle user cancellation (Ctrl+C)
|
|
193
|
+
const isCancel =
|
|
194
|
+
error === '' ||
|
|
195
|
+
(error instanceof Error &&
|
|
196
|
+
(error.message === '' || error.message === 'User cancelled'));
|
|
197
|
+
|
|
198
|
+
if (isCancel) {
|
|
199
|
+
tui.newline();
|
|
200
|
+
tui.info('Cancelled');
|
|
201
|
+
return { removed: false };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
logger.trace(error);
|
|
205
|
+
return logger.fatal(
|
|
206
|
+
'Failed to remove GitHub account: %s',
|
|
207
|
+
error,
|
|
208
|
+
ErrorCode.INTEGRATION_FAILED
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createCommand } from '../../types';
|
|
2
|
+
import { accountCommand } from './account';
|
|
3
|
+
import { linkSubcommand } from './link';
|
|
4
|
+
import { listSubcommand } from './list';
|
|
5
|
+
import { unlinkSubcommand } from './unlink';
|
|
6
|
+
import { statusSubcommand } from './status';
|
|
7
|
+
|
|
8
|
+
export const gitCommand = createCommand({
|
|
9
|
+
name: 'git',
|
|
10
|
+
description: 'Manage GitHub integration and repository connections',
|
|
11
|
+
subcommands: [
|
|
12
|
+
accountCommand,
|
|
13
|
+
linkSubcommand,
|
|
14
|
+
listSubcommand,
|
|
15
|
+
unlinkSubcommand,
|
|
16
|
+
statusSubcommand,
|
|
17
|
+
],
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export default gitCommand;
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
import { createSubcommand, type Config } from '../../types';
|
|
2
|
+
import * as tui from '../../tui';
|
|
3
|
+
import { getCommand } from '../../command-prefix';
|
|
4
|
+
import { ErrorCode } from '../../errors';
|
|
5
|
+
import enquirer from 'enquirer';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import {
|
|
8
|
+
getGithubIntegrationStatus,
|
|
9
|
+
listGithubRepos,
|
|
10
|
+
linkProjectToRepo,
|
|
11
|
+
getProjectGithubStatus,
|
|
12
|
+
type GithubRepo,
|
|
13
|
+
} from '../integration/api';
|
|
14
|
+
import type { APIClient } from '../../api';
|
|
15
|
+
import type { Logger } from '@agentuity/core';
|
|
16
|
+
import { runGitAccountConnect } from './account/add';
|
|
17
|
+
|
|
18
|
+
export interface DetectedGitInfo {
|
|
19
|
+
repo: string | null;
|
|
20
|
+
branch: string | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function detectGitInfo(): DetectedGitInfo {
|
|
24
|
+
let repo: string | null = null;
|
|
25
|
+
let branch: string | null = null;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
// Detect repo from origin remote
|
|
29
|
+
const remoteResult = Bun.spawnSync(['git', 'remote', 'get-url', 'origin'], {
|
|
30
|
+
stdout: 'pipe',
|
|
31
|
+
stderr: 'pipe',
|
|
32
|
+
});
|
|
33
|
+
if (remoteResult.exitCode === 0) {
|
|
34
|
+
const url = remoteResult.stdout.toString().trim();
|
|
35
|
+
// Parse GitHub URL formats:
|
|
36
|
+
// https://github.com/owner/repo.git
|
|
37
|
+
// git@github.com:owner/repo.git
|
|
38
|
+
const httpsMatch = url.match(/github\.com\/([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
39
|
+
if (httpsMatch) repo = httpsMatch[1];
|
|
40
|
+
|
|
41
|
+
const sshMatch = url.match(/github\.com:([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
42
|
+
if (sshMatch) repo = sshMatch[1];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Detect current branch
|
|
46
|
+
const branchResult = Bun.spawnSync(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
47
|
+
stdout: 'pipe',
|
|
48
|
+
stderr: 'pipe',
|
|
49
|
+
});
|
|
50
|
+
if (branchResult.exitCode === 0) {
|
|
51
|
+
branch = branchResult.stdout.toString().trim();
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
// Ignore errors
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { repo, branch };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface RunGitLinkOptions {
|
|
61
|
+
apiClient: APIClient;
|
|
62
|
+
projectId: string;
|
|
63
|
+
orgId: string;
|
|
64
|
+
logger: Logger;
|
|
65
|
+
branchOption?: string;
|
|
66
|
+
rootOption?: string;
|
|
67
|
+
noAuto?: boolean;
|
|
68
|
+
noPreview?: boolean;
|
|
69
|
+
skipAlreadyLinkedCheck?: boolean;
|
|
70
|
+
config?: Config | null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface RunGitLinkResult {
|
|
74
|
+
linked: boolean;
|
|
75
|
+
repoFullName?: string;
|
|
76
|
+
branch?: string;
|
|
77
|
+
autoDeploy?: boolean;
|
|
78
|
+
cancelled?: boolean;
|
|
79
|
+
noGithubConnected?: boolean;
|
|
80
|
+
noReposFound?: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function runGitLink(options: RunGitLinkOptions): Promise<RunGitLinkResult> {
|
|
84
|
+
const {
|
|
85
|
+
apiClient,
|
|
86
|
+
projectId,
|
|
87
|
+
orgId,
|
|
88
|
+
logger,
|
|
89
|
+
branchOption,
|
|
90
|
+
rootOption,
|
|
91
|
+
noAuto = false,
|
|
92
|
+
noPreview = false,
|
|
93
|
+
skipAlreadyLinkedCheck = false,
|
|
94
|
+
config,
|
|
95
|
+
} = options;
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
if (!skipAlreadyLinkedCheck) {
|
|
99
|
+
const currentStatus = await tui.spinner({
|
|
100
|
+
message: 'Checking current status...',
|
|
101
|
+
clearOnSuccess: true,
|
|
102
|
+
callback: () => getProjectGithubStatus(apiClient, projectId),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (currentStatus.linked) {
|
|
106
|
+
tui.newline();
|
|
107
|
+
tui.warning(
|
|
108
|
+
`This project is already linked to ${tui.bold(currentStatus.repoFullName ?? 'a repository')}`
|
|
109
|
+
);
|
|
110
|
+
tui.newline();
|
|
111
|
+
|
|
112
|
+
const confirmed = await tui.confirm('Do you want to change the linked repository?');
|
|
113
|
+
if (!confirmed) {
|
|
114
|
+
tui.info('Cancelled');
|
|
115
|
+
return { linked: false, cancelled: true };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let githubStatus = await tui.spinner({
|
|
121
|
+
message: 'Checking GitHub connection...',
|
|
122
|
+
clearOnSuccess: true,
|
|
123
|
+
callback: () => getGithubIntegrationStatus(apiClient, orgId),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (!githubStatus.connected || githubStatus.integrations.length === 0) {
|
|
127
|
+
tui.newline();
|
|
128
|
+
tui.warning('No GitHub accounts connected to this organization.');
|
|
129
|
+
tui.newline();
|
|
130
|
+
|
|
131
|
+
const wantConnect = await tui.confirm('Would you like to connect a GitHub account now?');
|
|
132
|
+
if (!wantConnect) {
|
|
133
|
+
tui.info('Cancelled');
|
|
134
|
+
return { linked: false, cancelled: true };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const connectResult = await runGitAccountConnect({
|
|
138
|
+
apiClient,
|
|
139
|
+
orgId,
|
|
140
|
+
logger,
|
|
141
|
+
config,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (!connectResult.connected) {
|
|
145
|
+
if (connectResult.cancelled) {
|
|
146
|
+
return { linked: false, cancelled: true };
|
|
147
|
+
}
|
|
148
|
+
return { linked: false, noGithubConnected: true };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
githubStatus = await getGithubIntegrationStatus(apiClient, orgId);
|
|
152
|
+
|
|
153
|
+
if (!githubStatus.connected || githubStatus.integrations.length === 0) {
|
|
154
|
+
tui.error('GitHub connection failed. Please try again.');
|
|
155
|
+
return { linked: false, noGithubConnected: true };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
tui.newline();
|
|
159
|
+
tui.info('Now continuing with repository linking...');
|
|
160
|
+
tui.newline();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const gitInfo = detectGitInfo();
|
|
164
|
+
|
|
165
|
+
const allRepos = await tui.spinner({
|
|
166
|
+
message: 'Fetching available repositories...',
|
|
167
|
+
clearOnSuccess: true,
|
|
168
|
+
callback: () => listGithubRepos(apiClient, orgId),
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
if (allRepos.length === 0) {
|
|
172
|
+
tui.newline();
|
|
173
|
+
tui.error('No repositories found.');
|
|
174
|
+
tui.newline();
|
|
175
|
+
console.log('Make sure your GitHub App has access to the repositories you want to link.');
|
|
176
|
+
return { linked: false, noReposFound: true };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let selectedRepo: GithubRepo | undefined;
|
|
180
|
+
let confirmed = false;
|
|
181
|
+
|
|
182
|
+
if (gitInfo.repo) {
|
|
183
|
+
const detectedRepo = allRepos.find(
|
|
184
|
+
(r) => r.fullName.toLowerCase() === gitInfo.repo!.toLowerCase()
|
|
185
|
+
);
|
|
186
|
+
if (detectedRepo) {
|
|
187
|
+
tui.newline();
|
|
188
|
+
tui.info(`Detected repository: ${tui.bold(detectedRepo.fullName)}`);
|
|
189
|
+
tui.newline();
|
|
190
|
+
|
|
191
|
+
const useDetected = await tui.confirm('Use this repository?');
|
|
192
|
+
if (useDetected) {
|
|
193
|
+
selectedRepo = detectedRepo;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!selectedRepo) {
|
|
199
|
+
let repos = allRepos;
|
|
200
|
+
|
|
201
|
+
if (githubStatus.integrations.length > 1) {
|
|
202
|
+
tui.newline();
|
|
203
|
+
|
|
204
|
+
const accountChoices = githubStatus.integrations.map((integration) => ({
|
|
205
|
+
name: integration.githubAccountName,
|
|
206
|
+
value: integration.id,
|
|
207
|
+
message: `${integration.githubAccountName} ${tui.muted(`(${integration.githubAccountType})`)}`,
|
|
208
|
+
}));
|
|
209
|
+
|
|
210
|
+
const accountResponse = await enquirer.prompt<{ integrationId: string }>({
|
|
211
|
+
type: 'select',
|
|
212
|
+
name: 'integrationId',
|
|
213
|
+
message: 'Select a GitHub account',
|
|
214
|
+
choices: accountChoices,
|
|
215
|
+
result(name: string) {
|
|
216
|
+
// Return the value (id) instead of the name
|
|
217
|
+
const choice = accountChoices.find((c) => c.name === name);
|
|
218
|
+
return choice?.value ?? name;
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
repos = await tui.spinner({
|
|
223
|
+
message: 'Fetching repositories...',
|
|
224
|
+
clearOnSuccess: true,
|
|
225
|
+
callback: () => listGithubRepos(apiClient, orgId, accountResponse.integrationId),
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
if (repos.length === 0) {
|
|
229
|
+
tui.newline();
|
|
230
|
+
tui.error('No repositories found for this account.');
|
|
231
|
+
return { linked: false, noReposFound: true };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const repoChoices = repos.map((repo) => ({
|
|
236
|
+
name: repo.fullName,
|
|
237
|
+
message: `${repo.fullName} ${repo.private ? tui.muted('(private)') : ''} ${tui.muted(`[${repo.defaultBranch}]`)}`,
|
|
238
|
+
}));
|
|
239
|
+
|
|
240
|
+
tui.newline();
|
|
241
|
+
|
|
242
|
+
const repoResponse = await enquirer.prompt<{ repoFullName: string }>({
|
|
243
|
+
type: 'autocomplete',
|
|
244
|
+
name: 'repoFullName',
|
|
245
|
+
message: 'Select a repository',
|
|
246
|
+
choices: repoChoices,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
selectedRepo = repos.find((r) => r.fullName === repoResponse.repoFullName);
|
|
250
|
+
if (!selectedRepo) {
|
|
251
|
+
tui.error('Repository not found');
|
|
252
|
+
return { linked: false };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Prompt for settings with defaults
|
|
257
|
+
const defaultBranch = branchOption ?? gitInfo.branch ?? selectedRepo.defaultBranch;
|
|
258
|
+
const defaultRoot = rootOption ?? '.';
|
|
259
|
+
|
|
260
|
+
tui.newline();
|
|
261
|
+
|
|
262
|
+
const { directory } = await enquirer.prompt<{ directory: string }>({
|
|
263
|
+
type: 'input',
|
|
264
|
+
name: 'directory',
|
|
265
|
+
message: 'Root directory',
|
|
266
|
+
initial: defaultRoot,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const { branch } = await enquirer.prompt<{ branch: string }>({
|
|
270
|
+
type: 'input',
|
|
271
|
+
name: 'branch',
|
|
272
|
+
message: 'Branch to deploy from',
|
|
273
|
+
initial: defaultBranch,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const finalAutoDeploy = await tui.confirm('Enable automatic deployments on push?', !noAuto);
|
|
277
|
+
const finalPreviewDeploy = await tui.confirm(
|
|
278
|
+
'Enable preview deployments on PRs?',
|
|
279
|
+
!noPreview
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
tui.newline();
|
|
283
|
+
console.log(tui.bold('Link Settings:'));
|
|
284
|
+
console.log(` Repository: ${selectedRepo.fullName}`);
|
|
285
|
+
console.log(` Branch: ${branch}`);
|
|
286
|
+
console.log(` Directory: ${directory}`);
|
|
287
|
+
console.log(
|
|
288
|
+
` Auto-deploy: ${finalAutoDeploy ? tui.colorSuccess('enabled') : tui.muted('disabled')}`
|
|
289
|
+
);
|
|
290
|
+
console.log(
|
|
291
|
+
` Preview deploys: ${finalPreviewDeploy ? tui.colorSuccess('enabled') : tui.muted('disabled')}`
|
|
292
|
+
);
|
|
293
|
+
tui.newline();
|
|
294
|
+
|
|
295
|
+
confirmed = await tui.confirm('Link this repository?');
|
|
296
|
+
if (!confirmed) {
|
|
297
|
+
tui.info('Cancelled');
|
|
298
|
+
return { linked: false, cancelled: true };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
await tui.spinner({
|
|
302
|
+
message: 'Linking repository...',
|
|
303
|
+
clearOnSuccess: true,
|
|
304
|
+
callback: () =>
|
|
305
|
+
linkProjectToRepo(apiClient, {
|
|
306
|
+
projectId,
|
|
307
|
+
repoFullName: selectedRepo.fullName,
|
|
308
|
+
branch,
|
|
309
|
+
autoDeploy: finalAutoDeploy,
|
|
310
|
+
previewDeploy: finalPreviewDeploy,
|
|
311
|
+
directory: directory === '.' ? undefined : directory,
|
|
312
|
+
integrationId: selectedRepo.integrationId,
|
|
313
|
+
}),
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
tui.newline();
|
|
317
|
+
tui.success(`Linked project to ${tui.bold(selectedRepo.fullName)}`);
|
|
318
|
+
tui.newline();
|
|
319
|
+
|
|
320
|
+
if (finalAutoDeploy) {
|
|
321
|
+
console.log(`Pushes to ${tui.bold(branch)} will trigger automatic deployments.`);
|
|
322
|
+
}
|
|
323
|
+
if (finalPreviewDeploy) {
|
|
324
|
+
console.log('Pull requests will create preview deployments.');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
linked: true,
|
|
329
|
+
repoFullName: selectedRepo.fullName,
|
|
330
|
+
branch,
|
|
331
|
+
autoDeploy: finalAutoDeploy,
|
|
332
|
+
};
|
|
333
|
+
} catch (error) {
|
|
334
|
+
const isCancel =
|
|
335
|
+
error === '' ||
|
|
336
|
+
(error instanceof Error && (error.message === '' || error.message === 'User cancelled'));
|
|
337
|
+
|
|
338
|
+
if (isCancel) {
|
|
339
|
+
tui.newline();
|
|
340
|
+
tui.info('Cancelled');
|
|
341
|
+
return { linked: false, cancelled: true };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
logger.trace(error);
|
|
345
|
+
throw error;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const LinkOptionsSchema = z.object({
|
|
350
|
+
repo: z.string().optional().describe('Repository full name (owner/repo) to link'),
|
|
351
|
+
deploy: z.boolean().optional().describe('Enable automatic deployments on push (default: true)'),
|
|
352
|
+
preview: z
|
|
353
|
+
.boolean()
|
|
354
|
+
.optional()
|
|
355
|
+
.describe('Enable preview deployments on pull requests (default: true)'),
|
|
356
|
+
branch: z.string().optional().describe('Branch to deploy from (default: repo default branch)'),
|
|
357
|
+
root: z.string().optional().describe('Root directory containing agentuity.json (default: .)'),
|
|
358
|
+
confirm: z.boolean().optional().describe('Skip confirmation prompts'),
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const LinkResponseSchema = z.object({
|
|
362
|
+
linked: z.boolean().describe('Whether the project was linked'),
|
|
363
|
+
repoFullName: z.string().optional().describe('Repository that was linked'),
|
|
364
|
+
branch: z.string().optional().describe('Branch configured'),
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
export const linkSubcommand = createSubcommand({
|
|
368
|
+
name: 'link',
|
|
369
|
+
description: 'Link a project to a GitHub repository',
|
|
370
|
+
tags: ['mutating', 'creates-resource'],
|
|
371
|
+
idempotent: false,
|
|
372
|
+
requires: { auth: true, apiClient: true, project: true },
|
|
373
|
+
schema: {
|
|
374
|
+
options: LinkOptionsSchema,
|
|
375
|
+
response: LinkResponseSchema,
|
|
376
|
+
},
|
|
377
|
+
examples: [
|
|
378
|
+
{
|
|
379
|
+
command: getCommand('git link'),
|
|
380
|
+
description: 'Link current project to a GitHub repository',
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
command: getCommand('git link --repo owner/repo --branch main --confirm'),
|
|
384
|
+
description: 'Link to a specific repo non-interactively',
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
command: getCommand('git link --root .'),
|
|
388
|
+
description: 'Link from the current directory',
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
command: getCommand('git link --branch main'),
|
|
392
|
+
description: 'Link to a specific branch',
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
command: getCommand('git link --preview true'),
|
|
396
|
+
description: 'Enable preview deployments on PRs',
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
command: getCommand('git link --deploy false'),
|
|
400
|
+
description: 'Disable automatic deployments on push',
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
command: getCommand('git link --root packages/my-agent'),
|
|
404
|
+
description: 'Link a subdirectory in a monorepo',
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
command: getCommand('--json git link --repo owner/repo --branch main --confirm'),
|
|
408
|
+
description: 'Link and return JSON result',
|
|
409
|
+
},
|
|
410
|
+
],
|
|
411
|
+
|
|
412
|
+
async handler(ctx) {
|
|
413
|
+
const { apiClient, project, opts, config, logger, options } = ctx;
|
|
414
|
+
|
|
415
|
+
try {
|
|
416
|
+
// Non-interactive mode when repo is provided
|
|
417
|
+
// Note: integrationId is not passed in non-interactive mode. The API will
|
|
418
|
+
// attempt to find a matching integration based on the repo owner. This may
|
|
419
|
+
// fail if the org has multiple GitHub integrations with access to the same repo.
|
|
420
|
+
if (opts.repo && opts.confirm) {
|
|
421
|
+
const branch = opts.branch ?? 'main';
|
|
422
|
+
const directory = opts.root === '.' ? undefined : opts.root;
|
|
423
|
+
|
|
424
|
+
await tui.spinner({
|
|
425
|
+
message: 'Linking repository...',
|
|
426
|
+
clearOnSuccess: true,
|
|
427
|
+
callback: () =>
|
|
428
|
+
linkProjectToRepo(apiClient, {
|
|
429
|
+
projectId: project.projectId,
|
|
430
|
+
repoFullName: opts.repo!,
|
|
431
|
+
branch,
|
|
432
|
+
autoDeploy: opts.deploy !== false,
|
|
433
|
+
previewDeploy: opts.preview !== false,
|
|
434
|
+
directory,
|
|
435
|
+
}),
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
if (!options.json) {
|
|
439
|
+
tui.newline();
|
|
440
|
+
tui.success(`Linked project to ${tui.bold(opts.repo)}`);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return { linked: true, repoFullName: opts.repo, branch };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const result = await runGitLink({
|
|
447
|
+
apiClient,
|
|
448
|
+
projectId: project.projectId,
|
|
449
|
+
orgId: project.orgId,
|
|
450
|
+
logger,
|
|
451
|
+
branchOption: opts.branch,
|
|
452
|
+
rootOption: opts.root,
|
|
453
|
+
noAuto: opts.deploy === false,
|
|
454
|
+
noPreview: opts.preview === false,
|
|
455
|
+
config,
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
linked: result.linked,
|
|
460
|
+
repoFullName: result.repoFullName,
|
|
461
|
+
branch: result.branch,
|
|
462
|
+
};
|
|
463
|
+
} catch (error) {
|
|
464
|
+
logger.trace(error);
|
|
465
|
+
return logger.fatal('Failed to link repository: %s', error, ErrorCode.INTEGRATION_FAILED);
|
|
466
|
+
}
|
|
467
|
+
},
|
|
468
|
+
});
|