@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.
Files changed (249) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +1 -1
  3. package/bin/cli.ts +7 -5
  4. package/dist/api.d.ts +3 -3
  5. package/dist/api.d.ts.map +1 -1
  6. package/dist/auth.d.ts +10 -2
  7. package/dist/auth.d.ts.map +1 -1
  8. package/dist/banner.d.ts.map +1 -1
  9. package/dist/cli.d.ts.map +1 -1
  10. package/dist/cmd/auth/api.d.ts +4 -4
  11. package/dist/cmd/auth/api.d.ts.map +1 -1
  12. package/dist/cmd/auth/index.d.ts.map +1 -1
  13. package/dist/cmd/auth/login.d.ts.map +1 -1
  14. package/dist/cmd/auth/signup.d.ts.map +1 -1
  15. package/dist/cmd/auth/ssh/add.d.ts +2 -0
  16. package/dist/cmd/auth/ssh/add.d.ts.map +1 -0
  17. package/dist/cmd/auth/ssh/api.d.ts +16 -0
  18. package/dist/cmd/auth/ssh/api.d.ts.map +1 -0
  19. package/dist/cmd/auth/ssh/delete.d.ts +2 -0
  20. package/dist/cmd/auth/ssh/delete.d.ts.map +1 -0
  21. package/dist/cmd/auth/ssh/index.d.ts +3 -0
  22. package/dist/cmd/auth/ssh/index.d.ts.map +1 -0
  23. package/dist/cmd/auth/ssh/list.d.ts +2 -0
  24. package/dist/cmd/auth/ssh/list.d.ts.map +1 -0
  25. package/dist/cmd/auth/whoami.d.ts +2 -0
  26. package/dist/cmd/auth/whoami.d.ts.map +1 -0
  27. package/dist/cmd/bundle/ast.d.ts +14 -3
  28. package/dist/cmd/bundle/ast.d.ts.map +1 -1
  29. package/dist/cmd/bundle/ast.test.d.ts +2 -0
  30. package/dist/cmd/bundle/ast.test.d.ts.map +1 -0
  31. package/dist/cmd/bundle/bundler.d.ts +6 -1
  32. package/dist/cmd/bundle/bundler.d.ts.map +1 -1
  33. package/dist/cmd/bundle/file.d.ts.map +1 -1
  34. package/dist/cmd/bundle/fix-duplicate-exports.d.ts +2 -0
  35. package/dist/cmd/bundle/fix-duplicate-exports.d.ts.map +1 -0
  36. package/dist/cmd/bundle/fix-duplicate-exports.test.d.ts +2 -0
  37. package/dist/cmd/bundle/fix-duplicate-exports.test.d.ts.map +1 -0
  38. package/dist/cmd/bundle/index.d.ts +1 -1
  39. package/dist/cmd/bundle/index.d.ts.map +1 -1
  40. package/dist/cmd/bundle/plugin.d.ts +2 -0
  41. package/dist/cmd/bundle/plugin.d.ts.map +1 -1
  42. package/dist/cmd/cloud/deploy.d.ts.map +1 -0
  43. package/dist/cmd/cloud/domain.d.ts +17 -0
  44. package/dist/cmd/cloud/domain.d.ts.map +1 -0
  45. package/dist/cmd/cloud/index.d.ts.map +1 -0
  46. package/dist/cmd/cloud/resource/add.d.ts +2 -0
  47. package/dist/cmd/cloud/resource/add.d.ts.map +1 -0
  48. package/dist/cmd/cloud/resource/delete.d.ts +2 -0
  49. package/dist/cmd/cloud/resource/delete.d.ts.map +1 -0
  50. package/dist/cmd/cloud/resource/index.d.ts +3 -0
  51. package/dist/cmd/cloud/resource/index.d.ts.map +1 -0
  52. package/dist/cmd/cloud/resource/list.d.ts +2 -0
  53. package/dist/cmd/cloud/resource/list.d.ts.map +1 -0
  54. package/dist/cmd/cloud/scp/download.d.ts +2 -0
  55. package/dist/cmd/cloud/scp/download.d.ts.map +1 -0
  56. package/dist/cmd/cloud/scp/index.d.ts +3 -0
  57. package/dist/cmd/cloud/scp/index.d.ts.map +1 -0
  58. package/dist/cmd/cloud/scp/upload.d.ts +2 -0
  59. package/dist/cmd/cloud/scp/upload.d.ts.map +1 -0
  60. package/dist/cmd/cloud/ssh.d.ts +2 -0
  61. package/dist/cmd/cloud/ssh.d.ts.map +1 -0
  62. package/dist/cmd/dev/api.d.ts +18 -0
  63. package/dist/cmd/dev/api.d.ts.map +1 -0
  64. package/dist/cmd/dev/download.d.ts +11 -0
  65. package/dist/cmd/dev/download.d.ts.map +1 -0
  66. package/dist/cmd/dev/index.d.ts.map +1 -1
  67. package/dist/cmd/dev/templates.d.ts +3 -0
  68. package/dist/cmd/dev/templates.d.ts.map +1 -0
  69. package/dist/cmd/env/delete.d.ts +2 -0
  70. package/dist/cmd/env/delete.d.ts.map +1 -0
  71. package/dist/cmd/env/get.d.ts +2 -0
  72. package/dist/cmd/env/get.d.ts.map +1 -0
  73. package/dist/cmd/env/import.d.ts +2 -0
  74. package/dist/cmd/env/import.d.ts.map +1 -0
  75. package/dist/cmd/env/index.d.ts +2 -0
  76. package/dist/cmd/env/index.d.ts.map +1 -0
  77. package/dist/cmd/env/list.d.ts.map +1 -0
  78. package/dist/cmd/env/pull.d.ts +2 -0
  79. package/dist/cmd/env/pull.d.ts.map +1 -0
  80. package/dist/cmd/env/push.d.ts +2 -0
  81. package/dist/cmd/env/push.d.ts.map +1 -0
  82. package/dist/cmd/env/set.d.ts +2 -0
  83. package/dist/cmd/env/set.d.ts.map +1 -0
  84. package/dist/cmd/profile/show.d.ts.map +1 -1
  85. package/dist/cmd/project/create.d.ts.map +1 -1
  86. package/dist/cmd/project/delete.d.ts.map +1 -1
  87. package/dist/cmd/project/download.d.ts +1 -1
  88. package/dist/cmd/project/download.d.ts.map +1 -1
  89. package/dist/cmd/project/list.d.ts.map +1 -1
  90. package/dist/cmd/project/show.d.ts.map +1 -1
  91. package/dist/cmd/project/template-flow.d.ts +5 -1
  92. package/dist/cmd/project/template-flow.d.ts.map +1 -1
  93. package/dist/cmd/secret/delete.d.ts +2 -0
  94. package/dist/cmd/secret/delete.d.ts.map +1 -0
  95. package/dist/cmd/secret/get.d.ts +2 -0
  96. package/dist/cmd/secret/get.d.ts.map +1 -0
  97. package/dist/cmd/secret/import.d.ts +2 -0
  98. package/dist/cmd/secret/import.d.ts.map +1 -0
  99. package/dist/cmd/secret/index.d.ts +2 -0
  100. package/dist/cmd/secret/index.d.ts.map +1 -0
  101. package/dist/cmd/secret/list.d.ts +2 -0
  102. package/dist/cmd/secret/list.d.ts.map +1 -0
  103. package/dist/cmd/secret/pull.d.ts +2 -0
  104. package/dist/cmd/secret/pull.d.ts.map +1 -0
  105. package/dist/cmd/secret/push.d.ts +2 -0
  106. package/dist/cmd/secret/push.d.ts.map +1 -0
  107. package/dist/cmd/secret/set.d.ts +2 -0
  108. package/dist/cmd/secret/set.d.ts.map +1 -0
  109. package/dist/cmd/version/index.d.ts.map +1 -1
  110. package/dist/config.d.ts +11 -3
  111. package/dist/config.d.ts.map +1 -1
  112. package/dist/crypto/box.d.ts +65 -0
  113. package/dist/crypto/box.d.ts.map +1 -0
  114. package/dist/crypto/box.test.d.ts +2 -0
  115. package/dist/crypto/box.test.d.ts.map +1 -0
  116. package/dist/download.d.ts.map +1 -1
  117. package/dist/env-util.d.ts +67 -0
  118. package/dist/env-util.d.ts.map +1 -0
  119. package/dist/env-util.test.d.ts +2 -0
  120. package/dist/env-util.test.d.ts.map +1 -0
  121. package/dist/index.d.ts +1 -1
  122. package/dist/index.d.ts.map +1 -1
  123. package/dist/schema-parser.d.ts.map +1 -1
  124. package/dist/steps.d.ts +4 -1
  125. package/dist/steps.d.ts.map +1 -1
  126. package/dist/terminal.d.ts.map +1 -1
  127. package/dist/tui.d.ts +32 -2
  128. package/dist/tui.d.ts.map +1 -1
  129. package/dist/types.d.ts +250 -127
  130. package/dist/types.d.ts.map +1 -1
  131. package/dist/utils/detectSubagent.d.ts +15 -0
  132. package/dist/utils/detectSubagent.d.ts.map +1 -0
  133. package/dist/utils/zip.d.ts +7 -0
  134. package/dist/utils/zip.d.ts.map +1 -0
  135. package/package.json +11 -3
  136. package/src/api-errors.md +2 -2
  137. package/src/api.ts +12 -7
  138. package/src/auth.ts +116 -7
  139. package/src/banner.ts +13 -6
  140. package/src/cli.ts +709 -36
  141. package/src/cmd/auth/api.ts +10 -16
  142. package/src/cmd/auth/index.ts +3 -1
  143. package/src/cmd/auth/login.ts +24 -8
  144. package/src/cmd/auth/signup.ts +15 -11
  145. package/src/cmd/auth/ssh/add.ts +263 -0
  146. package/src/cmd/auth/ssh/api.ts +94 -0
  147. package/src/cmd/auth/ssh/delete.ts +102 -0
  148. package/src/cmd/auth/ssh/index.ts +10 -0
  149. package/src/cmd/auth/ssh/list.ts +74 -0
  150. package/src/cmd/auth/whoami.ts +69 -0
  151. package/src/cmd/bundle/ast.test.ts +565 -0
  152. package/src/cmd/bundle/ast.ts +457 -44
  153. package/src/cmd/bundle/bundler.ts +255 -57
  154. package/src/cmd/bundle/file.ts +6 -12
  155. package/src/cmd/bundle/fix-duplicate-exports.test.ts +387 -0
  156. package/src/cmd/bundle/fix-duplicate-exports.ts +204 -0
  157. package/src/cmd/bundle/index.ts +11 -11
  158. package/src/cmd/bundle/patch/aisdk.ts +1 -1
  159. package/src/cmd/bundle/plugin.ts +373 -53
  160. package/src/cmd/cloud/deploy.ts +336 -0
  161. package/src/cmd/cloud/domain.ts +92 -0
  162. package/src/cmd/cloud/index.ts +11 -0
  163. package/src/cmd/cloud/resource/add.ts +56 -0
  164. package/src/cmd/cloud/resource/delete.ts +120 -0
  165. package/src/cmd/cloud/resource/index.ts +11 -0
  166. package/src/cmd/cloud/resource/list.ts +69 -0
  167. package/src/cmd/cloud/scp/download.ts +59 -0
  168. package/src/cmd/cloud/scp/index.ts +9 -0
  169. package/src/cmd/cloud/scp/upload.ts +62 -0
  170. package/src/cmd/cloud/ssh.ts +68 -0
  171. package/src/cmd/dev/api.ts +46 -0
  172. package/src/cmd/dev/download.ts +111 -0
  173. package/src/cmd/dev/index.ts +362 -34
  174. package/src/cmd/dev/templates.ts +84 -0
  175. package/src/cmd/env/delete.ts +47 -0
  176. package/src/cmd/env/get.ts +53 -0
  177. package/src/cmd/env/import.ts +102 -0
  178. package/src/cmd/env/index.ts +22 -0
  179. package/src/cmd/env/list.ts +56 -0
  180. package/src/cmd/env/pull.ts +80 -0
  181. package/src/cmd/env/push.ts +37 -0
  182. package/src/cmd/env/set.ts +71 -0
  183. package/src/cmd/index.ts +2 -2
  184. package/src/cmd/profile/show.ts +15 -6
  185. package/src/cmd/project/create.ts +7 -2
  186. package/src/cmd/project/delete.ts +75 -18
  187. package/src/cmd/project/download.ts +3 -3
  188. package/src/cmd/project/list.ts +8 -8
  189. package/src/cmd/project/show.ts +3 -7
  190. package/src/cmd/project/template-flow.ts +186 -48
  191. package/src/cmd/secret/delete.ts +40 -0
  192. package/src/cmd/secret/get.ts +54 -0
  193. package/src/cmd/secret/import.ts +64 -0
  194. package/src/cmd/secret/index.ts +22 -0
  195. package/src/cmd/secret/list.ts +56 -0
  196. package/src/cmd/secret/pull.ts +78 -0
  197. package/src/cmd/secret/push.ts +37 -0
  198. package/src/cmd/secret/set.ts +45 -0
  199. package/src/cmd/version/index.ts +2 -1
  200. package/src/config.ts +257 -27
  201. package/src/crypto/box.test.ts +431 -0
  202. package/src/crypto/box.ts +477 -0
  203. package/src/download.ts +1 -0
  204. package/src/env-util.test.ts +194 -0
  205. package/src/env-util.ts +290 -0
  206. package/src/index.ts +5 -1
  207. package/src/schema-parser.ts +2 -3
  208. package/src/steps.ts +144 -10
  209. package/src/terminal.ts +24 -23
  210. package/src/tui.ts +208 -68
  211. package/src/types.ts +292 -202
  212. package/src/utils/detectSubagent.ts +31 -0
  213. package/src/utils/zip.ts +38 -0
  214. package/dist/cmd/example/create-user.d.ts +0 -2
  215. package/dist/cmd/example/create-user.d.ts.map +0 -1
  216. package/dist/cmd/example/create.d.ts +0 -2
  217. package/dist/cmd/example/create.d.ts.map +0 -1
  218. package/dist/cmd/example/deploy.d.ts.map +0 -1
  219. package/dist/cmd/example/index.d.ts.map +0 -1
  220. package/dist/cmd/example/list.d.ts.map +0 -1
  221. package/dist/cmd/example/optional-auth.d.ts +0 -3
  222. package/dist/cmd/example/optional-auth.d.ts.map +0 -1
  223. package/dist/cmd/example/run-command.d.ts +0 -2
  224. package/dist/cmd/example/run-command.d.ts.map +0 -1
  225. package/dist/cmd/example/sound.d.ts +0 -3
  226. package/dist/cmd/example/sound.d.ts.map +0 -1
  227. package/dist/cmd/example/spinner.d.ts +0 -2
  228. package/dist/cmd/example/spinner.d.ts.map +0 -1
  229. package/dist/cmd/example/steps.d.ts +0 -2
  230. package/dist/cmd/example/steps.d.ts.map +0 -1
  231. package/dist/cmd/example/version.d.ts +0 -2
  232. package/dist/cmd/example/version.d.ts.map +0 -1
  233. package/dist/logger.d.ts +0 -24
  234. package/dist/logger.d.ts.map +0 -1
  235. package/src/cmd/example/create-user.ts +0 -38
  236. package/src/cmd/example/create.ts +0 -31
  237. package/src/cmd/example/deploy.ts +0 -36
  238. package/src/cmd/example/index.ts +0 -29
  239. package/src/cmd/example/list.ts +0 -32
  240. package/src/cmd/example/optional-auth.ts +0 -38
  241. package/src/cmd/example/run-command.ts +0 -45
  242. package/src/cmd/example/sound.ts +0 -14
  243. package/src/cmd/example/spinner.ts +0 -44
  244. package/src/cmd/example/steps.ts +0 -66
  245. package/src/cmd/example/version.ts +0 -13
  246. package/src/logger.ts +0 -235
  247. /package/dist/cmd/{example → cloud}/deploy.d.ts +0 -0
  248. /package/dist/cmd/{example → cloud}/index.d.ts +0 -0
  249. /package/dist/cmd/{example → env}/list.d.ts +0 -0
