@agentuity/cli 0.0.53 → 0.0.54

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 (133) hide show
  1. package/dist/cli.d.ts.map +1 -1
  2. package/dist/cli.js +66 -8
  3. package/dist/cli.js.map +1 -1
  4. package/dist/cmd/auth/ssh/add.d.ts.map +1 -1
  5. package/dist/cmd/auth/ssh/add.js +28 -13
  6. package/dist/cmd/auth/ssh/add.js.map +1 -1
  7. package/dist/cmd/auth/ssh/delete.d.ts.map +1 -1
  8. package/dist/cmd/auth/ssh/delete.js +28 -18
  9. package/dist/cmd/auth/ssh/delete.js.map +1 -1
  10. package/dist/cmd/auth/ssh/list.d.ts.map +1 -1
  11. package/dist/cmd/auth/ssh/list.js +5 -6
  12. package/dist/cmd/auth/ssh/list.js.map +1 -1
  13. package/dist/cmd/build/ast.d.ts +34 -0
  14. package/dist/cmd/build/ast.d.ts.map +1 -1
  15. package/dist/cmd/build/ast.js +159 -0
  16. package/dist/cmd/build/ast.js.map +1 -1
  17. package/dist/cmd/build/bundler.d.ts +2 -1
  18. package/dist/cmd/build/bundler.d.ts.map +1 -1
  19. package/dist/cmd/build/bundler.js +77 -16
  20. package/dist/cmd/build/bundler.js.map +1 -1
  21. package/dist/cmd/build/plugin.d.ts.map +1 -1
  22. package/dist/cmd/build/plugin.js +74 -4
  23. package/dist/cmd/build/plugin.js.map +1 -1
  24. package/dist/cmd/cloud/deployment/list.d.ts.map +1 -1
  25. package/dist/cmd/cloud/deployment/list.js +28 -23
  26. package/dist/cmd/cloud/deployment/list.js.map +1 -1
  27. package/dist/cmd/cloud/deployment/show.d.ts.map +1 -1
  28. package/dist/cmd/cloud/deployment/show.js +50 -47
  29. package/dist/cmd/cloud/deployment/show.js.map +1 -1
  30. package/dist/cmd/cloud/env/get.d.ts.map +1 -1
  31. package/dist/cmd/cloud/env/get.js +16 -14
  32. package/dist/cmd/cloud/env/get.js.map +1 -1
  33. package/dist/cmd/cloud/env/list.d.ts.map +1 -1
  34. package/dist/cmd/cloud/env/list.js +24 -20
  35. package/dist/cmd/cloud/env/list.js.map +1 -1
  36. package/dist/cmd/cloud/keyvalue/get.d.ts.map +1 -1
  37. package/dist/cmd/cloud/keyvalue/get.js +18 -16
  38. package/dist/cmd/cloud/keyvalue/get.js.map +1 -1
  39. package/dist/cmd/cloud/keyvalue/keys.d.ts.map +1 -1
  40. package/dist/cmd/cloud/keyvalue/keys.js +11 -11
  41. package/dist/cmd/cloud/keyvalue/keys.js.map +1 -1
  42. package/dist/cmd/cloud/keyvalue/list-namespaces.d.ts.map +1 -1
  43. package/dist/cmd/cloud/keyvalue/list-namespaces.js +11 -7
  44. package/dist/cmd/cloud/keyvalue/list-namespaces.js.map +1 -1
  45. package/dist/cmd/cloud/keyvalue/search.d.ts.map +1 -1
  46. package/dist/cmd/cloud/keyvalue/search.js +16 -17
  47. package/dist/cmd/cloud/keyvalue/search.js.map +1 -1
  48. package/dist/cmd/cloud/keyvalue/stats.d.ts.map +1 -1
  49. package/dist/cmd/cloud/keyvalue/stats.js +38 -23
  50. package/dist/cmd/cloud/keyvalue/stats.js.map +1 -1
  51. package/dist/cmd/cloud/objectstore/get.d.ts.map +1 -1
  52. package/dist/cmd/cloud/objectstore/get.js +17 -15
  53. package/dist/cmd/cloud/objectstore/get.js.map +1 -1
  54. package/dist/cmd/cloud/objectstore/list-buckets.d.ts.map +1 -1
  55. package/dist/cmd/cloud/objectstore/list-buckets.js +12 -8
  56. package/dist/cmd/cloud/objectstore/list-buckets.js.map +1 -1
  57. package/dist/cmd/cloud/objectstore/list-keys.d.ts.map +1 -1
  58. package/dist/cmd/cloud/objectstore/list-keys.js +13 -10
  59. package/dist/cmd/cloud/objectstore/list-keys.js.map +1 -1
  60. package/dist/cmd/cloud/resource/list.d.ts.map +1 -1
  61. package/dist/cmd/cloud/resource/list.js +38 -27
  62. package/dist/cmd/cloud/resource/list.js.map +1 -1
  63. package/dist/cmd/cloud/secret/get.d.ts.map +1 -1
  64. package/dist/cmd/cloud/secret/get.js +17 -15
  65. package/dist/cmd/cloud/secret/get.js.map +1 -1
  66. package/dist/cmd/cloud/secret/list.d.ts.map +1 -1
  67. package/dist/cmd/cloud/secret/list.js +24 -20
  68. package/dist/cmd/cloud/secret/list.js.map +1 -1
  69. package/dist/cmd/cloud/session/logs.d.ts.map +1 -1
  70. package/dist/cmd/cloud/session/logs.js +18 -15
  71. package/dist/cmd/cloud/session/logs.js.map +1 -1
  72. package/dist/cmd/dev/agents.d.ts.map +1 -1
  73. package/dist/cmd/dev/agents.js +55 -41
  74. package/dist/cmd/dev/agents.js.map +1 -1
  75. package/dist/cmd/dev/index.d.ts.map +1 -1
  76. package/dist/cmd/dev/index.js +2 -0
  77. package/dist/cmd/dev/index.js.map +1 -1
  78. package/dist/cmd/profile/create.js +1 -1
  79. package/dist/cmd/profile/create.js.map +1 -1
  80. package/dist/cmd/profile/delete.d.ts.map +1 -1
  81. package/dist/cmd/profile/delete.js +1 -1
  82. package/dist/cmd/profile/delete.js.map +1 -1
  83. package/dist/cmd/profile/list.d.ts.map +1 -1
  84. package/dist/cmd/profile/list.js +29 -11
  85. package/dist/cmd/profile/list.js.map +1 -1
  86. package/dist/cmd/profile/show.d.ts.map +1 -1
  87. package/dist/cmd/profile/show.js +7 -10
  88. package/dist/cmd/profile/show.js.map +1 -1
  89. package/dist/cmd/project/delete.js +1 -1
  90. package/dist/cmd/project/delete.js.map +1 -1
  91. package/dist/cmd/version/index.d.ts.map +1 -1
  92. package/dist/cmd/version/index.js +1 -1
  93. package/dist/cmd/version/index.js.map +1 -1
  94. package/dist/tui.d.ts.map +1 -1
  95. package/dist/tui.js +3 -1
  96. package/dist/tui.js.map +1 -1
  97. package/dist/types.d.ts +32 -8
  98. package/dist/types.d.ts.map +1 -1
  99. package/dist/types.js.map +1 -1
  100. package/package.json +3 -3
  101. package/src/cli.ts +109 -8
  102. package/src/cmd/auth/ssh/add.ts +37 -17
  103. package/src/cmd/auth/ssh/delete.ts +36 -23
  104. package/src/cmd/auth/ssh/list.ts +8 -6
  105. package/src/cmd/build/ast.ts +203 -0
  106. package/src/cmd/build/bundler.ts +81 -15
  107. package/src/cmd/build/plugin.ts +92 -4
  108. package/src/cmd/cloud/deployment/list.ts +30 -26
  109. package/src/cmd/cloud/deployment/show.ts +47 -42
  110. package/src/cmd/cloud/env/get.ts +14 -12
  111. package/src/cmd/cloud/env/list.ts +24 -22
  112. package/src/cmd/cloud/keyvalue/get.ts +19 -14
  113. package/src/cmd/cloud/keyvalue/keys.ts +10 -12
  114. package/src/cmd/cloud/keyvalue/list-namespaces.ts +10 -8
  115. package/src/cmd/cloud/keyvalue/search.ts +14 -17
  116. package/src/cmd/cloud/keyvalue/stats.ts +52 -28
  117. package/src/cmd/cloud/objectstore/get.ts +18 -13
  118. package/src/cmd/cloud/objectstore/list-buckets.ts +11 -9
  119. package/src/cmd/cloud/objectstore/list-keys.ts +12 -11
  120. package/src/cmd/cloud/resource/list.ts +33 -23
  121. package/src/cmd/cloud/secret/get.ts +15 -13
  122. package/src/cmd/cloud/secret/list.ts +24 -22
  123. package/src/cmd/cloud/session/logs.ts +18 -17
  124. package/src/cmd/dev/agents.ts +70 -50
  125. package/src/cmd/dev/index.ts +2 -0
  126. package/src/cmd/profile/create.ts +3 -3
  127. package/src/cmd/profile/delete.ts +5 -2
  128. package/src/cmd/profile/list.ts +31 -11
  129. package/src/cmd/profile/show.ts +15 -12
  130. package/src/cmd/project/delete.ts +1 -1
  131. package/src/cmd/version/index.ts +5 -1
  132. package/src/tui.ts +3 -1
  133. package/src/types.ts +32 -10
