@agentuity/cli 0.0.48 → 0.0.49

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 (256) hide show
  1. package/bin/cli.ts +26 -5
  2. package/dist/banner.d.ts +1 -1
  3. package/dist/banner.d.ts.map +1 -1
  4. package/dist/cli-logger.d.ts +27 -0
  5. package/dist/cli-logger.d.ts.map +1 -0
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cmd/auth/index.d.ts.map +1 -1
  8. package/dist/cmd/auth/login.d.ts.map +1 -1
  9. package/dist/cmd/auth/logout.d.ts.map +1 -1
  10. package/dist/cmd/auth/signup.d.ts.map +1 -1
  11. package/dist/cmd/auth/ssh/add.d.ts.map +1 -1
  12. package/dist/cmd/auth/ssh/delete.d.ts.map +1 -1
  13. package/dist/cmd/auth/ssh/index.d.ts +1 -2
  14. package/dist/cmd/auth/ssh/index.d.ts.map +1 -1
  15. package/dist/cmd/auth/ssh/list.d.ts.map +1 -1
  16. package/dist/cmd/auth/whoami.d.ts.map +1 -1
  17. package/dist/cmd/bundle/ast.d.ts.map +1 -1
  18. package/dist/cmd/bundle/index.d.ts.map +1 -1
  19. package/dist/cmd/bundle/plugin.d.ts.map +1 -1
  20. package/dist/cmd/capabilities/index.d.ts +4 -0
  21. package/dist/cmd/capabilities/index.d.ts.map +1 -0
  22. package/dist/cmd/capabilities/show.d.ts +20 -0
  23. package/dist/cmd/capabilities/show.d.ts.map +1 -0
  24. package/dist/cmd/cloud/deploy.d.ts.map +1 -1
  25. package/dist/cmd/cloud/deployment/index.d.ts.map +1 -1
  26. package/dist/cmd/cloud/deployment/list.d.ts.map +1 -1
  27. package/dist/cmd/cloud/deployment/remove.d.ts.map +1 -1
  28. package/dist/cmd/cloud/deployment/rollback.d.ts.map +1 -1
  29. package/dist/cmd/cloud/deployment/show.d.ts.map +1 -1
  30. package/dist/cmd/cloud/deployment/undeploy.d.ts.map +1 -1
  31. package/dist/cmd/cloud/deployment/utils.d.ts +4 -2
  32. package/dist/cmd/cloud/deployment/utils.d.ts.map +1 -1
  33. package/dist/cmd/cloud/domain.d.ts.map +1 -1
  34. package/dist/cmd/cloud/index.d.ts.map +1 -1
  35. package/dist/cmd/cloud/resource/add.d.ts.map +1 -1
  36. package/dist/cmd/cloud/resource/delete.d.ts.map +1 -1
  37. package/dist/cmd/cloud/resource/index.d.ts +1 -2
  38. package/dist/cmd/cloud/resource/index.d.ts.map +1 -1
  39. package/dist/cmd/cloud/resource/list.d.ts.map +1 -1
  40. package/dist/cmd/cloud/scp/download.d.ts.map +1 -1
  41. package/dist/cmd/cloud/scp/index.d.ts +1 -2
  42. package/dist/cmd/cloud/scp/index.d.ts.map +1 -1
  43. package/dist/cmd/cloud/scp/upload.d.ts.map +1 -1
  44. package/dist/cmd/cloud/ssh.d.ts.map +1 -1
  45. package/dist/cmd/dev/index.d.ts.map +1 -1
  46. package/dist/cmd/env/delete.d.ts.map +1 -1
  47. package/dist/cmd/env/get.d.ts.map +1 -1
  48. package/dist/cmd/env/import.d.ts.map +1 -1
  49. package/dist/cmd/env/index.d.ts.map +1 -1
  50. package/dist/cmd/env/list.d.ts.map +1 -1
  51. package/dist/cmd/env/pull.d.ts.map +1 -1
  52. package/dist/cmd/env/push.d.ts.map +1 -1
  53. package/dist/cmd/env/set.d.ts.map +1 -1
  54. package/dist/cmd/index.d.ts.map +1 -1
  55. package/dist/cmd/kv/create-namespace.d.ts +3 -0
  56. package/dist/cmd/kv/create-namespace.d.ts.map +1 -0
  57. package/dist/cmd/kv/delete-namespace.d.ts +3 -0
  58. package/dist/cmd/kv/delete-namespace.d.ts.map +1 -0
  59. package/dist/cmd/kv/delete.d.ts +3 -0
  60. package/dist/cmd/kv/delete.d.ts.map +1 -0
  61. package/dist/cmd/kv/get.d.ts +3 -0
  62. package/dist/cmd/kv/get.d.ts.map +1 -0
  63. package/dist/cmd/kv/index.d.ts +2 -0
  64. package/dist/cmd/kv/index.d.ts.map +1 -0
  65. package/dist/cmd/kv/keys.d.ts +3 -0
  66. package/dist/cmd/kv/keys.d.ts.map +1 -0
  67. package/dist/cmd/kv/list-namespaces.d.ts +3 -0
  68. package/dist/cmd/kv/list-namespaces.d.ts.map +1 -0
  69. package/dist/cmd/kv/repl.d.ts +3 -0
  70. package/dist/cmd/kv/repl.d.ts.map +1 -0
  71. package/dist/cmd/kv/search.d.ts +3 -0
  72. package/dist/cmd/kv/search.d.ts.map +1 -0
  73. package/dist/cmd/kv/set.d.ts +3 -0
  74. package/dist/cmd/kv/set.d.ts.map +1 -0
  75. package/dist/cmd/kv/stats.d.ts +3 -0
  76. package/dist/cmd/kv/stats.d.ts.map +1 -0
  77. package/dist/cmd/kv/util.d.ts +8 -0
  78. package/dist/cmd/kv/util.d.ts.map +1 -0
  79. package/dist/cmd/objectstore/delete-bucket.d.ts +3 -0
  80. package/dist/cmd/objectstore/delete-bucket.d.ts.map +1 -0
  81. package/dist/cmd/objectstore/delete.d.ts +3 -0
  82. package/dist/cmd/objectstore/delete.d.ts.map +1 -0
  83. package/dist/cmd/objectstore/get.d.ts +3 -0
  84. package/dist/cmd/objectstore/get.d.ts.map +1 -0
  85. package/dist/cmd/objectstore/index.d.ts +2 -0
  86. package/dist/cmd/objectstore/index.d.ts.map +1 -0
  87. package/dist/cmd/objectstore/list-buckets.d.ts +3 -0
  88. package/dist/cmd/objectstore/list-buckets.d.ts.map +1 -0
  89. package/dist/cmd/objectstore/list-keys.d.ts +3 -0
  90. package/dist/cmd/objectstore/list-keys.d.ts.map +1 -0
  91. package/dist/cmd/objectstore/put.d.ts +3 -0
  92. package/dist/cmd/objectstore/put.d.ts.map +1 -0
  93. package/dist/cmd/objectstore/repl.d.ts +3 -0
  94. package/dist/cmd/objectstore/repl.d.ts.map +1 -0
  95. package/dist/cmd/objectstore/url.d.ts +3 -0
  96. package/dist/cmd/objectstore/url.d.ts.map +1 -0
  97. package/dist/cmd/objectstore/util.d.ts +8 -0
  98. package/dist/cmd/objectstore/util.d.ts.map +1 -0
  99. package/dist/cmd/profile/create.d.ts.map +1 -1
  100. package/dist/cmd/profile/delete.d.ts.map +1 -1
  101. package/dist/cmd/profile/index.d.ts.map +1 -1
  102. package/dist/cmd/profile/list.d.ts +1 -2
  103. package/dist/cmd/profile/list.d.ts.map +1 -1
  104. package/dist/cmd/profile/show.d.ts.map +1 -1
  105. package/dist/cmd/profile/use.d.ts.map +1 -1
  106. package/dist/cmd/project/create.d.ts.map +1 -1
  107. package/dist/cmd/project/delete.d.ts.map +1 -1
  108. package/dist/cmd/project/index.d.ts.map +1 -1
  109. package/dist/cmd/project/list.d.ts.map +1 -1
  110. package/dist/cmd/project/show.d.ts.map +1 -1
  111. package/dist/cmd/project/template-flow.d.ts +1 -1
  112. package/dist/cmd/project/template-flow.d.ts.map +1 -1
  113. package/dist/cmd/prompt/index.d.ts +4 -0
  114. package/dist/cmd/prompt/index.d.ts.map +1 -0
  115. package/dist/cmd/prompt/llm.d.ts +3 -0
  116. package/dist/cmd/prompt/llm.d.ts.map +1 -0
  117. package/dist/cmd/repl/index.d.ts +3 -0
  118. package/dist/cmd/repl/index.d.ts.map +1 -0
  119. package/dist/cmd/schema/index.d.ts +4 -0
  120. package/dist/cmd/schema/index.d.ts.map +1 -0
  121. package/dist/cmd/schema/show.d.ts +3 -0
  122. package/dist/cmd/schema/show.d.ts.map +1 -0
  123. package/dist/cmd/secret/delete.d.ts.map +1 -1
  124. package/dist/cmd/secret/get.d.ts.map +1 -1
  125. package/dist/cmd/secret/import.d.ts.map +1 -1
  126. package/dist/cmd/secret/index.d.ts.map +1 -1
  127. package/dist/cmd/secret/list.d.ts.map +1 -1
  128. package/dist/cmd/secret/pull.d.ts.map +1 -1
  129. package/dist/cmd/secret/push.d.ts.map +1 -1
  130. package/dist/cmd/secret/set.d.ts.map +1 -1
  131. package/dist/cmd/version/index.d.ts.map +1 -1
  132. package/dist/config.d.ts +1 -1
  133. package/dist/config.d.ts.map +1 -1
  134. package/dist/errors.d.ts +83 -0
  135. package/dist/errors.d.ts.map +1 -0
  136. package/dist/explain.d.ts +47 -0
  137. package/dist/explain.d.ts.map +1 -0
  138. package/dist/index.d.ts +6 -0
  139. package/dist/index.d.ts.map +1 -1
  140. package/dist/json.d.ts +3 -0
  141. package/dist/json.d.ts.map +1 -0
  142. package/dist/output.d.ts +136 -0
  143. package/dist/output.d.ts.map +1 -0
  144. package/dist/repl.d.ts +124 -0
  145. package/dist/repl.d.ts.map +1 -0
  146. package/dist/schema-generator.d.ts +67 -0
  147. package/dist/schema-generator.d.ts.map +1 -0
  148. package/dist/tui.d.ts +11 -1
  149. package/dist/tui.d.ts.map +1 -1
  150. package/dist/types.d.ts +65 -6
  151. package/dist/types.d.ts.map +1 -1
  152. package/package.json +9 -4
  153. package/src/banner.ts +7 -7
  154. package/src/cli-logger.ts +80 -0
  155. package/src/cli.ts +186 -54
  156. package/src/cmd/auth/index.ts +1 -0
  157. package/src/cmd/auth/login.ts +7 -2
  158. package/src/cmd/auth/logout.ts +4 -0
  159. package/src/cmd/auth/signup.ts +7 -2
  160. package/src/cmd/auth/ssh/add.ts +20 -3
  161. package/src/cmd/auth/ssh/delete.ts +57 -4
  162. package/src/cmd/auth/ssh/index.ts +4 -3
  163. package/src/cmd/auth/ssh/list.ts +29 -12
  164. package/src/cmd/auth/whoami.ts +32 -21
  165. package/src/cmd/bundle/ast.ts +27 -5
  166. package/src/cmd/bundle/index.ts +20 -0
  167. package/src/cmd/bundle/plugin.ts +36 -12
  168. package/src/cmd/capabilities/index.ts +12 -0
  169. package/src/cmd/capabilities/show.ts +256 -0
  170. package/src/cmd/cloud/deploy.ts +54 -0
  171. package/src/cmd/cloud/deployment/index.ts +1 -0
  172. package/src/cmd/cloud/deployment/list.ts +50 -3
  173. package/src/cmd/cloud/deployment/remove.ts +26 -2
  174. package/src/cmd/cloud/deployment/rollback.ts +35 -4
  175. package/src/cmd/cloud/deployment/show.ts +37 -2
  176. package/src/cmd/cloud/deployment/undeploy.ts +12 -1
  177. package/src/cmd/cloud/deployment/utils.ts +5 -2
  178. package/src/cmd/cloud/domain.ts +3 -2
  179. package/src/cmd/cloud/index.ts +8 -1
  180. package/src/cmd/cloud/resource/add.ts +19 -0
  181. package/src/cmd/cloud/resource/delete.ts +24 -3
  182. package/src/cmd/cloud/resource/index.ts +4 -3
  183. package/src/cmd/cloud/resource/list.ts +36 -10
  184. package/src/cmd/cloud/scp/download.ts +27 -1
  185. package/src/cmd/cloud/scp/index.ts +4 -3
  186. package/src/cmd/cloud/scp/upload.ts +27 -1
  187. package/src/cmd/cloud/ssh.ts +12 -0
  188. package/src/cmd/dev/index.ts +11 -7
  189. package/src/cmd/dev/templates.ts +1 -1
  190. package/src/cmd/env/delete.ts +17 -0
  191. package/src/cmd/env/get.ts +17 -1
  192. package/src/cmd/env/import.ts +47 -3
  193. package/src/cmd/env/index.ts +1 -0
  194. package/src/cmd/env/list.ts +13 -1
  195. package/src/cmd/env/pull.ts +20 -0
  196. package/src/cmd/env/push.ts +33 -1
  197. package/src/cmd/env/set.ts +25 -1
  198. package/src/cmd/index.ts +9 -2
  199. package/src/cmd/kv/create-namespace.ts +45 -0
  200. package/src/cmd/kv/delete-namespace.ts +73 -0
  201. package/src/cmd/kv/delete.ts +51 -0
  202. package/src/cmd/kv/get.ts +65 -0
  203. package/src/cmd/kv/index.ts +31 -0
  204. package/src/cmd/kv/keys.ts +57 -0
  205. package/src/cmd/kv/list-namespaces.ts +43 -0
  206. package/src/cmd/kv/repl.ts +284 -0
  207. package/src/cmd/kv/search.ts +80 -0
  208. package/src/cmd/kv/set.ts +63 -0
  209. package/src/cmd/kv/stats.ts +96 -0
  210. package/src/cmd/kv/util.ts +32 -0
  211. package/src/cmd/objectstore/delete-bucket.ts +72 -0
  212. package/src/cmd/objectstore/delete.ts +59 -0
  213. package/src/cmd/objectstore/get.ts +64 -0
  214. package/src/cmd/objectstore/index.ts +27 -0
  215. package/src/cmd/objectstore/list-buckets.ts +45 -0
  216. package/src/cmd/objectstore/list-keys.ts +60 -0
  217. package/src/cmd/objectstore/put.ts +62 -0
  218. package/src/cmd/objectstore/repl.ts +235 -0
  219. package/src/cmd/objectstore/url.ts +59 -0
  220. package/src/cmd/objectstore/util.ts +28 -0
  221. package/src/cmd/profile/create.ts +28 -2
  222. package/src/cmd/profile/delete.ts +17 -2
  223. package/src/cmd/profile/index.ts +1 -0
  224. package/src/cmd/profile/list.ts +7 -3
  225. package/src/cmd/profile/show.ts +20 -5
  226. package/src/cmd/profile/use.ts +8 -0
  227. package/src/cmd/project/create.ts +31 -0
  228. package/src/cmd/project/delete.ts +24 -2
  229. package/src/cmd/project/index.ts +1 -0
  230. package/src/cmd/project/list.ts +23 -9
  231. package/src/cmd/project/show.ts +27 -8
  232. package/src/cmd/project/template-flow.ts +10 -6
  233. package/src/cmd/prompt/index.ts +12 -0
  234. package/src/cmd/prompt/llm.ts +368 -0
  235. package/src/cmd/repl/index.ts +477 -0
  236. package/src/cmd/schema/index.ts +12 -0
  237. package/src/cmd/schema/show.ts +27 -0
  238. package/src/cmd/secret/delete.ts +17 -0
  239. package/src/cmd/secret/get.ts +20 -1
  240. package/src/cmd/secret/import.ts +45 -2
  241. package/src/cmd/secret/index.ts +1 -0
  242. package/src/cmd/secret/list.ts +10 -1
  243. package/src/cmd/secret/pull.ts +20 -0
  244. package/src/cmd/secret/push.ts +33 -1
  245. package/src/cmd/secret/set.ts +20 -0
  246. package/src/cmd/version/index.ts +15 -2
  247. package/src/config.ts +17 -4
  248. package/src/errors.ts +222 -0
  249. package/src/explain.ts +126 -0
  250. package/src/index.ts +51 -0
  251. package/src/json.ts +28 -0
  252. package/src/output.ts +307 -0
  253. package/src/repl.ts +1517 -0
  254. package/src/schema-generator.ts +389 -0
  255. package/src/tui.ts +84 -13
  256. package/src/types.ts +62 -12