@@ -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(apiUrl: string, config?: Config | null): Promise<string> {
41
- const client = new APIClient(apiUrl, config);
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
- apiUrl: string,
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 client.request(
65
+ const resp = await apiClient.request(
70
66
  'POST',
71
67
  '/cli/auth/check',
72
- APIResponseSchema(OTPCompleteDataSchema),
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
- apiUrl: string,
103
+ apiClient: APIClient,
108
104
  otp: string,
109
- config?: Config | null,
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 client.request(
111
+ const resp = await apiClient.request(
118
112
  'GET',
119
113
  `/cli/auth/signup/${otp}`,
120
114
  APIResponseSchema(SignupCompleteDataSchema)
@@ -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
  });
@@ -1,6 +1,6 @@
1
1
  import { createSubcommand } from '../../types';
2
- import { UpgradeRequiredError } from '@agentuity/server';
3
- import { getAPIBaseURL, getAppBaseURL } from '../../api';
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
- const apiUrl = getAPIBaseURL(config);
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('Generating login one time code...', () => {
20
- return generateLoginOTP(apiUrl, config);
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(apiUrl, otp, config);
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 {
@@ -1,5 +1,5 @@
1
1
  import { createSubcommand } from '../../types';
2
- import { getAPIBaseURL, getAppBaseURL, UpgradeRequiredError } from '@agentuity/server';
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('Waiting for signup to complete...', async () => {
28
- const result = await pollForSignupCompletion(apiUrl, otp, config);
29
-
30
- await saveAuth({
31
- apiKey: result.apiKey,
32
- userId: result.userId,
33
- expires: result.expires,
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
+ };