package/src/cli.ts CHANGED
@@ -51,7 +51,8 @@ function createAPIClient(baseCtx: CommandContext, config: Config | null): APICli
51
51
  async function executeOrValidate(
52
52
  ctx: CommandContext,
53
53
  commandName: string,
54
- handler?: (ctx: CommandContext) => unknown | Promise<unknown>
54
+ handler?: (ctx: CommandContext) => unknown | Promise<unknown>,
55
+ hasResponseSchema?: boolean
55
56
  ): Promise<void> {
56
57
  if (isValidateMode(ctx.options)) {
57
58
  // In validate mode, just output success (validation already passed via Zod)
@@ -62,7 +63,29 @@ async function executeOrValidate(
62
63
  outputValidation(result, ctx.options);
63
64
  } else if (handler) {
64
65
  // Normal execution
65
- await handler(ctx);
66
+ const result = await handler(ctx);
67
+
68
+ // If --json flag is set
69
+ if (ctx.options.json) {
70
+ // If command has a response schema but returned nothing, that's an error
71
+ if (hasResponseSchema && result === undefined) {
72
+ const { createError, exitWithError, ErrorCode } = await import('./errors');
73
+ exitWithError(
74
+ createError(
75
+ ErrorCode.INTERNAL_ERROR,
76
+ `Command '${commandName}' declares a response schema but returned no data. This is a bug in the command implementation.`
77
+ ),
78
+ ctx.logger,
79
+ ctx.options.errorFormat
80
+ );
81
+ }
82
+
83
+ // Output the result as JSON if we have data
84
+ if (result !== undefined) {
85
+ const { outputJSON } = await import('./output');
86
+ outputJSON(result);
87
+ }
88
+ }
66
89
  }
67
90
  }
68
91
 
@@ -593,7 +616,8 @@ async function registerSubcommand(
593
616
  await executeOrValidate(
594
617
  ctx as CommandContext,
595
618
  `${parent.name()} ${subcommand.name}`,
596
- subcommand.handler
619
+ subcommand.handler,
620
+ !!subcommand.schema?.response
597
621
  );
598
622
  } catch (error) {
599
623
  if (error && typeof error === 'object' && 'issues' in error) {
@@ -653,7 +677,32 @@ async function registerSubcommand(
653
677
  }
654
678
  }
655
679
  if (subcommand.handler) {
656
- await subcommand.handler(ctx as CommandContext);
680
+ const result = await subcommand.handler(ctx as CommandContext);
681
+
682
+ // If --json flag is set
683
+ if (baseCtx.options.json) {
684
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
685
+ const hasResponseSchema = !!(subcommand as any).schema?.response;
686
+
687
+ // If command has a response schema but returned nothing, that's an error
688
+ if (hasResponseSchema && result === undefined) {
689
+ const { createError, exitWithError, ErrorCode } = await import('./errors');
690
+ exitWithError(
691
+ createError(
692
+ ErrorCode.INTERNAL_ERROR,
693
+ `Command '${parent.name()} ${subcommand.name}' declares a response schema but returned no data. This is a bug in the command implementation.`
694
+ ),
695
+ baseCtx.logger,
696
+ baseCtx.options.errorFormat
697
+ );
698
+ }
699
+
700
+ // Output the result as JSON if we have data
701
+ if (result !== undefined) {
702
+ const { outputJSON } = await import('./output');
703
+ outputJSON(result);
704
+ }
705
+ }
657
706
  }
658
707
  }
659
708
  } else if (normalized.optionalAuth) {
@@ -740,7 +789,8 @@ async function registerSubcommand(
740
789
  await executeOrValidate(
741
790
  ctx as CommandContext,
742
791
  `${parent.name()} ${subcommand.name}`,
743
- subcommand.handler
792
+ subcommand.handler,
793
+ !!subcommand.schema?.response
744
794
  );
745
795
  } catch (error) {
746
796
  if (error && typeof error === 'object' && 'issues' in error) {
@@ -799,7 +849,32 @@ async function registerSubcommand(
799
849
  }
800
850
  }
801
851
  if (subcommand.handler) {
802
- await subcommand.handler(ctx as CommandContext);
852
+ const result = await subcommand.handler(ctx as CommandContext);
853
+
854
+ // If --json flag is set
855
+ if (baseCtx.options.json) {
856
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
857
+ const hasResponseSchema = !!(subcommand as any).schema?.response;
858
+
859
+ // If command has a response schema but returned nothing, that's an error
860
+ if (hasResponseSchema && result === undefined) {
861
+ const { createError, exitWithError, ErrorCode } = await import('./errors');
862
+ exitWithError(
863
+ createError(
864
+ ErrorCode.INTERNAL_ERROR,
865
+ `Command '${parent.name()} ${subcommand.name}' declares a response schema but returned no data. This is a bug in the command implementation.`
866
+ ),
867
+ baseCtx.logger,
868
+ baseCtx.options.errorFormat
869
+ );
870
+ }
871
+
872
+ // Output the result as JSON if we have data
873
+ if (result !== undefined) {
874
+ const { outputJSON } = await import('./output');
875
+ outputJSON(result);
876
+ }
877
+ }
803
878
  }
804
879
  }
805
880
  } else {
@@ -837,7 +912,8 @@ async function registerSubcommand(
837
912
  await executeOrValidate(
838
913
  ctx as CommandContext,
839
914
  `${parent.name()} ${subcommand.name}`,
840
- subcommand.handler
915
+ subcommand.handler,
916
+ !!subcommand.schema?.response
841
917
  );
842
918
  } catch (error) {
843
919
  if (error && typeof error === 'object' && 'issues' in error) {
@@ -882,7 +958,32 @@ async function registerSubcommand(
882
958
  }
883
959
  }
884
960
  if (subcommand.handler) {
885
- await subcommand.handler(ctx as CommandContext);
961
+ const result = await subcommand.handler(ctx as CommandContext);
962
+
963
+ // If --json flag is set
964
+ if (baseCtx.options.json) {
965
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
966
+ const hasResponseSchema = !!(subcommand as any).schema?.response;
967
+
968
+ // If command has a response schema but returned nothing, that's an error
969
+ if (hasResponseSchema && result === undefined) {
970
+ const { createError, exitWithError, ErrorCode } = await import('./errors');
971
+ exitWithError(
972
+ createError(
973
+ ErrorCode.INTERNAL_ERROR,
974
+ `Command '${parent.name()} ${subcommand.name}' declares a response schema but returned no data. This is a bug in the command implementation.`
975
+ ),
976
+ baseCtx.logger,
977
+ baseCtx.options.errorFormat
978
+ );
979
+ }
980
+
981
+ // Output the result as JSON if we have data
982
+ if (result !== undefined) {
983
+ const { outputJSON } = await import('./output');
984
+ outputJSON(result);
985
+ }
986
+ }
886
987
  }
887
988
  }
888
989
  }