package/src/cli.ts CHANGED
@@ -9,6 +9,7 @@ import type {
9
9
  Optional,
10
10
  Logger,
11
11
  AuthData,
12
+ GlobalOptions,
12
13
  } from './types';
13
14
  import { showBanner } from './banner';
14
15
  import { requireAuth, optionalAuth, requireOrg, optionalOrg as selectOptionalOrg } from './auth';
@@ -18,6 +19,9 @@ import * as tui from './tui';
18
19
  import { parseArgsSchema, parseOptionsSchema, buildValidationInput } from './schema-parser';
19
20
  import { defaultProfileName, loadProjectConfig } from './config';
20
21
  import { APIClient, getAPIBaseURL, type APIClient as APIClientType } from './api';
22
+ import { ErrorCode, ExitCode, createError, exitWithError } from './errors';
23
+ import { getCommand } from './command-prefix';
24
+ import { isValidateMode, outputValidation, type ValidationResult } from './output';
21
25
 
22
26
  function createAPIClient(baseCtx: CommandContext, config: Config | null): APIClient {
23
27
  try {
@@ -41,6 +45,71 @@ function createAPIClient(baseCtx: CommandContext, config: Config | null): APICli
41
45
  }
42
46
  }
43
47
 
48
+ /**
49
+ * Execute handler or output validation result based on mode
50
+ */
51
+ async function executeOrValidate(
52
+ ctx: CommandContext,
53
+ commandName: string,
54
+ handler?: (ctx: CommandContext) => unknown | Promise<unknown>
55
+ ): Promise<void> {
56
+ if (isValidateMode(ctx.options)) {
57
+ // In validate mode, just output success (validation already passed via Zod)
58
+ const result: ValidationResult = {
59
+ valid: true,
60
+ command: commandName,
61
+ };
62
+ outputValidation(result, ctx.options);
63
+ } else if (handler) {
64
+ // Normal execution
65
+ await handler(ctx);
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Handle validation error - output structured result in validate mode, otherwise log and exit
71
+ */
72
+ function handleValidationError(
73
+ error: unknown,
74
+ commandName: string,
75
+ baseCtx: { options: GlobalOptions; logger: Logger }
76
+ ): never {
77
+ if (error && typeof error === 'object' && 'issues' in error) {
78
+ const issues = (error as { issues: Array<{ path: string[]; message: string }> }).issues;
79
+
80
+ if (isValidateMode(baseCtx.options)) {
81
+ // In validate mode, output structured validation result
82
+ const result: ValidationResult = {
83
+ valid: false,
84
+ command: commandName,
85
+ errors: issues.map((issue) => ({
86
+ field: issue.path?.length ? issue.path.join('.') : 'unknown',
87
+ message: issue.message,
88
+ })),
89
+ };
90
+ outputValidation(result, baseCtx.options);
91
+ process.exit(ExitCode.VALIDATION_ERROR);
92
+ } else {
93
+ // Use centralized error handling
94
+ exitWithError(
95
+ {
96
+ code: ErrorCode.VALIDATION_FAILED,
97
+ message: 'Validation error',
98
+ details: {
99
+ issues: issues.map((issue) => ({
100
+ field: issue.path?.length ? issue.path.join('.') : 'unknown',
101
+ message: issue.message,
102
+ })),
103
+ },
104
+ },
105
+ baseCtx.logger,
106
+ baseCtx.options.errorFormat ?? 'text'
107
+ );
108
+ }
109
+ }
110
+ throw error;
111
+ }
112
+
44
113
  type Normalized = {
45
114
  requiresAuth: boolean;
46
115
  optionalAuth: false | string;
@@ -97,7 +166,12 @@ function normalizeReqs(def: CommandDefinition | SubcommandDefinition): Normalize
97
166
  };
98
167
  }
99
168
 
100
- function handleProjectConfigError(error: unknown, requiresProject: boolean, logger: Logger): never {
169
+ function handleProjectConfigError(
170
+ error: unknown,
171
+ requiresProject: boolean,
172
+ logger: Logger,
173
+ errorFormat?: 'json' | 'text'
174
+ ): never {
101
175
  if (
102
176
  requiresProject &&
103
177
  error &&
@@ -105,8 +179,14 @@ function handleProjectConfigError(error: unknown, requiresProject: boolean, logg
105
179
  'name' in error &&
106
180
  error.name === 'ProjectConfigNotFoundExpection'
107
181
  ) {
108
- logger.fatal(
109
- 'invalid project folder. use --dir to specify a different directory or change to a project folder'
182
+ exitWithError(
183
+ createError(ErrorCode.PROJECT_NOT_FOUND, 'Invalid project folder', undefined, [
184
+ 'Use --dir to specify a different directory',
185
+ 'Change to a directory containing agentuity.json',
186
+ `Run "${getCommand('project init')}" to create a new project`,
187
+ ]),
188
+ logger,
189
+ errorFormat ?? 'text'
110
190
  );
111
191
  }
112
192
  throw error;
@@ -117,11 +197,11 @@ export async function createCLI(version: string): Promise<Command> {
117
197
 
118
198
  program
119
199
  .name('agentuity')
120
- .description('Agentuity CLI')
121
200
  .version(version, '-V, --version', 'Display version')
122
- .helpOption('-h, --help', 'Display help')
201
+ .helpOption('-h, --help=[json]', 'Display help (with optional JSON output)')
123
202
  .allowUnknownOption(false)
124
- .allowExcessArguments(false);
203
+ .allowExcessArguments(false)
204
+ .showHelpAfterError(true);
125
205
 
126
206
  program
127
207
  .option('--config <path>', 'Config file path')
@@ -133,7 +213,15 @@ export async function createCLI(version: string): Promise<Command> {
133
213
  'Use a specific organization when performing operations',
134
214
  process.env.AGENTUITY_CLOUD_ORG_ID
135
215
  )
136
- .option('--color-scheme <scheme>', 'Color scheme: light or dark');
216
+ .option('--color-scheme <scheme>', 'Color scheme: light or dark')
217
+ .option('--color <mode>', 'Color output: auto, always, never', 'auto')
218
+ .option('--error-format <format>', 'Error output format: json or text', 'text')
219
+ .option('--json', 'Output in JSON format (machine-readable)', false)
220
+ .option('--quiet', 'Suppress non-essential output', false)
221
+ .option('--no-progress', 'Disable progress indicators', false)
222
+ .option('--explain', 'Show what the command would do without executing', false)
223
+ .option('--dry-run', 'Execute command without making changes', false)
224
+ .option('--validate', 'Validate arguments and options without executing', false);
137
225
 
138
226
  const skipVersionCheckOption = program.createOption(
139
227
  '--skip-version-check',
@@ -160,7 +248,7 @@ export async function createCLI(version: string): Promise<Command> {
160
248
  });
161
249
  }
162
250
  console.error();
163
- console.error(`Run 'agentuity --help' for usage information.`);
251
+ console.error(`Run '${getCommand('--help')}' for usage information.`);
164
252
  process.exit(1);
165
253
  });
166
254
 
@@ -173,7 +261,7 @@ export async function createCLI(version: string): Promise<Command> {
173
261
  const match = str.match(/got (\d+)/);
174
262
  if (match) {
175
263
  write(`error: unknown command or subcommand\n`);
176
- write(`\nRun 'agentuity --help' for available commands.\n`);
264
+ write(`\nRun '${getCommand('--help')}' for available commands.\n`);
177
265
  } else {
178
266
  write(str);
179
267
  }
@@ -219,7 +307,17 @@ async function resolveRegion(opts: ResolveRegionOptions): Promise<string | undef
219
307
  // No regions available
220
308
  if (regions.length === 0) {
221
309
  if (required) {
222
- logger.fatal('No cloud regions available');
310
+ const errorFormat = (options as Record<string, unknown>).errorFormat as
311
+ | 'json'
312
+ | 'text'
313
+ | undefined;
314
+ exitWithError(
315
+ createError(ErrorCode.NO_REGIONS_AVAILABLE, 'No cloud regions available', undefined, [
316
+ 'Contact support if you need access to cloud regions',
317
+ ]),
318
+ logger,
319
+ errorFormat ?? 'text'
320
+ );
223
321
  }
224
322
  return undefined;
225
323
  }
@@ -231,8 +329,19 @@ async function resolveRegion(opts: ResolveRegionOptions): Promise<string | undef
231
329
  if (region) {
232
330
  const found = regions.find((r) => r.region === region);
233
331
  if (!found) {
234
- logger.fatal(
235
- `Invalid region '${region}'. Use one of: ${regions.map((r) => r.region).join(', ')}`
332
+ const errorFormat = (options as Record<string, unknown>).errorFormat as
333
+ | 'json'
334
+ | 'text'
335
+ | undefined;
336
+ exitWithError(
337
+ createError(
338
+ ErrorCode.REGION_NOT_FOUND,
339
+ `Invalid region '${region}'`,
340
+ { region, availableRegions: regions.map((r) => r.region) },
341
+ [`Use one of: ${regions.map((r) => r.region).join(', ')}`]
342
+ ),
343
+ logger,
344
+ errorFormat ?? 'text'
236
345
  );
237
346
  }
238
347
  return region;
@@ -249,7 +358,20 @@ async function resolveRegion(opts: ResolveRegionOptions): Promise<string | undef
249
358
 
250
359
  // No flag provided - handle TTY vs non-TTY
251
360
  if (required && !process.stdin.isTTY) {
252
- logger.fatal('--region flag is required in non-interactive mode');
361
+ const errorFormat = (options as Record<string, unknown>).errorFormat as
362
+ | 'json'
363
+ | 'text'
364
+ | undefined;
365
+ exitWithError(
366
+ createError(
367
+ ErrorCode.REGION_REQUIRED,
368
+ '--region flag is required in non-interactive mode',
369
+ { availableRegions: regions.map((r) => r.region) },
370
+ [`Use --region with one of: ${regions.map((r) => r.region).join(', ')}`]
371
+ ),
372
+ logger,
373
+ errorFormat ?? 'text'
374
+ );
253
375
  }
254
376
 
255
377
  if (process.stdin.isTTY) {
@@ -274,6 +396,13 @@ async function registerSubcommand(
274
396
  cmd.aliases(subcommand.aliases);
275
397
  }
276
398
 
399
+ // Add examples to help text
400
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
401
+ const examples = (subcommand as any).examples as string[] | undefined;
402
+ if (examples && examples.length > 0) {
403
+ cmd.addHelpText('after', '\nExamples:\n' + examples.map((ex) => ` ${ex}`).join('\n'));
404
+ }
405
+
277
406
  // Check if this subcommand has its own subcommands (nested subcommands)
278
407
  const subDef = subcommand as unknown as { subcommands?: SubcommandDefinition[] };
279
408
  if (subDef.subcommands && subDef.subcommands.length > 0) {
@@ -368,8 +497,14 @@ async function registerSubcommand(
368
497
  'name' in error &&
369
498
  error.name === 'ProjectConfigNotFoundExpection'
370
499
  ) {
371
- baseCtx.logger.fatal(
372
- 'invalid project folder. use --dir to specify a different directory or change to a project folder'
500
+ exitWithError(
501
+ createError(ErrorCode.PROJECT_NOT_FOUND, 'Invalid project folder', undefined, [
502
+ 'Use --dir to specify a different directory',
503
+ 'Change to a directory containing agentuity.json',
504
+ `Run "${getCommand('project init')}" to create a new project`,
505
+ ]),
506
+ baseCtx.logger,
507
+ baseCtx.options.errorFormat
373
508
  );
374
509
  }
375
510
  throw error;
@@ -448,22 +583,21 @@ async function registerSubcommand(
448
583
  ctx.region = region;
449
584
  }
450
585
  }
451
- if (subcommand.handler) {
452
- await subcommand.handler(ctx as CommandContext);
453
- }
586
+ await executeOrValidate(
587
+ ctx as CommandContext,
588
+ `${parent.name()} ${subcommand.name}`,
589
+ subcommand.handler
590
+ );
454
591
  } catch (error) {
455
592
  if (error && typeof error === 'object' && 'issues' in error) {
456
- baseCtx.logger.error('Validation error:');
457
- const issues = (error as { issues: Array<{ path: string[]; message: string }> })
458
- .issues;
459
- for (const issue of issues) {
460
- baseCtx.logger.error(
461
- ` ${issue.path?.length ? issue.path.join('.') + ': ' : ''}${issue.message}`
462
- );
463
- }
464
- process.exit(1);
593
+ handleValidationError(error, `${parent.name()} ${subcommand.name}`, baseCtx);
465
594
  }
466
- handleProjectConfigError(error, normalized.requiresProject, baseCtx.logger);
595
+ handleProjectConfigError(
596
+ error,
597
+ normalized.requiresProject,
598
+ baseCtx.logger,
599
+ baseCtx.options.errorFormat
600
+ );
467
601
  }
468
602
  } else {
469
603
  const ctx: Record<string, unknown> = {
@@ -596,22 +730,21 @@ async function registerSubcommand(
596
730
  ctx.region = region;
597
731
  }
598
732
  }
599
- if (subcommand.handler) {
600
- await subcommand.handler(ctx as CommandContext);
601
- }
733
+ await executeOrValidate(
734
+ ctx as CommandContext,
735
+ `${parent.name()} ${subcommand.name}`,
736
+ subcommand.handler
737
+ );
602
738
  } catch (error) {
603
739
  if (error && typeof error === 'object' && 'issues' in error) {
604
- baseCtx.logger.error('Validation error:');
605
- const issues = (error as { issues: Array<{ path: string[]; message: string }> })
606
- .issues;
607
- for (const issue of issues) {
608
- baseCtx.logger.error(
609
- ` ${issue.path?.length ? issue.path.join('.') + ': ' : ''}${issue.message}`
610
- );
611
- }
612
- process.exit(1);
740
+ handleValidationError(error, `${parent.name()} ${subcommand.name}`, baseCtx);
613
741
  }
614
- handleProjectConfigError(error, normalized.requiresProject, baseCtx.logger);
742
+ handleProjectConfigError(
743
+ error,
744
+ normalized.requiresProject,
745
+ baseCtx.logger,
746
+ baseCtx.options.errorFormat
747
+ );
615
748
  }
616
749
  } else {
617
750
  const ctx: Record<string, unknown> = {
@@ -694,22 +827,21 @@ async function registerSubcommand(
694
827
  ctx as CommandContext & { apiClient: APIClientType }
695
828
  );
696
829
  }
697
- if (subcommand.handler) {
698
- await subcommand.handler(ctx as CommandContext);
699
- }
830
+ await executeOrValidate(
831
+ ctx as CommandContext,
832
+ `${parent.name()} ${subcommand.name}`,
833
+ subcommand.handler
834
+ );
700
835
  } catch (error) {
701
836
  if (error && typeof error === 'object' && 'issues' in error) {
702
- baseCtx.logger.error('Validation error:');
703
- const issues = (error as { issues: Array<{ path: string[]; message: string }> })
704
- .issues;
705
- for (const issue of issues) {
706
- baseCtx.logger.error(
707
- ` ${issue.path?.length ? issue.path.join('.') + ': ' : ''}${issue.message}`
708
- );
709
- }
710
- process.exit(1);
837
+ handleValidationError(error, `${parent.name()} ${subcommand.name}`, baseCtx);
711
838
  }
712
- handleProjectConfigError(error, normalized.requiresProject, baseCtx.logger);
839
+ handleProjectConfigError(
840
+ error,
841
+ normalized.requiresProject,
842
+ baseCtx.logger,
843
+ baseCtx.options.errorFormat
844
+ );
713
845
  }
714
846
  } else {
715
847
  const ctx: Record<string, unknown> = {
@@ -8,5 +8,6 @@ import { sshSubcommand } from './ssh';
8
8
  export const command = createCommand({
9
9
  name: 'auth',
10
10
  description: 'Authentication and authorization related commands',
11
+ tags: ['read-only', 'fast'],
11
12
  subcommands: [loginCommand, logoutCommand, signupCommand, whoamiCommand, sshSubcommand],
12
13
  });
@@ -4,12 +4,17 @@ import { getAppBaseURL } from '../../api';
4
4
  import { saveAuth } from '../../config';
5
5
  import { generateLoginOTP, pollForLoginCompletion } from './api';
6
6
  import * as tui from '../../tui';
7
+ import { getCommand } from '../../command-prefix';
8
+ import { ErrorCode } from '../../errors';
7
9
 
8
10
  export const loginCommand = createSubcommand({
9
11
  name: 'login',
10
12
  description: 'Login to the Agentuity Platform using a browser-based authentication flow',
13
+ tags: ['mutating', 'creates-resource', 'slow', 'api-intensive'],
11
14
  toplevel: true,
15
+ idempotent: false,
12
16
  requires: { apiClient: true },
17
+ examples: [getCommand('auth login'), getCommand('login')],
13
18
  async handler(ctx) {
14
19
  const { logger, config, apiClient } = ctx;
15
20
 
@@ -78,9 +83,9 @@ export const loginCommand = createSubcommand({
78
83
  error.issues.map((i) => tui.arrow(`${i.message} for ${i.path}`));
79
84
  process.exit(1);
80
85
  } else if (error instanceof Error) {
81
- logger.fatal(`Login failed: ${error.message}`);
86
+ logger.fatal(`Login failed: ${error.message}`, ErrorCode.AUTH_FAILED);
82
87
  } else {
83
- logger.fatal('Login failed');
88
+ logger.fatal('Login failed', ErrorCode.AUTH_FAILED);
84
89
  }
85
90
  }
86
91
  },
@@ -1,11 +1,15 @@
1
1
  import { createSubcommand } from '../../types';
2
2
  import { clearAuth } from '../../config';
3
3
  import * as tui from '../../tui';
4
+ import { getCommand } from '../../command-prefix';
4
5
 
5
6
  export const logoutCommand = createSubcommand({
6
7
  name: 'logout',
7
8
  description: 'Logout of the Agentuity Cloud Platform',
9
+ tags: ['mutating', 'deletes-resource', 'fast', 'requires-auth'],
8
10
  toplevel: true,
11
+ idempotent: false,
12
+ examples: [getCommand('auth logout'), getCommand('logout')],
9
13
 
10
14
  async handler() {
11
15
  await clearAuth();
@@ -3,12 +3,17 @@ 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';
6
+ import { getCommand } from '../../command-prefix';
7
+ import { ErrorCode } from '../../errors';
6
8
 
7
9
  export const signupCommand = createSubcommand({
8
10
  name: 'signup',
9
11
  description: 'Create a new Agentuity Cloud Platform account',
12
+ tags: ['mutating', 'creates-resource', 'slow', 'api-intensive'],
10
13
  toplevel: true,
14
+ idempotent: false,
11
15
  requires: { apiClient: true },
16
+ examples: [getCommand('auth signup'), getCommand('signup')],
12
17
 
13
18
  async handler(ctx) {
14
19
  const { logger, config, apiClient } = ctx;
@@ -46,9 +51,9 @@ export const signupCommand = createSubcommand({
46
51
  tui.banner('CLI Upgrade Required', bannerBody);
47
52
  process.exit(1);
48
53
  } else if (error instanceof Error) {
49
- logger.fatal(`Signup failed: ${error.message}`);
54
+ logger.fatal(`Signup failed: ${error.message}`, ErrorCode.AUTH_FAILED);
50
55
  } else {
51
- logger.fatal('Signup failed');
56
+ logger.fatal('Signup failed', ErrorCode.AUTH_FAILED);
52
57
  }
53
58
  }
54
59
  },
@@ -7,11 +7,19 @@ import { readFileSync, readdirSync, statSync } from 'fs';
7
7
  import { join } from 'path';
8
8
  import { homedir } from 'os';
9
9
  import { z } from 'zod';
10
+ import { ErrorCode } from '../../../errors';
10
11
 
11
12
  const optionsSchema = z.object({
12
13
  file: z.string().optional().describe('File containing the public key'),
13
14
  });
14
15
 
16
+ const SSHAddResponseSchema = z.object({
17
+ success: z.boolean().describe('Whether the operation succeeded'),
18
+ fingerprint: z.string().describe('SSH key fingerprint'),
19
+ keyType: z.string().describe('SSH key type (e.g., ssh-rsa, ssh-ed25519)'),
20
+ added: z.number().describe('Number of keys added'),
21
+ });
22
+
15
23
  interface SSHKeyOption {
16
24
  path: string;
17
25
  filename: string;
@@ -107,15 +115,24 @@ export const addCommand = createSubcommand({
107
115
  name: 'add',
108
116
  aliases: ['create'],
109
117
  description: 'Add an SSH public key to your account (reads from file or stdin)',
118
+ tags: ['mutating', 'creates-resource', 'slow', 'requires-auth'],
119
+ idempotent: false,
110
120
  requires: { apiClient: true, auth: true },
121
+ examples: [
122
+ getCommand('auth ssh add'),
123
+ getCommand('auth ssh add --file ~/.ssh/id_ed25519.pub'),
124
+ getCommand('auth ssh add --file ./deploy_key.pub'),
125
+ 'cat ~/.ssh/id_rsa.pub | ' + getCommand('auth ssh add'),
126
+ ],
111
127
  schema: {
112
128
  options: optionsSchema,
129
+ response: SSHAddResponseSchema,
113
130
  },
114
131
  async handler(ctx) {
115
132
  const { logger, apiClient, opts } = ctx;
116
133
 
117
134
  if (!apiClient) {
118
- logger.fatal('API client is not available');
135
+ logger.fatal('API client is not available', ErrorCode.INTERNAL_ERROR);
119
136
  }
120
137
 
121
138
  try {
@@ -254,9 +271,9 @@ export const addCommand = createSubcommand({
254
271
  } catch (error) {
255
272
  logger.trace(error);
256
273
  if (error instanceof Error) {
257
- logger.fatal(`Failed to add SSH key: ${error.message}`);
274
+ logger.fatal(`Failed to add SSH key: ${error.message}`, ErrorCode.API_ERROR);
258
275
  } else {
259
- logger.fatal('Failed to add SSH key');
276
+ logger.fatal('Failed to add SSH key', ErrorCode.API_ERROR);
260
277
  }
261
278
  }
262
279
  },
@@ -3,12 +3,29 @@ import { removeSSHKey, listSSHKeys } from './api';
3
3
  import * as tui from '../../../tui';
4
4
  import enquirer from 'enquirer';
5
5
  import { z } from 'zod';
6
+ import { isExplainMode, isDryRunMode, outputExplain, outputDryRun } from '../../../explain';
7
+ import { getCommand } from '../../../command-prefix';
8
+ import { ErrorCode } from '../../../errors';
9
+
10
+ const SSHDeleteResponseSchema = z.object({
11
+ success: z.boolean().describe('Whether the operation succeeded'),
12
+ removed: z.number().describe('Number of keys removed'),
13
+ fingerprints: z.array(z.string()).describe('Fingerprints of removed keys'),
14
+ });
6
15
 
7
16
  export const deleteCommand = createSubcommand({
8
17
  name: 'delete',
9
18
  aliases: ['rm', 'del', 'remove'],
10
19
  description: 'Delete an SSH key from your account',
20
+ tags: ['destructive', 'deletes-resource', 'slow', 'requires-auth'],
11
21
  requires: { apiClient: true, auth: true },
22
+ idempotent: false,
23
+ examples: [
24
+ getCommand('auth ssh delete'),
25
+ getCommand('auth ssh delete <fingerprint>'),
26
+ getCommand('--explain auth ssh delete abc123'),
27
+ getCommand('--dry-run auth ssh delete abc123'),
28
+ ],
12
29
  schema: {
13
30
  args: z.object({
14
31
  fingerprints: z.array(z.string()).optional().describe('SSH key fingerprint(s) to remove'),
@@ -16,12 +33,13 @@ export const deleteCommand = createSubcommand({
16
33
  options: z.object({
17
34
  confirm: z.boolean().default(true).describe('prompt for confirmation before deletion'),
18
35
  }),
36
+ response: SSHDeleteResponseSchema,
19
37
  },
20
38
  async handler(ctx) {
21
- const { logger, apiClient, args, opts } = ctx;
39
+ const { logger, apiClient, args, opts, options } = ctx;
22
40
 
23
41
  if (!apiClient) {
24
- logger.fatal('API client is not available');
42
+ logger.fatal('API client is not available', ErrorCode.INTERNAL_ERROR);
25
43
  }
26
44
 
27
45
  const shouldConfirm = process.stdin.isTTY ? opts.confirm : false;
@@ -67,6 +85,22 @@ export const deleteCommand = createSubcommand({
67
85
  }
68
86
  }
69
87
 
88
+ // If in explain mode, show what would happen
89
+ if (isExplainMode(options)) {
90
+ outputExplain(
91
+ {
92
+ command: 'auth ssh delete',
93
+ description: 'Delete SSH keys from your account',
94
+ steps: fingerprintsToRemove.map((fp) => ({
95
+ action: `Remove SSH key with fingerprint: ${fp}`,
96
+ })),
97
+ warnings: ['This action cannot be undone'],
98
+ },
99
+ options
100
+ );
101
+ return;
102
+ }
103
+
70
104
  if (shouldConfirm) {
71
105
  tui.newline();
72
106
  const confirmed = await tui.confirm(
@@ -80,6 +114,19 @@ export const deleteCommand = createSubcommand({
80
114
  }
81
115
  }
82
116
 
117
+ // Handle dry-run mode
118
+ if (isDryRunMode(options)) {
119
+ for (const fingerprint of fingerprintsToRemove) {
120
+ outputDryRun(`Would remove SSH key: ${fingerprint}`, options);
121
+ }
122
+ tui.newline();
123
+ tui.info(
124
+ `[DRY RUN] Would remove ${fingerprintsToRemove.length} SSH key${fingerprintsToRemove.length > 1 ? 's' : ''}`
125
+ );
126
+ return;
127
+ }
128
+
129
+ // Actually execute the deletion
83
130
  for (const fingerprint of fingerprintsToRemove) {
84
131
  await tui.spinner(`Removing SSH key ${fingerprint}...`, () =>
85
132
  removeSSHKey(apiClient, fingerprint)
@@ -90,12 +137,18 @@ export const deleteCommand = createSubcommand({
90
137
  tui.success(
91
138
  `Removed ${fingerprintsToRemove.length} SSH key${fingerprintsToRemove.length > 1 ? 's' : ''}`
92
139
  );
140
+
141
+ return {
142
+ success: true,
143
+ removed: fingerprintsToRemove.length,
144
+ fingerprints: fingerprintsToRemove,
145
+ };
93
146
  } catch (error) {
94
147
  logger.trace(error);
95
148
  if (error instanceof Error) {
96
- logger.fatal(`Failed to remove SSH key: ${error.message}`);
149
+ logger.fatal(`Failed to remove SSH key: ${error.message}`, ErrorCode.API_ERROR);
97
150
  } else {
98
- logger.fatal('Failed to remove SSH key');
151
+ logger.fatal('Failed to remove SSH key', ErrorCode.API_ERROR);
99
152
  }
100
153
  }
101
154
  },
@@ -1,10 +1,11 @@
1
- import type { SubcommandDefinition } from '../../../types';
1
+ import { createCommand } from '../../../types';
2
2
  import { listCommand } from './list';
3
3
  import { addCommand } from './add';
4
4
  import { deleteCommand } from './delete';
5
5
 
6
- export const sshSubcommand: SubcommandDefinition = {
6
+ export const sshSubcommand = createCommand({
7
7
  name: 'ssh',
8
8
  description: 'Manage SSH keys',
9
+ tags: ['fast', 'requires-auth'],
9
10
  subcommands: [listCommand, addCommand, deleteCommand],
10
- };
11
+ });