@agentuity/cli 0.0.42 → 0.0.44
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/AGENTS.md +1 -1
- package/README.md +1 -1
- package/bin/cli.ts +7 -5
- package/dist/api.d.ts +3 -3
- package/dist/api.d.ts.map +1 -1
- package/dist/auth.d.ts +10 -2
- package/dist/auth.d.ts.map +1 -1
- package/dist/banner.d.ts.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cmd/auth/api.d.ts +4 -4
- package/dist/cmd/auth/api.d.ts.map +1 -1
- package/dist/cmd/auth/index.d.ts.map +1 -1
- package/dist/cmd/auth/login.d.ts.map +1 -1
- package/dist/cmd/auth/signup.d.ts.map +1 -1
- package/dist/cmd/auth/ssh/add.d.ts +2 -0
- package/dist/cmd/auth/ssh/add.d.ts.map +1 -0
- package/dist/cmd/auth/ssh/api.d.ts +16 -0
- package/dist/cmd/auth/ssh/api.d.ts.map +1 -0
- package/dist/cmd/auth/ssh/delete.d.ts +2 -0
- package/dist/cmd/auth/ssh/delete.d.ts.map +1 -0
- package/dist/cmd/auth/ssh/index.d.ts +3 -0
- package/dist/cmd/auth/ssh/index.d.ts.map +1 -0
- package/dist/cmd/auth/ssh/list.d.ts +2 -0
- package/dist/cmd/auth/ssh/list.d.ts.map +1 -0
- package/dist/cmd/auth/whoami.d.ts +2 -0
- package/dist/cmd/auth/whoami.d.ts.map +1 -0
- package/dist/cmd/bundle/ast.d.ts +14 -3
- package/dist/cmd/bundle/ast.d.ts.map +1 -1
- package/dist/cmd/bundle/ast.test.d.ts +2 -0
- package/dist/cmd/bundle/ast.test.d.ts.map +1 -0
- package/dist/cmd/bundle/bundler.d.ts +6 -1
- package/dist/cmd/bundle/bundler.d.ts.map +1 -1
- package/dist/cmd/bundle/file.d.ts.map +1 -1
- package/dist/cmd/bundle/fix-duplicate-exports.d.ts +2 -0
- package/dist/cmd/bundle/fix-duplicate-exports.d.ts.map +1 -0
- package/dist/cmd/bundle/fix-duplicate-exports.test.d.ts +2 -0
- package/dist/cmd/bundle/fix-duplicate-exports.test.d.ts.map +1 -0
- package/dist/cmd/bundle/index.d.ts +1 -1
- package/dist/cmd/bundle/index.d.ts.map +1 -1
- package/dist/cmd/bundle/plugin.d.ts +2 -0
- package/dist/cmd/bundle/plugin.d.ts.map +1 -1
- package/dist/cmd/cloud/deploy.d.ts.map +1 -0
- package/dist/cmd/cloud/domain.d.ts +17 -0
- package/dist/cmd/cloud/domain.d.ts.map +1 -0
- package/dist/cmd/cloud/index.d.ts.map +1 -0
- package/dist/cmd/cloud/resource/add.d.ts +2 -0
- package/dist/cmd/cloud/resource/add.d.ts.map +1 -0
- package/dist/cmd/cloud/resource/delete.d.ts +2 -0
- package/dist/cmd/cloud/resource/delete.d.ts.map +1 -0
- package/dist/cmd/cloud/resource/index.d.ts +3 -0
- package/dist/cmd/cloud/resource/index.d.ts.map +1 -0
- package/dist/cmd/cloud/resource/list.d.ts +2 -0
- package/dist/cmd/cloud/resource/list.d.ts.map +1 -0
- package/dist/cmd/cloud/scp/download.d.ts +2 -0
- package/dist/cmd/cloud/scp/download.d.ts.map +1 -0
- package/dist/cmd/cloud/scp/index.d.ts +3 -0
- package/dist/cmd/cloud/scp/index.d.ts.map +1 -0
- package/dist/cmd/cloud/scp/upload.d.ts +2 -0
- package/dist/cmd/cloud/scp/upload.d.ts.map +1 -0
- package/dist/cmd/cloud/ssh.d.ts +2 -0
- package/dist/cmd/cloud/ssh.d.ts.map +1 -0
- package/dist/cmd/dev/api.d.ts +18 -0
- package/dist/cmd/dev/api.d.ts.map +1 -0
- package/dist/cmd/dev/download.d.ts +11 -0
- package/dist/cmd/dev/download.d.ts.map +1 -0
- package/dist/cmd/dev/index.d.ts.map +1 -1
- package/dist/cmd/dev/templates.d.ts +3 -0
- package/dist/cmd/dev/templates.d.ts.map +1 -0
- package/dist/cmd/env/delete.d.ts +2 -0
- package/dist/cmd/env/delete.d.ts.map +1 -0
- package/dist/cmd/env/get.d.ts +2 -0
- package/dist/cmd/env/get.d.ts.map +1 -0
- package/dist/cmd/env/import.d.ts +2 -0
- package/dist/cmd/env/import.d.ts.map +1 -0
- package/dist/cmd/env/index.d.ts +2 -0
- package/dist/cmd/env/index.d.ts.map +1 -0
- package/dist/cmd/env/list.d.ts.map +1 -0
- package/dist/cmd/env/pull.d.ts +2 -0
- package/dist/cmd/env/pull.d.ts.map +1 -0
- package/dist/cmd/env/push.d.ts +2 -0
- package/dist/cmd/env/push.d.ts.map +1 -0
- package/dist/cmd/env/set.d.ts +2 -0
- package/dist/cmd/env/set.d.ts.map +1 -0
- package/dist/cmd/profile/show.d.ts.map +1 -1
- package/dist/cmd/project/create.d.ts.map +1 -1
- package/dist/cmd/project/delete.d.ts.map +1 -1
- package/dist/cmd/project/download.d.ts +1 -1
- package/dist/cmd/project/download.d.ts.map +1 -1
- package/dist/cmd/project/list.d.ts.map +1 -1
- package/dist/cmd/project/show.d.ts.map +1 -1
- package/dist/cmd/project/template-flow.d.ts +5 -1
- package/dist/cmd/project/template-flow.d.ts.map +1 -1
- package/dist/cmd/secret/delete.d.ts +2 -0
- package/dist/cmd/secret/delete.d.ts.map +1 -0
- package/dist/cmd/secret/get.d.ts +2 -0
- package/dist/cmd/secret/get.d.ts.map +1 -0
- package/dist/cmd/secret/import.d.ts +2 -0
- package/dist/cmd/secret/import.d.ts.map +1 -0
- package/dist/cmd/secret/index.d.ts +2 -0
- package/dist/cmd/secret/index.d.ts.map +1 -0
- package/dist/cmd/secret/list.d.ts +2 -0
- package/dist/cmd/secret/list.d.ts.map +1 -0
- package/dist/cmd/secret/pull.d.ts +2 -0
- package/dist/cmd/secret/pull.d.ts.map +1 -0
- package/dist/cmd/secret/push.d.ts +2 -0
- package/dist/cmd/secret/push.d.ts.map +1 -0
- package/dist/cmd/secret/set.d.ts +2 -0
- package/dist/cmd/secret/set.d.ts.map +1 -0
- package/dist/cmd/version/index.d.ts.map +1 -1
- package/dist/config.d.ts +11 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/crypto/box.d.ts +65 -0
- package/dist/crypto/box.d.ts.map +1 -0
- package/dist/crypto/box.test.d.ts +2 -0
- package/dist/crypto/box.test.d.ts.map +1 -0
- package/dist/download.d.ts.map +1 -1
- package/dist/env-util.d.ts +67 -0
- package/dist/env-util.d.ts.map +1 -0
- package/dist/env-util.test.d.ts +2 -0
- package/dist/env-util.test.d.ts.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/schema-parser.d.ts.map +1 -1
- package/dist/steps.d.ts +4 -1
- package/dist/steps.d.ts.map +1 -1
- package/dist/terminal.d.ts.map +1 -1
- package/dist/tui.d.ts +32 -2
- package/dist/tui.d.ts.map +1 -1
- package/dist/types.d.ts +250 -127
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/detectSubagent.d.ts +15 -0
- package/dist/utils/detectSubagent.d.ts.map +1 -0
- package/dist/utils/zip.d.ts +7 -0
- package/dist/utils/zip.d.ts.map +1 -0
- package/package.json +11 -3
- package/src/api-errors.md +2 -2
- package/src/api.ts +12 -7
- package/src/auth.ts +116 -7
- package/src/banner.ts +13 -6
- package/src/cli.ts +709 -36
- package/src/cmd/auth/api.ts +10 -16
- package/src/cmd/auth/index.ts +3 -1
- package/src/cmd/auth/login.ts +24 -8
- package/src/cmd/auth/signup.ts +15 -11
- package/src/cmd/auth/ssh/add.ts +263 -0
- package/src/cmd/auth/ssh/api.ts +94 -0
- package/src/cmd/auth/ssh/delete.ts +102 -0
- package/src/cmd/auth/ssh/index.ts +10 -0
- package/src/cmd/auth/ssh/list.ts +74 -0
- package/src/cmd/auth/whoami.ts +69 -0
- package/src/cmd/bundle/ast.test.ts +565 -0
- package/src/cmd/bundle/ast.ts +457 -44
- package/src/cmd/bundle/bundler.ts +255 -57
- package/src/cmd/bundle/file.ts +6 -12
- package/src/cmd/bundle/fix-duplicate-exports.test.ts +387 -0
- package/src/cmd/bundle/fix-duplicate-exports.ts +204 -0
- package/src/cmd/bundle/index.ts +11 -11
- package/src/cmd/bundle/patch/aisdk.ts +1 -1
- package/src/cmd/bundle/plugin.ts +373 -53
- package/src/cmd/cloud/deploy.ts +336 -0
- package/src/cmd/cloud/domain.ts +92 -0
- package/src/cmd/cloud/index.ts +11 -0
- package/src/cmd/cloud/resource/add.ts +56 -0
- package/src/cmd/cloud/resource/delete.ts +120 -0
- package/src/cmd/cloud/resource/index.ts +11 -0
- package/src/cmd/cloud/resource/list.ts +69 -0
- package/src/cmd/cloud/scp/download.ts +59 -0
- package/src/cmd/cloud/scp/index.ts +9 -0
- package/src/cmd/cloud/scp/upload.ts +62 -0
- package/src/cmd/cloud/ssh.ts +68 -0
- package/src/cmd/dev/api.ts +46 -0
- package/src/cmd/dev/download.ts +111 -0
- package/src/cmd/dev/index.ts +362 -34
- package/src/cmd/dev/templates.ts +84 -0
- package/src/cmd/env/delete.ts +47 -0
- package/src/cmd/env/get.ts +53 -0
- package/src/cmd/env/import.ts +102 -0
- package/src/cmd/env/index.ts +22 -0
- package/src/cmd/env/list.ts +56 -0
- package/src/cmd/env/pull.ts +80 -0
- package/src/cmd/env/push.ts +37 -0
- package/src/cmd/env/set.ts +71 -0
- package/src/cmd/index.ts +2 -2
- package/src/cmd/profile/show.ts +15 -6
- package/src/cmd/project/create.ts +7 -2
- package/src/cmd/project/delete.ts +75 -18
- package/src/cmd/project/download.ts +3 -3
- package/src/cmd/project/list.ts +8 -8
- package/src/cmd/project/show.ts +3 -7
- package/src/cmd/project/template-flow.ts +186 -48
- package/src/cmd/secret/delete.ts +40 -0
- package/src/cmd/secret/get.ts +54 -0
- package/src/cmd/secret/import.ts +64 -0
- package/src/cmd/secret/index.ts +22 -0
- package/src/cmd/secret/list.ts +56 -0
- package/src/cmd/secret/pull.ts +78 -0
- package/src/cmd/secret/push.ts +37 -0
- package/src/cmd/secret/set.ts +45 -0
- package/src/cmd/version/index.ts +2 -1
- package/src/config.ts +257 -27
- package/src/crypto/box.test.ts +431 -0
- package/src/crypto/box.ts +477 -0
- package/src/download.ts +1 -0
- package/src/env-util.test.ts +194 -0
- package/src/env-util.ts +290 -0
- package/src/index.ts +5 -1
- package/src/schema-parser.ts +2 -3
- package/src/steps.ts +144 -10
- package/src/terminal.ts +24 -23
- package/src/tui.ts +208 -68
- package/src/types.ts +292 -202
- package/src/utils/detectSubagent.ts +31 -0
- package/src/utils/zip.ts +38 -0
- package/dist/cmd/example/create-user.d.ts +0 -2
- package/dist/cmd/example/create-user.d.ts.map +0 -1
- package/dist/cmd/example/create.d.ts +0 -2
- package/dist/cmd/example/create.d.ts.map +0 -1
- package/dist/cmd/example/deploy.d.ts.map +0 -1
- package/dist/cmd/example/index.d.ts.map +0 -1
- package/dist/cmd/example/list.d.ts.map +0 -1
- package/dist/cmd/example/optional-auth.d.ts +0 -3
- package/dist/cmd/example/optional-auth.d.ts.map +0 -1
- package/dist/cmd/example/run-command.d.ts +0 -2
- package/dist/cmd/example/run-command.d.ts.map +0 -1
- package/dist/cmd/example/sound.d.ts +0 -3
- package/dist/cmd/example/sound.d.ts.map +0 -1
- package/dist/cmd/example/spinner.d.ts +0 -2
- package/dist/cmd/example/spinner.d.ts.map +0 -1
- package/dist/cmd/example/steps.d.ts +0 -2
- package/dist/cmd/example/steps.d.ts.map +0 -1
- package/dist/cmd/example/version.d.ts +0 -2
- package/dist/cmd/example/version.d.ts.map +0 -1
- package/dist/logger.d.ts +0 -24
- package/dist/logger.d.ts.map +0 -1
- package/src/cmd/example/create-user.ts +0 -38
- package/src/cmd/example/create.ts +0 -31
- package/src/cmd/example/deploy.ts +0 -36
- package/src/cmd/example/index.ts +0 -29
- package/src/cmd/example/list.ts +0 -32
- package/src/cmd/example/optional-auth.ts +0 -38
- package/src/cmd/example/run-command.ts +0 -45
- package/src/cmd/example/sound.ts +0 -14
- package/src/cmd/example/spinner.ts +0 -44
- package/src/cmd/example/steps.ts +0 -66
- package/src/cmd/example/version.ts +0 -13
- package/src/logger.ts +0 -235
- /package/dist/cmd/{example → cloud}/deploy.d.ts +0 -0
- /package/dist/cmd/{example → cloud}/index.d.ts +0 -0
- /package/dist/cmd/{example → env}/list.d.ts +0 -0
package/src/cmd/auth/api.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { APIError, APIResponseSchema } from '@agentuity/server';
|
|
3
|
-
import { APIClient } from '../../api';
|
|
4
|
-
import type { Config } from '../../types';
|
|
2
|
+
import { APIError, APIResponseSchema, APIResponseSchemaOptionalData } from '@agentuity/server';
|
|
3
|
+
import type { APIClient } from '../../api';
|
|
5
4
|
|
|
6
5
|
// Zod schemas for API validation
|
|
7
6
|
const OTPStartDataSchema = z.object({
|
|
@@ -37,9 +36,8 @@ export interface SignupResult {
|
|
|
37
36
|
expires: Date;
|
|
38
37
|
}
|
|
39
38
|
|
|
40
|
-
export async function generateLoginOTP(
|
|
41
|
-
const
|
|
42
|
-
const resp = await client.request(
|
|
39
|
+
export async function generateLoginOTP(apiClient: APIClient): Promise<string> {
|
|
40
|
+
const resp = await apiClient.request(
|
|
43
41
|
'GET',
|
|
44
42
|
'/cli/auth/start',
|
|
45
43
|
APIResponseSchema(OTPStartDataSchema)
|
|
@@ -57,19 +55,17 @@ export async function generateLoginOTP(apiUrl: string, config?: Config | null):
|
|
|
57
55
|
}
|
|
58
56
|
|
|
59
57
|
export async function pollForLoginCompletion(
|
|
60
|
-
|
|
58
|
+
apiClient: APIClient,
|
|
61
59
|
otp: string,
|
|
62
|
-
config?: Config | null,
|
|
63
60
|
timeoutMs = 60000
|
|
64
61
|
): Promise<LoginResult> {
|
|
65
|
-
const client = new APIClient(apiUrl, config);
|
|
66
62
|
const started = Date.now();
|
|
67
63
|
|
|
68
64
|
while (Date.now() - started < timeoutMs) {
|
|
69
|
-
const resp = await
|
|
65
|
+
const resp = await apiClient.request(
|
|
70
66
|
'POST',
|
|
71
67
|
'/cli/auth/check',
|
|
72
|
-
|
|
68
|
+
APIResponseSchemaOptionalData(OTPCompleteDataSchema),
|
|
73
69
|
{ otp },
|
|
74
70
|
OTPCheckRequestSchema
|
|
75
71
|
);
|
|
@@ -104,17 +100,15 @@ export function generateSignupOTP(): string {
|
|
|
104
100
|
}
|
|
105
101
|
|
|
106
102
|
export async function pollForSignupCompletion(
|
|
107
|
-
|
|
103
|
+
apiClient: APIClient,
|
|
108
104
|
otp: string,
|
|
109
|
-
|
|
110
|
-
timeoutMs = 300000
|
|
105
|
+
timeoutMs = 350000
|
|
111
106
|
): Promise<SignupResult> {
|
|
112
|
-
const client = new APIClient(apiUrl, config);
|
|
113
107
|
const started = Date.now();
|
|
114
108
|
|
|
115
109
|
while (Date.now() - started < timeoutMs) {
|
|
116
110
|
try {
|
|
117
|
-
const resp = await
|
|
111
|
+
const resp = await apiClient.request(
|
|
118
112
|
'GET',
|
|
119
113
|
`/cli/auth/signup/${otp}`,
|
|
120
114
|
APIResponseSchema(SignupCompleteDataSchema)
|
package/src/cmd/auth/index.ts
CHANGED
|
@@ -2,9 +2,11 @@ import { createCommand } from '../../types';
|
|
|
2
2
|
import { loginCommand } from './login';
|
|
3
3
|
import { logoutCommand } from './logout';
|
|
4
4
|
import { signupCommand } from './signup';
|
|
5
|
+
import { whoamiCommand } from './whoami';
|
|
6
|
+
import { sshSubcommand } from './ssh';
|
|
5
7
|
|
|
6
8
|
export const command = createCommand({
|
|
7
9
|
name: 'auth',
|
|
8
10
|
description: 'Authentication and authorization related commands',
|
|
9
|
-
subcommands: [loginCommand, logoutCommand, signupCommand],
|
|
11
|
+
subcommands: [loginCommand, logoutCommand, signupCommand, whoamiCommand, sshSubcommand],
|
|
10
12
|
});
|
package/src/cmd/auth/login.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createSubcommand } from '../../types';
|
|
2
|
-
import { UpgradeRequiredError } from '@agentuity/server';
|
|
3
|
-
import {
|
|
2
|
+
import { UpgradeRequiredError, ValidationError } from '@agentuity/server';
|
|
3
|
+
import { getAppBaseURL } from '../../api';
|
|
4
4
|
import { saveAuth } from '../../config';
|
|
5
5
|
import { generateLoginOTP, pollForLoginCompletion } from './api';
|
|
6
6
|
import * as tui from '../../tui';
|
|
@@ -9,15 +9,25 @@ export const loginCommand = createSubcommand({
|
|
|
9
9
|
name: 'login',
|
|
10
10
|
description: 'Login to the Agentuity Platform using a browser-based authentication flow',
|
|
11
11
|
toplevel: true,
|
|
12
|
-
|
|
12
|
+
requires: { apiClient: true },
|
|
13
13
|
async handler(ctx) {
|
|
14
|
-
const { logger, config } = ctx;
|
|
15
|
-
|
|
14
|
+
const { logger, config, apiClient } = ctx;
|
|
15
|
+
|
|
16
|
+
if (!apiClient) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
'API client is not available. This is likely a configuration or initialization issue.'
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
16
22
|
const appUrl = getAppBaseURL(config);
|
|
17
23
|
|
|
18
24
|
try {
|
|
19
|
-
const otp = await tui.spinner(
|
|
20
|
-
|
|
25
|
+
const otp = await tui.spinner({
|
|
26
|
+
message: 'Generating login one time code...',
|
|
27
|
+
clearOnSuccess: true,
|
|
28
|
+
callback: () => {
|
|
29
|
+
return generateLoginOTP(apiClient);
|
|
30
|
+
},
|
|
21
31
|
});
|
|
22
32
|
|
|
23
33
|
if (!otp) {
|
|
@@ -46,7 +56,7 @@ export const loginCommand = createSubcommand({
|
|
|
46
56
|
|
|
47
57
|
console.log('Waiting for login to complete...');
|
|
48
58
|
|
|
49
|
-
const result = await pollForLoginCompletion(
|
|
59
|
+
const result = await pollForLoginCompletion(apiClient, otp);
|
|
50
60
|
|
|
51
61
|
await saveAuth({
|
|
52
62
|
apiKey: result.apiKey,
|
|
@@ -57,10 +67,16 @@ export const loginCommand = createSubcommand({
|
|
|
57
67
|
tui.newline();
|
|
58
68
|
tui.success('Welcome to Agentuity! You are now logged in');
|
|
59
69
|
} catch (error) {
|
|
70
|
+
logger.trace(error);
|
|
60
71
|
if (error instanceof UpgradeRequiredError) {
|
|
61
72
|
const bannerBody = `${error.message}\n\nVisit: ${tui.link('https://agentuity.dev/CLI/installation')}`;
|
|
62
73
|
tui.banner('CLI Upgrade Required', bannerBody);
|
|
63
74
|
process.exit(1);
|
|
75
|
+
} else if (error instanceof ValidationError) {
|
|
76
|
+
tui.error(`API error: ${error.message}`);
|
|
77
|
+
tui.warning(`API url: ${error.url}`);
|
|
78
|
+
error.issues.map((i) => tui.arrow(`${i.message} for ${i.path}`));
|
|
79
|
+
process.exit(1);
|
|
64
80
|
} else if (error instanceof Error) {
|
|
65
81
|
logger.fatal(`Login failed: ${error.message}`);
|
|
66
82
|
} else {
|
package/src/cmd/auth/signup.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createSubcommand } from '../../types';
|
|
2
|
-
import {
|
|
2
|
+
import { getAppBaseURL, UpgradeRequiredError } from '@agentuity/server';
|
|
3
3
|
import { saveAuth } from '../../config';
|
|
4
4
|
import { generateSignupOTP, pollForSignupCompletion } from './api';
|
|
5
5
|
import * as tui from '../../tui';
|
|
@@ -8,10 +8,10 @@ export const signupCommand = createSubcommand({
|
|
|
8
8
|
name: 'signup',
|
|
9
9
|
description: 'Create a new Agentuity Cloud Platform account',
|
|
10
10
|
toplevel: true,
|
|
11
|
+
requires: { apiClient: true },
|
|
11
12
|
|
|
12
13
|
async handler(ctx) {
|
|
13
|
-
const { logger, config } = ctx;
|
|
14
|
-
const apiUrl = getAPIBaseURL(config?.overrides);
|
|
14
|
+
const { logger, config, apiClient } = ctx;
|
|
15
15
|
const appUrl = getAppBaseURL(config?.overrides);
|
|
16
16
|
|
|
17
17
|
try {
|
|
@@ -24,14 +24,18 @@ export const signupCommand = createSubcommand({
|
|
|
24
24
|
tui.banner('Signup for Agentuity', bannerBody);
|
|
25
25
|
tui.newline();
|
|
26
26
|
|
|
27
|
-
await tui.spinner(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
await tui.spinner({
|
|
28
|
+
message: 'Waiting for signup to complete...',
|
|
29
|
+
clearOnSuccess: true,
|
|
30
|
+
callback: async () => {
|
|
31
|
+
const result = await pollForSignupCompletion(apiClient, otp);
|
|
32
|
+
|
|
33
|
+
await saveAuth({
|
|
34
|
+
apiKey: result.apiKey,
|
|
35
|
+
userId: result.userId,
|
|
36
|
+
expires: result.expires,
|
|
37
|
+
});
|
|
38
|
+
},
|
|
35
39
|
});
|
|
36
40
|
|
|
37
41
|
tui.newline();
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { createSubcommand } from '../../../types';
|
|
2
|
+
import { addSSHKey, computeSSHKeyFingerprint, listSSHKeys } from './api';
|
|
3
|
+
import * as tui from '../../../tui';
|
|
4
|
+
import { getCommand } from '../../../command-prefix';
|
|
5
|
+
import enquirer from 'enquirer';
|
|
6
|
+
import { readFileSync, readdirSync, statSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { homedir } from 'os';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
|
|
11
|
+
const optionsSchema = z.object({
|
|
12
|
+
file: z.string().optional().describe('File containing the public key'),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
interface SSHKeyOption {
|
|
16
|
+
path: string;
|
|
17
|
+
filename: string;
|
|
18
|
+
publicKey: string;
|
|
19
|
+
fingerprint: string;
|
|
20
|
+
comment: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Scan ~/.ssh directory for valid SSH public keys
|
|
25
|
+
*/
|
|
26
|
+
function discoverSSHKeys(): SSHKeyOption[] {
|
|
27
|
+
const sshDir = join(homedir(), '.ssh');
|
|
28
|
+
const keys: SSHKeyOption[] = [];
|
|
29
|
+
const seenFingerprints = new Set<string>();
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const files = readdirSync(sshDir);
|
|
33
|
+
|
|
34
|
+
for (const file of files) {
|
|
35
|
+
// Only look at .pub files (public keys)
|
|
36
|
+
if (!file.endsWith('.pub')) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const filePath = join(sshDir, file);
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const stat = statSync(filePath);
|
|
44
|
+
if (!stat.isFile()) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const content = readFileSync(filePath, 'utf-8').trim();
|
|
49
|
+
|
|
50
|
+
// Validate it's a valid SSH key
|
|
51
|
+
const fingerprint = computeSSHKeyFingerprint(content);
|
|
52
|
+
|
|
53
|
+
// Skip duplicate fingerprints
|
|
54
|
+
if (seenFingerprints.has(fingerprint)) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
seenFingerprints.add(fingerprint);
|
|
58
|
+
|
|
59
|
+
// Extract comment if present (last part of the key)
|
|
60
|
+
const parts = content.split(/\s+/);
|
|
61
|
+
const comment = parts.length >= 3 ? parts.slice(2).join(' ') : '';
|
|
62
|
+
|
|
63
|
+
keys.push({
|
|
64
|
+
path: filePath,
|
|
65
|
+
filename: file,
|
|
66
|
+
publicKey: content,
|
|
67
|
+
fingerprint,
|
|
68
|
+
comment,
|
|
69
|
+
});
|
|
70
|
+
} catch {
|
|
71
|
+
// Skip invalid keys
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
// If we can't read ~/.ssh, just return empty array
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Sort by filename for predictable ordering
|
|
81
|
+
return keys.sort((a, b) => a.filename.localeCompare(b.filename));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Read stdin once if non-TTY and return its contents, or null when there is
|
|
86
|
+
* no piped data (e.g. timeout).
|
|
87
|
+
* This helper should be the only place that consumes Bun.stdin.
|
|
88
|
+
*/
|
|
89
|
+
async function readStdinIfPiped(): Promise<string | null> {
|
|
90
|
+
if (process.stdin.isTTY) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const stdin = await Promise.race([
|
|
96
|
+
Bun.stdin.text(),
|
|
97
|
+
new Promise<null>((resolve) => setTimeout(() => resolve(null), 1000)),
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
return stdin !== null && stdin.trim().length > 0 ? stdin : null;
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const addCommand = createSubcommand({
|
|
107
|
+
name: 'add',
|
|
108
|
+
aliases: ['create'],
|
|
109
|
+
description: 'Add an SSH public key to your account (reads from file or stdin)',
|
|
110
|
+
requires: { apiClient: true, auth: true },
|
|
111
|
+
schema: {
|
|
112
|
+
options: optionsSchema,
|
|
113
|
+
},
|
|
114
|
+
async handler(ctx) {
|
|
115
|
+
const { logger, apiClient, opts } = ctx;
|
|
116
|
+
|
|
117
|
+
if (!apiClient) {
|
|
118
|
+
logger.fatal('API client is not available');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
let publicKey: string = '';
|
|
123
|
+
|
|
124
|
+
if (opts.file) {
|
|
125
|
+
// Read from file
|
|
126
|
+
try {
|
|
127
|
+
publicKey = readFileSync(opts.file, 'utf-8').trim();
|
|
128
|
+
} catch (error) {
|
|
129
|
+
logger.fatal(
|
|
130
|
+
`Error reading file: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
const stdin = await readStdinIfPiped();
|
|
135
|
+
if (stdin) {
|
|
136
|
+
// Read from stdin if data is piped
|
|
137
|
+
publicKey = stdin.trim();
|
|
138
|
+
} else {
|
|
139
|
+
// No file or stdin - discover SSH keys
|
|
140
|
+
const discoveredKeys = discoverSSHKeys();
|
|
141
|
+
|
|
142
|
+
if (discoveredKeys.length === 0) {
|
|
143
|
+
logger.fatal(
|
|
144
|
+
'No SSH public keys found in ~/.ssh/\n' +
|
|
145
|
+
'Please specify a file with --file or pipe the key via stdin'
|
|
146
|
+
);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Fetch existing keys from server to filter out already-added ones
|
|
151
|
+
const existingKeys = await tui.spinner({
|
|
152
|
+
type: 'simple',
|
|
153
|
+
message: 'Checking existing SSH keys...',
|
|
154
|
+
callback: () => listSSHKeys(apiClient),
|
|
155
|
+
clearOnSuccess: true,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const existingFingerprints = new Set(existingKeys.map((k) => k.fingerprint));
|
|
159
|
+
const newKeys = discoveredKeys.filter(
|
|
160
|
+
(k) => !existingFingerprints.has(k.fingerprint)
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
if (newKeys.length === 0) {
|
|
164
|
+
const cmd = getCommand('auth ssh add');
|
|
165
|
+
const boldcmd = tui.bold('cat key.pub | ' + cmd);
|
|
166
|
+
tui.info('All local SSH keys in ~/.ssh/ have already been added to your account');
|
|
167
|
+
tui.newline();
|
|
168
|
+
console.log('To add a different key:');
|
|
169
|
+
tui.bullet(`Use ${tui.bold('--file <path>')} to specify a key file`);
|
|
170
|
+
tui.bullet(`Pipe the key via stdin: ${boldcmd}`);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!process.stdin.isTTY) {
|
|
175
|
+
logger.fatal(
|
|
176
|
+
'Interactive selection required but cannot prompt in non-TTY environment. Use --file or pipe the key via stdin.'
|
|
177
|
+
);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const response = await enquirer.prompt<{ keys: string[] }>({
|
|
182
|
+
type: 'multiselect',
|
|
183
|
+
name: 'keys',
|
|
184
|
+
message: 'Select SSH keys to add (Space to select, Enter to confirm)',
|
|
185
|
+
choices: newKeys.map((key) => {
|
|
186
|
+
const keyType = key.publicKey.split(/\s+/)[0] || 'unknown';
|
|
187
|
+
return {
|
|
188
|
+
name: key.fingerprint,
|
|
189
|
+
message: `${keyType.padEnd(12)} ${key.fingerprint} ${tui.muted(key.comment || key.filename)}`,
|
|
190
|
+
};
|
|
191
|
+
}),
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const selectedFingerprints = response.keys;
|
|
195
|
+
|
|
196
|
+
if (selectedFingerprints.length === 0) {
|
|
197
|
+
tui.newline();
|
|
198
|
+
tui.info('No keys selected');
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Build Map for O(1) lookups
|
|
203
|
+
const keyMap = new Map(newKeys.map((k) => [k.fingerprint, k]));
|
|
204
|
+
|
|
205
|
+
// Add all selected keys
|
|
206
|
+
for (const fingerprint of selectedFingerprints) {
|
|
207
|
+
const key = keyMap.get(fingerprint);
|
|
208
|
+
if (!key) continue;
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const result = await tui.spinner({
|
|
212
|
+
type: 'simple',
|
|
213
|
+
message: `Adding SSH key ${fingerprint}...`,
|
|
214
|
+
callback: () => addSSHKey(apiClient, key.publicKey),
|
|
215
|
+
clearOnSuccess: true,
|
|
216
|
+
});
|
|
217
|
+
tui.success(`SSH key added: ${tui.muted(result.fingerprint)}`);
|
|
218
|
+
} catch (error) {
|
|
219
|
+
tui.newline();
|
|
220
|
+
if (error instanceof Error) {
|
|
221
|
+
tui.error(`Failed to add ${fingerprint}: ${error.message}`);
|
|
222
|
+
} else {
|
|
223
|
+
tui.error(`Failed to add ${fingerprint}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Only process single key if we got here (from --file or stdin)
|
|
233
|
+
if (!publicKey) {
|
|
234
|
+
logger.fatal('No public key provided');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Validate key format
|
|
238
|
+
try {
|
|
239
|
+
computeSSHKeyFingerprint(publicKey);
|
|
240
|
+
} catch (error) {
|
|
241
|
+
logger.fatal(
|
|
242
|
+
`Invalid SSH key format: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const result = await tui.spinner({
|
|
247
|
+
type: 'simple',
|
|
248
|
+
message: 'Adding SSH key...',
|
|
249
|
+
callback: () => addSSHKey(apiClient, publicKey),
|
|
250
|
+
clearOnSuccess: true,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
tui.success(`SSH key added: ${tui.muted(result.fingerprint)}`);
|
|
254
|
+
} catch (error) {
|
|
255
|
+
logger.trace(error);
|
|
256
|
+
if (error instanceof Error) {
|
|
257
|
+
logger.fatal(`Failed to add SSH key: ${error.message}`);
|
|
258
|
+
} else {
|
|
259
|
+
logger.fatal('Failed to add SSH key');
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { APIResponseSchema } from '@agentuity/server';
|
|
3
|
+
import type { APIClient } from '../../../api';
|
|
4
|
+
import { createHash } from 'crypto';
|
|
5
|
+
|
|
6
|
+
// Zod schemas for API validation
|
|
7
|
+
const SSHKeySchema = z.object({
|
|
8
|
+
fingerprint: z.string(),
|
|
9
|
+
keyType: z.string(),
|
|
10
|
+
comment: z.string(),
|
|
11
|
+
publicKey: z.string(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const AddSSHKeyResponseSchema = z.object({
|
|
15
|
+
fingerprint: z.string(),
|
|
16
|
+
added: z.boolean(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const RemoveSSHKeyResponseSchema = z.object({
|
|
20
|
+
removed: z.boolean(),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Exported result types
|
|
24
|
+
export interface SSHKey {
|
|
25
|
+
fingerprint: string;
|
|
26
|
+
keyType: string;
|
|
27
|
+
comment: string;
|
|
28
|
+
publicKey: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface AddSSHKeyResult {
|
|
32
|
+
fingerprint: string;
|
|
33
|
+
added: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function computeSSHKeyFingerprint(publicKey: string): string {
|
|
37
|
+
// Parse the key (format: "ssh-ed25519 AAAAC3... [comment]")
|
|
38
|
+
const parts = publicKey.trim().split(/\s+/);
|
|
39
|
+
if (parts.length < 2) {
|
|
40
|
+
throw new Error('Invalid SSH public key format');
|
|
41
|
+
}
|
|
42
|
+
const keyData = parts[1]; // Base64-encoded key data
|
|
43
|
+
const buffer = Buffer.from(keyData, 'base64');
|
|
44
|
+
const fingerprint = createHash('sha256').update(buffer).digest('base64');
|
|
45
|
+
return `SHA256:${fingerprint.replace(/=+$/, '')}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function addSSHKey(apiClient: APIClient, publicKey: string): Promise<AddSSHKeyResult> {
|
|
49
|
+
const resp = await apiClient.request(
|
|
50
|
+
'POST',
|
|
51
|
+
'/cli/auth/ssh-keys',
|
|
52
|
+
APIResponseSchema(AddSSHKeyResponseSchema),
|
|
53
|
+
{ publicKey }
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
if (!resp.success) {
|
|
57
|
+
throw new Error(resp.message);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!resp.data) {
|
|
61
|
+
throw new Error('No data returned from server');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return resp.data;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function listSSHKeys(apiClient: APIClient): Promise<SSHKey[]> {
|
|
68
|
+
const resp = await apiClient.request(
|
|
69
|
+
'GET',
|
|
70
|
+
'/cli/auth/ssh-keys',
|
|
71
|
+
APIResponseSchema(z.array(SSHKeySchema))
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if (!resp.success) {
|
|
75
|
+
throw new Error(resp.message);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return resp.data ?? [];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function removeSSHKey(apiClient: APIClient, fingerprint: string): Promise<boolean> {
|
|
82
|
+
const resp = await apiClient.request(
|
|
83
|
+
'DELETE',
|
|
84
|
+
'/cli/auth/ssh-keys',
|
|
85
|
+
APIResponseSchema(RemoveSSHKeyResponseSchema),
|
|
86
|
+
{ fingerprint }
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
if (!resp.success) {
|
|
90
|
+
throw new Error(resp.message);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return resp.data?.removed ?? false;
|
|
94
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { createSubcommand } from '../../../types';
|
|
2
|
+
import { removeSSHKey, listSSHKeys } from './api';
|
|
3
|
+
import * as tui from '../../../tui';
|
|
4
|
+
import enquirer from 'enquirer';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
|
|
7
|
+
export const deleteCommand = createSubcommand({
|
|
8
|
+
name: 'delete',
|
|
9
|
+
aliases: ['rm', 'del', 'remove'],
|
|
10
|
+
description: 'Delete an SSH key from your account',
|
|
11
|
+
requires: { apiClient: true, auth: true },
|
|
12
|
+
schema: {
|
|
13
|
+
args: z.object({
|
|
14
|
+
fingerprints: z.array(z.string()).optional().describe('SSH key fingerprint(s) to remove'),
|
|
15
|
+
}),
|
|
16
|
+
options: z.object({
|
|
17
|
+
confirm: z.boolean().default(true).describe('prompt for confirmation before deletion'),
|
|
18
|
+
}),
|
|
19
|
+
},
|
|
20
|
+
async handler(ctx) {
|
|
21
|
+
const { logger, apiClient, args, opts } = ctx;
|
|
22
|
+
|
|
23
|
+
if (!apiClient) {
|
|
24
|
+
logger.fatal('API client is not available');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const shouldConfirm = process.stdin.isTTY ? opts.confirm : false;
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
let fingerprintsToRemove: string[] = [];
|
|
31
|
+
|
|
32
|
+
if (args.fingerprints && args.fingerprints.length > 0) {
|
|
33
|
+
fingerprintsToRemove = args.fingerprints;
|
|
34
|
+
} else {
|
|
35
|
+
const keys = await tui.spinner('Fetching SSH keys...', () => listSSHKeys(apiClient));
|
|
36
|
+
|
|
37
|
+
if (keys.length === 0) {
|
|
38
|
+
tui.newline();
|
|
39
|
+
tui.info('No SSH keys found');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!process.stdin.isTTY) {
|
|
44
|
+
logger.fatal(
|
|
45
|
+
'Interactive selection required but cannot prompt in non-TTY environment. Provide fingerprint as argument.'
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
tui.newline();
|
|
50
|
+
|
|
51
|
+
const response = await enquirer.prompt<{ keys: string[] }>({
|
|
52
|
+
type: 'multiselect',
|
|
53
|
+
name: 'keys',
|
|
54
|
+
message: 'Select SSH keys to remove (Space to select, Enter to confirm)',
|
|
55
|
+
choices: keys.map((key) => ({
|
|
56
|
+
name: key.fingerprint,
|
|
57
|
+
message: `${key.keyType.padEnd(12)} ${key.fingerprint} ${tui.muted(key.comment || '(no comment)')}`,
|
|
58
|
+
})),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
fingerprintsToRemove = response.keys;
|
|
62
|
+
|
|
63
|
+
if (fingerprintsToRemove.length === 0) {
|
|
64
|
+
tui.newline();
|
|
65
|
+
tui.info('No keys selected');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (shouldConfirm) {
|
|
71
|
+
tui.newline();
|
|
72
|
+
const confirmed = await tui.confirm(
|
|
73
|
+
`Remove ${fingerprintsToRemove.length} SSH key${fingerprintsToRemove.length > 1 ? 's' : ''}?`,
|
|
74
|
+
false
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
if (!confirmed) {
|
|
78
|
+
tui.info('Cancelled');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (const fingerprint of fingerprintsToRemove) {
|
|
84
|
+
await tui.spinner(`Removing SSH key ${fingerprint}...`, () =>
|
|
85
|
+
removeSSHKey(apiClient, fingerprint)
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
tui.newline();
|
|
90
|
+
tui.success(
|
|
91
|
+
`Removed ${fingerprintsToRemove.length} SSH key${fingerprintsToRemove.length > 1 ? 's' : ''}`
|
|
92
|
+
);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
logger.trace(error);
|
|
95
|
+
if (error instanceof Error) {
|
|
96
|
+
logger.fatal(`Failed to remove SSH key: ${error.message}`);
|
|
97
|
+
} else {
|
|
98
|
+
logger.fatal('Failed to remove SSH key');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SubcommandDefinition } from '../../../types';
|
|
2
|
+
import { listCommand } from './list';
|
|
3
|
+
import { addCommand } from './add';
|
|
4
|
+
import { deleteCommand } from './delete';
|
|
5
|
+
|
|
6
|
+
export const sshSubcommand: SubcommandDefinition = {
|
|
7
|
+
name: 'ssh',
|
|
8
|
+
description: 'Manage SSH keys',
|
|
9
|
+
subcommands: [listCommand, addCommand, deleteCommand],
|
|
10
|
+
};
|