@@ -132,7 +132,7 @@ export const addCommand = createSubcommand({
132
132
  const { logger, apiClient, opts } = ctx;
133
133
 
134
134
  if (!apiClient) {
135
- logger.fatal('API client is not available', ErrorCode.INTERNAL_ERROR);
135
+ return logger.fatal('API client is not available', ErrorCode.INTERNAL_ERROR) as never;
136
136
  }
137
137
 
138
138
  try {
@@ -143,9 +143,9 @@ export const addCommand = createSubcommand({
143
143
  try {
144
144
  publicKey = readFileSync(opts.file, 'utf-8').trim();
145
145
  } catch (error) {
146
- logger.fatal(
146
+ return logger.fatal(
147
147
  `Error reading file: ${error instanceof Error ? error.message : 'Unknown error'}`
148
- );
148
+ ) as never;
149
149
  }
150
150
  } else {
151
151
  const stdin = await readStdinIfPiped();
@@ -157,11 +157,10 @@ export const addCommand = createSubcommand({
157
157
  const discoveredKeys = discoverSSHKeys();
158
158
 
159
159
  if (discoveredKeys.length === 0) {
160
- logger.fatal(
160
+ return logger.fatal(
161
161
  'No SSH public keys found in ~/.ssh/\n' +
162
162
  'Please specify a file with --file or pipe the key via stdin'
163
- );
164
- return;
163
+ ) as never;
165
164
  }
166
165
 
167
166
  // Fetch existing keys from server to filter out already-added ones
@@ -185,14 +184,13 @@ export const addCommand = createSubcommand({
185
184
  console.log('To add a different key:');
186
185
  tui.bullet(`Use ${tui.bold('--file <path>')} to specify a key file`);
187
186
  tui.bullet(`Pipe the key via stdin: ${boldcmd}`);
188
- return;
187
+ return { success: false, fingerprint: '', keyType: '', added: 0 };
189
188
  }
190
189
 
191
190
  if (!process.stdin.isTTY) {
192
- logger.fatal(
191
+ return logger.fatal(
193
192
  'Interactive selection required but cannot prompt in non-TTY environment. Use --file or pipe the key via stdin.'
194
- );
195
- return;
193
+ ) as never;
196
194
  }
197
195
 
198
196
  const response = await enquirer.prompt<{ keys: string[] }>({
@@ -213,13 +211,17 @@ export const addCommand = createSubcommand({
213
211
  if (selectedFingerprints.length === 0) {
214
212
  tui.newline();
215
213
  tui.info('No keys selected');
216
- return;
214
+ return { success: false, fingerprint: '', keyType: '', added: 0 };
217
215
  }
218
216
 
219
217
  // Build Map for O(1) lookups
220
218
  const keyMap = new Map(newKeys.map((k) => [k.fingerprint, k]));
221
219
 
222
220
  // Add all selected keys
221
+ let addedCount = 0;
222
+ let lastFingerprint = '';
223
+ let lastKeyType = '';
224
+
223
225
  for (const fingerprint of selectedFingerprints) {
224
226
  const key = keyMap.get(fingerprint);
225
227
  if (!key) continue;
@@ -232,6 +234,9 @@ export const addCommand = createSubcommand({
232
234
  clearOnSuccess: true,
233
235
  });
234
236
  tui.success(`SSH key added: ${tui.muted(result.fingerprint)}`);
237
+ addedCount++;
238
+ lastFingerprint = result.fingerprint;
239
+ lastKeyType = key.publicKey.split(/\s+/)[0] || 'unknown';
235
240
  } catch (error) {
236
241
  tui.newline();
237
242
  if (error instanceof Error) {
@@ -242,22 +247,27 @@ export const addCommand = createSubcommand({
242
247
  }
243
248
  }
244
249
 
245
- return;
250
+ return {
251
+ success: addedCount > 0,
252
+ fingerprint: lastFingerprint,
253
+ keyType: lastKeyType,
254
+ added: addedCount,
255
+ };
246
256
  }
247
257
  }
248
258
 
249
259
  // Only process single key if we got here (from --file or stdin)
250
260
  if (!publicKey) {
251
- logger.fatal('No public key provided');
261
+ return logger.fatal('No public key provided') as never;
252
262
  }
253
263
 
254
264
  // Validate key format
255
265
  try {
256
266
  computeSSHKeyFingerprint(publicKey);
257
267
  } catch (error) {
258
- logger.fatal(
268
+ return logger.fatal(
259
269
  `Invalid SSH key format: ${error instanceof Error ? error.message : 'Unknown error'}`
260
- );
270
+ ) as never;
261
271
  }
262
272
 
263
273
  const result = await tui.spinner({
@@ -268,12 +278,22 @@ export const addCommand = createSubcommand({
268
278
  });
269
279
 
270
280
  tui.success(`SSH key added: ${tui.muted(result.fingerprint)}`);
281
+
282
+ return {
283
+ success: true,
284
+ fingerprint: result.fingerprint,
285
+ keyType: publicKey.trim().split(/\s+/)[0] || 'unknown',
286
+ added: 1,
287
+ };
271
288
  } catch (error) {
272
289
  logger.trace(error);
273
290
  if (error instanceof Error) {
274
- logger.fatal(`Failed to add SSH key: ${error.message}`, ErrorCode.API_ERROR);
291
+ return logger.fatal(
292
+ `Failed to add SSH key: ${error.message}`,
293
+ ErrorCode.API_ERROR
294
+ ) as never;
275
295
  } else {
276
- logger.fatal('Failed to add SSH key', ErrorCode.API_ERROR);
296
+ return logger.fatal('Failed to add SSH key', ErrorCode.API_ERROR) as never;
277
297
  }
278
298
  }
279
299
  },
@@ -39,7 +39,7 @@ export const deleteCommand = createSubcommand({
39
39
  const { logger, apiClient, args, opts, options } = ctx;
40
40
 
41
41
  if (!apiClient) {
42
- logger.fatal('API client is not available', ErrorCode.INTERNAL_ERROR);
42
+ return logger.fatal('API client is not available', ErrorCode.INTERNAL_ERROR) as never;
43
43
  }
44
44
 
45
45
  const shouldConfirm = process.stdin.isTTY ? opts.confirm : false;
@@ -53,15 +53,17 @@ export const deleteCommand = createSubcommand({
53
53
  const keys = await tui.spinner('Fetching SSH keys...', () => listSSHKeys(apiClient));
54
54
 
55
55
  if (keys.length === 0) {
56
- tui.newline();
57
- tui.info('No SSH keys found');
58
- return;
56
+ if (!options.json) {
57
+ tui.newline();
58
+ tui.info('No SSH keys found');
59
+ }
60
+ return { success: false, removed: 0, fingerprints: [] };
59
61
  }
60
62
 
61
63
  if (!process.stdin.isTTY) {
62
- logger.fatal(
64
+ return logger.fatal(
63
65
  'Interactive selection required but cannot prompt in non-TTY environment. Provide fingerprint as argument.'
64
- );
66
+ ) as never;
65
67
  }
66
68
 
67
69
  tui.newline();
@@ -79,9 +81,11 @@ export const deleteCommand = createSubcommand({
79
81
  fingerprintsToRemove = response.keys;
80
82
 
81
83
  if (fingerprintsToRemove.length === 0) {
82
- tui.newline();
83
- tui.info('No keys selected');
84
- return;
84
+ if (!options.json) {
85
+ tui.newline();
86
+ tui.info('No keys selected');
87
+ }
88
+ return { success: false, removed: 0, fingerprints: [] };
85
89
  }
86
90
  }
87
91
 
@@ -98,7 +102,7 @@ export const deleteCommand = createSubcommand({
98
102
  },
99
103
  options
100
104
  );
101
- return;
105
+ return { success: false, removed: 0, fingerprints: [] };
102
106
  }
103
107
 
104
108
  if (shouldConfirm) {
@@ -109,8 +113,10 @@ export const deleteCommand = createSubcommand({
109
113
  );
110
114
 
111
115
  if (!confirmed) {
112
- tui.info('Cancelled');
113
- return;
116
+ if (!options.json) {
117
+ tui.info('Cancelled');
118
+ }
119
+ return { success: false, removed: 0, fingerprints: [] };
114
120
  }
115
121
  }
116
122
 
@@ -119,11 +125,13 @@ export const deleteCommand = createSubcommand({
119
125
  for (const fingerprint of fingerprintsToRemove) {
120
126
  outputDryRun(`Would remove SSH key: ${fingerprint}`, options);
121
127
  }
122
- tui.newline();
123
- tui.info(
124
- `[DRY RUN] Would remove ${fingerprintsToRemove.length} SSH key${fingerprintsToRemove.length > 1 ? 's' : ''}`
125
- );
126
- return;
128
+ if (!options.json) {
129
+ tui.newline();
130
+ tui.info(
131
+ `[DRY RUN] Would remove ${fingerprintsToRemove.length} SSH key${fingerprintsToRemove.length > 1 ? 's' : ''}`
132
+ );
133
+ }
134
+ return { success: false, removed: 0, fingerprints: [] };
127
135
  }
128
136
 
129
137
  // Actually execute the deletion
@@ -133,10 +141,12 @@ export const deleteCommand = createSubcommand({
133
141
  );
134
142
  }
135
143
 
136
- tui.newline();
137
- tui.success(
138
- `Removed ${fingerprintsToRemove.length} SSH key${fingerprintsToRemove.length > 1 ? 's' : ''}`
139
- );
144
+ if (!options.json) {
145
+ tui.newline();
146
+ tui.success(
147
+ `Removed ${fingerprintsToRemove.length} SSH key${fingerprintsToRemove.length > 1 ? 's' : ''}`
148
+ );
149
+ }
140
150
 
141
151
  return {
142
152
  success: true,
@@ -146,9 +156,12 @@ export const deleteCommand = createSubcommand({
146
156
  } catch (error) {
147
157
  logger.trace(error);
148
158
  if (error instanceof Error) {
149
- logger.fatal(`Failed to remove SSH key: ${error.message}`, ErrorCode.API_ERROR);
159
+ return logger.fatal(
160
+ `Failed to remove SSH key: ${error.message}`,
161
+ ErrorCode.API_ERROR
162
+ ) as never;
150
163
  } else {
151
- logger.fatal('Failed to remove SSH key', ErrorCode.API_ERROR);
164
+ return logger.fatal('Failed to remove SSH key', ErrorCode.API_ERROR) as never;
152
165
  }
153
166
  }
154
167
  },
@@ -7,11 +7,10 @@ import { z } from 'zod';
7
7
 
8
8
  const SSHListResponseSchema = z.array(
9
9
  z.object({
10
- id: z.string().describe('SSH key ID'),
11
10
  fingerprint: z.string().describe('SSH key fingerprint'),
12
11
  keyType: z.string().describe('SSH key type (e.g., ssh-rsa, ssh-ed25519)'),
13
- comment: z.string().optional().describe('SSH key comment'),
14
- createdAt: z.string().optional().describe('Creation timestamp'),
12
+ comment: z.string().describe('SSH key comment'),
13
+ publicKey: z.string().describe('SSH public key'),
15
14
  })
16
15
  );
17
16
 
@@ -34,7 +33,7 @@ export const listCommand = createSubcommand({
34
33
  const { logger, apiClient, options } = ctx;
35
34
 
36
35
  if (!apiClient) {
37
- logger.fatal('API client is not available', ErrorCode.INTERNAL_ERROR);
36
+ return logger.fatal('API client is not available', ErrorCode.INTERNAL_ERROR) as never;
38
37
  }
39
38
 
40
39
  try {
@@ -71,9 +70,12 @@ export const listCommand = createSubcommand({
71
70
  } catch (error) {
72
71
  logger.trace(error);
73
72
  if (error instanceof Error) {
74
- logger.fatal(`Failed to list SSH keys: ${error.message}`, ErrorCode.API_ERROR);
73
+ return logger.fatal(
74
+ `Failed to list SSH keys: ${error.message}`,
75
+ ErrorCode.API_ERROR
76
+ ) as never;
75
77
  } else {
76
- logger.fatal('Failed to list SSH keys', ErrorCode.API_ERROR);
78
+ return logger.fatal('Failed to list SSH keys', ErrorCode.API_ERROR) as never;
77
79
  }
78
80
  }
79
81
  },
@@ -897,3 +897,206 @@ export async function parseRoute(
897
897
  }
898
898
  return routes;
899
899
  }
900
+
901
+ /**
902
+ * Configuration extracted from createWorkbench call
903
+ */
904
+ export interface WorkbenchConfig {
905
+ route: string;
906
+ headers?: Record<string, string>;
907
+ }
908
+
909
+ /**
910
+ * Result of workbench analysis
911
+ */
912
+ export interface WorkbenchAnalysis {
913
+ hasWorkbench: boolean;
914
+ config: WorkbenchConfig | null;
915
+ }
916
+
917
+ /**
918
+ * Check if a TypeScript file actively uses a specific function
919
+ * (ignores comments and unused imports)
920
+ *
921
+ * @param content - The TypeScript source code
922
+ * @param functionName - The function name to check for (e.g., 'createWorkbench')
923
+ * @returns true if the function is both imported and called
924
+ */
925
+ export async function checkFunctionUsage(content: string, functionName: string): Promise<boolean> {
926
+ try {
927
+ const ts = await import('typescript');
928
+ const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true);
929
+
930
+ let hasImport = false;
931
+ let hasUsage = false;
932
+
933
+ function visitNode(node: import('typescript').Node): void {
934
+ // Check for import declarations with the function
935
+ if (ts.isImportDeclaration(node) && node.importClause?.namedBindings) {
936
+ if (ts.isNamedImports(node.importClause.namedBindings)) {
937
+ for (const element of node.importClause.namedBindings.elements) {
938
+ if (element.name.text === functionName) {
939
+ hasImport = true;
940
+ }
941
+ }
942
+ }
943
+ }
944
+ // Check for function calls
945
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
946
+ if (node.expression.text === functionName) {
947
+ hasUsage = true;
948
+ }
949
+ }
950
+ // Recursively visit child nodes
951
+ ts.forEachChild(node, visitNode);
952
+ }
953
+
954
+ visitNode(sourceFile);
955
+ // Only return true if both import and usage are present
956
+ return hasImport && hasUsage;
957
+ } catch (error) {
958
+ // Fallback to string check if AST parsing fails
959
+ console.warn(`AST parsing failed for ${functionName}, falling back to string check:`, error);
960
+ return content.includes(functionName);
961
+ }
962
+ }
963
+
964
+ /**
965
+ * Check if app.ts contains conflicting routes for a given endpoint
966
+ */
967
+ export async function checkRouteConflicts(
968
+ content: string,
969
+ workbenchEndpoint: string
970
+ ): Promise<boolean> {
971
+ try {
972
+ const ts = await import('typescript');
973
+ const sourceFile = ts.createSourceFile('app.ts', content, ts.ScriptTarget.Latest, true);
974
+
975
+ let hasConflict = false;
976
+
977
+ function visitNode(node: import('typescript').Node): void {
978
+ // Check for router.get calls
979
+ if (
980
+ ts.isCallExpression(node) &&
981
+ ts.isPropertyAccessExpression(node.expression) &&
982
+ ts.isIdentifier(node.expression.name) &&
983
+ node.expression.name.text === 'get'
984
+ ) {
985
+ // Check if first argument is the workbench endpoint
986
+ if (node.arguments.length > 0 && ts.isStringLiteral(node.arguments[0])) {
987
+ if (node.arguments[0].text === workbenchEndpoint) {
988
+ hasConflict = true;
989
+ }
990
+ }
991
+ }
992
+
993
+ ts.forEachChild(node, visitNode);
994
+ }
995
+
996
+ visitNode(sourceFile);
997
+ return hasConflict;
998
+ } catch (_error) {
999
+ return false;
1000
+ }
1001
+ }
1002
+
1003
+ /**
1004
+ * Analyze workbench usage and extract configuration
1005
+ *
1006
+ * @param content - The TypeScript source code
1007
+ * @returns workbench analysis including usage and config
1008
+ */
1009
+ export async function analyzeWorkbench(content: string): Promise<WorkbenchAnalysis> {
1010
+ try {
1011
+ const ts = await import('typescript');
1012
+ const sourceFile = ts.createSourceFile('app.ts', content, ts.ScriptTarget.Latest, true);
1013
+
1014
+ let hasImport = false;
1015
+ let hasUsage = false;
1016
+ let config: WorkbenchConfig | null = null;
1017
+
1018
+ function visitNode(node: import('typescript').Node): void {
1019
+ // Check for import declarations with createWorkbench
1020
+ if (ts.isImportDeclaration(node) && node.importClause?.namedBindings) {
1021
+ if (ts.isNamedImports(node.importClause.namedBindings)) {
1022
+ for (const element of node.importClause.namedBindings.elements) {
1023
+ if (element.name.text === 'createWorkbench') {
1024
+ hasImport = true;
1025
+ }
1026
+ }
1027
+ }
1028
+ }
1029
+
1030
+ // Check for createWorkbench function calls and extract config
1031
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
1032
+ if (node.expression.text === 'createWorkbench') {
1033
+ hasUsage = true;
1034
+
1035
+ // Extract configuration from the first argument (if any)
1036
+ if (node.arguments.length > 0) {
1037
+ const configArg = node.arguments[0];
1038
+ config = parseConfigObject(configArg, ts);
1039
+ } else {
1040
+ // Default config if no arguments provided
1041
+ config = { route: '/workbench' };
1042
+ }
1043
+ }
1044
+ }
1045
+
1046
+ // Recursively visit child nodes
1047
+ ts.forEachChild(node, visitNode);
1048
+ }
1049
+
1050
+ visitNode(sourceFile);
1051
+
1052
+ // Set default config if workbench is used but no config was parsed
1053
+ if (hasImport && hasUsage && !config) {
1054
+ config = { route: '/workbench' };
1055
+ }
1056
+
1057
+ return {
1058
+ hasWorkbench: hasImport && hasUsage,
1059
+ config: config,
1060
+ };
1061
+ } catch (error) {
1062
+ // Fallback to simple check if AST parsing fails
1063
+ console.warn('Workbench AST parsing failed, falling back to string check:', error);
1064
+ const hasWorkbench = content.includes('createWorkbench');
1065
+ return {
1066
+ hasWorkbench,
1067
+ config: hasWorkbench ? { route: '/workbench' } : null,
1068
+ };
1069
+ }
1070
+ }
1071
+
1072
+ /**
1073
+ * Parse a TypeScript object literal to extract configuration
1074
+ */
1075
+ function parseConfigObject(
1076
+ node: import('typescript').Node,
1077
+ ts: typeof import('typescript')
1078
+ ): WorkbenchConfig | null {
1079
+ if (!ts.isObjectLiteralExpression(node)) {
1080
+ return { route: '/workbench' }; // Default config
1081
+ }
1082
+
1083
+ const config: WorkbenchConfig = { route: '/workbench' };
1084
+
1085
+ for (const property of node.properties) {
1086
+ if (ts.isPropertyAssignment(property) && ts.isIdentifier(property.name)) {
1087
+ const propertyName = property.name.text;
1088
+
1089
+ if (propertyName === 'route' && ts.isStringLiteral(property.initializer)) {
1090
+ config.route = property.initializer.text;
1091
+ } else if (
1092
+ propertyName === 'headers' &&
1093
+ ts.isObjectLiteralExpression(property.initializer)
1094
+ ) {
1095
+ // Parse headers object if needed (not implemented for now)
1096
+ config.headers = {};
1097
+ }
1098
+ }
1099
+ }
1100
+
1101
+ return config;
1102
+ }