@agentuity/cli 0.0.43 → 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 (209) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +1 -1
  3. package/dist/api.d.ts +3 -3
  4. package/dist/api.d.ts.map +1 -1
  5. package/dist/auth.d.ts +10 -2
  6. package/dist/auth.d.ts.map +1 -1
  7. package/dist/banner.d.ts.map +1 -1
  8. package/dist/cli.d.ts.map +1 -1
  9. package/dist/cmd/auth/api.d.ts +4 -4
  10. package/dist/cmd/auth/api.d.ts.map +1 -1
  11. package/dist/cmd/auth/index.d.ts.map +1 -1
  12. package/dist/cmd/auth/login.d.ts.map +1 -1
  13. package/dist/cmd/auth/signup.d.ts.map +1 -1
  14. package/dist/cmd/auth/ssh/add.d.ts +2 -0
  15. package/dist/cmd/auth/ssh/add.d.ts.map +1 -0
  16. package/dist/cmd/auth/ssh/api.d.ts +16 -0
  17. package/dist/cmd/auth/ssh/api.d.ts.map +1 -0
  18. package/dist/cmd/auth/ssh/delete.d.ts +2 -0
  19. package/dist/cmd/auth/ssh/delete.d.ts.map +1 -0
  20. package/dist/cmd/auth/ssh/index.d.ts +3 -0
  21. package/dist/cmd/auth/ssh/index.d.ts.map +1 -0
  22. package/dist/cmd/auth/ssh/list.d.ts +2 -0
  23. package/dist/cmd/auth/ssh/list.d.ts.map +1 -0
  24. package/dist/cmd/auth/whoami.d.ts.map +1 -1
  25. package/dist/cmd/bundle/ast.d.ts +14 -3
  26. package/dist/cmd/bundle/ast.d.ts.map +1 -1
  27. package/dist/cmd/bundle/ast.test.d.ts +2 -0
  28. package/dist/cmd/bundle/ast.test.d.ts.map +1 -0
  29. package/dist/cmd/bundle/bundler.d.ts +6 -1
  30. package/dist/cmd/bundle/bundler.d.ts.map +1 -1
  31. package/dist/cmd/bundle/file.d.ts.map +1 -1
  32. package/dist/cmd/bundle/fix-duplicate-exports.d.ts +2 -0
  33. package/dist/cmd/bundle/fix-duplicate-exports.d.ts.map +1 -0
  34. package/dist/cmd/bundle/fix-duplicate-exports.test.d.ts +2 -0
  35. package/dist/cmd/bundle/fix-duplicate-exports.test.d.ts.map +1 -0
  36. package/dist/cmd/bundle/plugin.d.ts +2 -0
  37. package/dist/cmd/bundle/plugin.d.ts.map +1 -1
  38. package/dist/cmd/cloud/deploy.d.ts.map +1 -1
  39. package/dist/cmd/cloud/domain.d.ts +17 -0
  40. package/dist/cmd/cloud/domain.d.ts.map +1 -0
  41. package/dist/cmd/cloud/index.d.ts.map +1 -1
  42. package/dist/cmd/cloud/resource/add.d.ts +2 -0
  43. package/dist/cmd/cloud/resource/add.d.ts.map +1 -0
  44. package/dist/cmd/cloud/resource/delete.d.ts +2 -0
  45. package/dist/cmd/cloud/resource/delete.d.ts.map +1 -0
  46. package/dist/cmd/cloud/resource/index.d.ts +3 -0
  47. package/dist/cmd/cloud/resource/index.d.ts.map +1 -0
  48. package/dist/cmd/cloud/resource/list.d.ts +2 -0
  49. package/dist/cmd/cloud/resource/list.d.ts.map +1 -0
  50. package/dist/cmd/cloud/scp/download.d.ts +2 -0
  51. package/dist/cmd/cloud/scp/download.d.ts.map +1 -0
  52. package/dist/cmd/cloud/scp/index.d.ts +3 -0
  53. package/dist/cmd/cloud/scp/index.d.ts.map +1 -0
  54. package/dist/cmd/cloud/scp/upload.d.ts +2 -0
  55. package/dist/cmd/cloud/scp/upload.d.ts.map +1 -0
  56. package/dist/cmd/cloud/ssh.d.ts +2 -0
  57. package/dist/cmd/cloud/ssh.d.ts.map +1 -0
  58. package/dist/cmd/dev/api.d.ts +18 -0
  59. package/dist/cmd/dev/api.d.ts.map +1 -0
  60. package/dist/cmd/dev/download.d.ts +11 -0
  61. package/dist/cmd/dev/download.d.ts.map +1 -0
  62. package/dist/cmd/dev/index.d.ts.map +1 -1
  63. package/dist/cmd/dev/templates.d.ts +3 -0
  64. package/dist/cmd/dev/templates.d.ts.map +1 -0
  65. package/dist/cmd/env/delete.d.ts.map +1 -1
  66. package/dist/cmd/env/get.d.ts.map +1 -1
  67. package/dist/cmd/env/import.d.ts.map +1 -1
  68. package/dist/cmd/env/list.d.ts.map +1 -1
  69. package/dist/cmd/env/pull.d.ts.map +1 -1
  70. package/dist/cmd/env/push.d.ts.map +1 -1
  71. package/dist/cmd/env/set.d.ts.map +1 -1
  72. package/dist/cmd/profile/show.d.ts.map +1 -1
  73. package/dist/cmd/project/create.d.ts.map +1 -1
  74. package/dist/cmd/project/delete.d.ts.map +1 -1
  75. package/dist/cmd/project/list.d.ts.map +1 -1
  76. package/dist/cmd/project/show.d.ts.map +1 -1
  77. package/dist/cmd/project/template-flow.d.ts +4 -0
  78. package/dist/cmd/project/template-flow.d.ts.map +1 -1
  79. package/dist/cmd/secret/delete.d.ts.map +1 -1
  80. package/dist/cmd/secret/get.d.ts.map +1 -1
  81. package/dist/cmd/secret/import.d.ts.map +1 -1
  82. package/dist/cmd/secret/list.d.ts.map +1 -1
  83. package/dist/cmd/secret/pull.d.ts.map +1 -1
  84. package/dist/cmd/secret/push.d.ts.map +1 -1
  85. package/dist/cmd/secret/set.d.ts.map +1 -1
  86. package/dist/config.d.ts +9 -3
  87. package/dist/config.d.ts.map +1 -1
  88. package/dist/crypto/box.d.ts +65 -0
  89. package/dist/crypto/box.d.ts.map +1 -0
  90. package/dist/crypto/box.test.d.ts +2 -0
  91. package/dist/crypto/box.test.d.ts.map +1 -0
  92. package/dist/download.d.ts.map +1 -1
  93. package/dist/steps.d.ts +4 -1
  94. package/dist/steps.d.ts.map +1 -1
  95. package/dist/terminal.d.ts.map +1 -1
  96. package/dist/tui.d.ts +31 -1
  97. package/dist/tui.d.ts.map +1 -1
  98. package/dist/types.d.ts +249 -126
  99. package/dist/types.d.ts.map +1 -1
  100. package/dist/utils/detectSubagent.d.ts +15 -0
  101. package/dist/utils/detectSubagent.d.ts.map +1 -0
  102. package/dist/utils/zip.d.ts +7 -0
  103. package/dist/utils/zip.d.ts.map +1 -0
  104. package/package.json +11 -3
  105. package/src/api-errors.md +2 -2
  106. package/src/api.ts +12 -7
  107. package/src/auth.ts +116 -7
  108. package/src/banner.ts +13 -6
  109. package/src/cli.ts +695 -63
  110. package/src/cmd/auth/api.ts +10 -16
  111. package/src/cmd/auth/index.ts +2 -1
  112. package/src/cmd/auth/login.ts +24 -8
  113. package/src/cmd/auth/signup.ts +15 -11
  114. package/src/cmd/auth/ssh/add.ts +263 -0
  115. package/src/cmd/auth/ssh/api.ts +94 -0
  116. package/src/cmd/auth/ssh/delete.ts +102 -0
  117. package/src/cmd/auth/ssh/index.ts +10 -0
  118. package/src/cmd/auth/ssh/list.ts +74 -0
  119. package/src/cmd/auth/whoami.ts +13 -13
  120. package/src/cmd/bundle/ast.test.ts +565 -0
  121. package/src/cmd/bundle/ast.ts +457 -44
  122. package/src/cmd/bundle/bundler.ts +255 -57
  123. package/src/cmd/bundle/file.ts +6 -12
  124. package/src/cmd/bundle/fix-duplicate-exports.test.ts +387 -0
  125. package/src/cmd/bundle/fix-duplicate-exports.ts +204 -0
  126. package/src/cmd/bundle/index.ts +9 -9
  127. package/src/cmd/bundle/patch/aisdk.ts +1 -1
  128. package/src/cmd/bundle/plugin.ts +373 -53
  129. package/src/cmd/cloud/deploy.ts +300 -93
  130. package/src/cmd/cloud/domain.ts +92 -0
  131. package/src/cmd/cloud/index.ts +4 -1
  132. package/src/cmd/cloud/resource/add.ts +56 -0
  133. package/src/cmd/cloud/resource/delete.ts +120 -0
  134. package/src/cmd/cloud/resource/index.ts +11 -0
  135. package/src/cmd/cloud/resource/list.ts +69 -0
  136. package/src/cmd/cloud/scp/download.ts +59 -0
  137. package/src/cmd/cloud/scp/index.ts +9 -0
  138. package/src/cmd/cloud/scp/upload.ts +62 -0
  139. package/src/cmd/cloud/ssh.ts +68 -0
  140. package/src/cmd/dev/api.ts +46 -0
  141. package/src/cmd/dev/download.ts +111 -0
  142. package/src/cmd/dev/index.ts +360 -34
  143. package/src/cmd/dev/templates.ts +84 -0
  144. package/src/cmd/env/delete.ts +5 -20
  145. package/src/cmd/env/get.ts +5 -18
  146. package/src/cmd/env/import.ts +5 -20
  147. package/src/cmd/env/list.ts +5 -18
  148. package/src/cmd/env/pull.ts +10 -23
  149. package/src/cmd/env/push.ts +5 -23
  150. package/src/cmd/env/set.ts +5 -20
  151. package/src/cmd/index.ts +2 -2
  152. package/src/cmd/profile/show.ts +15 -6
  153. package/src/cmd/project/create.ts +7 -2
  154. package/src/cmd/project/delete.ts +75 -18
  155. package/src/cmd/project/download.ts +2 -2
  156. package/src/cmd/project/list.ts +8 -8
  157. package/src/cmd/project/show.ts +3 -7
  158. package/src/cmd/project/template-flow.ts +170 -72
  159. package/src/cmd/secret/delete.ts +5 -20
  160. package/src/cmd/secret/get.ts +5 -18
  161. package/src/cmd/secret/import.ts +5 -20
  162. package/src/cmd/secret/list.ts +5 -18
  163. package/src/cmd/secret/pull.ts +10 -23
  164. package/src/cmd/secret/push.ts +5 -23
  165. package/src/cmd/secret/set.ts +5 -20
  166. package/src/config.ts +224 -24
  167. package/src/crypto/box.test.ts +431 -0
  168. package/src/crypto/box.ts +477 -0
  169. package/src/download.ts +1 -0
  170. package/src/env-util.test.ts +1 -1
  171. package/src/steps.ts +65 -6
  172. package/src/terminal.ts +24 -23
  173. package/src/tui.ts +192 -61
  174. package/src/types.ts +291 -201
  175. package/src/utils/detectSubagent.ts +31 -0
  176. package/src/utils/zip.ts +38 -0
  177. package/dist/cmd/example/create-user.d.ts +0 -2
  178. package/dist/cmd/example/create-user.d.ts.map +0 -1
  179. package/dist/cmd/example/create.d.ts +0 -2
  180. package/dist/cmd/example/create.d.ts.map +0 -1
  181. package/dist/cmd/example/deploy.d.ts +0 -2
  182. package/dist/cmd/example/deploy.d.ts.map +0 -1
  183. package/dist/cmd/example/index.d.ts +0 -2
  184. package/dist/cmd/example/index.d.ts.map +0 -1
  185. package/dist/cmd/example/list.d.ts +0 -2
  186. package/dist/cmd/example/list.d.ts.map +0 -1
  187. package/dist/cmd/example/optional-auth.d.ts +0 -3
  188. package/dist/cmd/example/optional-auth.d.ts.map +0 -1
  189. package/dist/cmd/example/run-command.d.ts +0 -2
  190. package/dist/cmd/example/run-command.d.ts.map +0 -1
  191. package/dist/cmd/example/sound.d.ts +0 -3
  192. package/dist/cmd/example/sound.d.ts.map +0 -1
  193. package/dist/cmd/example/spinner.d.ts +0 -2
  194. package/dist/cmd/example/spinner.d.ts.map +0 -1
  195. package/dist/cmd/example/steps.d.ts +0 -2
  196. package/dist/cmd/example/steps.d.ts.map +0 -1
  197. package/dist/cmd/example/version.d.ts +0 -2
  198. package/dist/cmd/example/version.d.ts.map +0 -1
  199. package/src/cmd/example/create-user.ts +0 -38
  200. package/src/cmd/example/create.ts +0 -31
  201. package/src/cmd/example/deploy.ts +0 -36
  202. package/src/cmd/example/index.ts +0 -29
  203. package/src/cmd/example/list.ts +0 -32
  204. package/src/cmd/example/optional-auth.ts +0 -38
  205. package/src/cmd/example/run-command.ts +0 -45
  206. package/src/cmd/example/sound.ts +0 -14
  207. package/src/cmd/example/spinner.ts +0 -44
  208. package/src/cmd/example/steps.ts +0 -66
  209. package/src/cmd/example/version.ts +0 -13
package/src/cli.ts CHANGED
@@ -1,8 +1,116 @@
1
1
  import { Command } from 'commander';
2
- import type { CommandDefinition, SubcommandDefinition, CommandContext } from './types';
2
+ import type {
3
+ CommandDefinition,
4
+ SubcommandDefinition,
5
+ CommandContext,
6
+ ProjectConfig,
7
+ Config,
8
+ Requires,
9
+ Optional,
10
+ Logger,
11
+ AuthData,
12
+ } from './types';
3
13
  import { showBanner } from './banner';
4
- import { requireAuth, optionalAuth } from './auth';
14
+ import { requireAuth, optionalAuth, requireOrg, optionalOrg as selectOptionalOrg } from './auth';
15
+ import { listRegions, type RegionList } from '@agentuity/server';
16
+ import enquirer from 'enquirer';
17
+ import * as tui from './tui';
5
18
  import { parseArgsSchema, parseOptionsSchema, buildValidationInput } from './schema-parser';
19
+ import { defaultProfileName, loadProjectConfig } from './config';
20
+ import { APIClient, getAPIBaseURL, type APIClient as APIClientType } from './api';
21
+
22
+ function createAPIClient(baseCtx: CommandContext, config: Config | null): APIClient {
23
+ try {
24
+ const apiUrl = getAPIBaseURL(config);
25
+ const apiClient = new APIClient(apiUrl, baseCtx.logger, config);
26
+
27
+ if (!apiClient) {
28
+ throw new Error('APIClient constructor returned null/undefined');
29
+ }
30
+
31
+ if (typeof apiClient.request !== 'function') {
32
+ throw new Error('APIClient instance is missing request method');
33
+ }
34
+
35
+ return apiClient;
36
+ } catch (error) {
37
+ baseCtx.logger.error('Failed to create API client:', error);
38
+ throw new Error(
39
+ `API client initialization failed: ${error instanceof Error ? error.message : 'Unknown error'}`
40
+ );
41
+ }
42
+ }
43
+
44
+ type Normalized = {
45
+ requiresAuth: boolean;
46
+ optionalAuth: false | string;
47
+ requiresProject: boolean;
48
+ optionalProject: boolean;
49
+ requiresAPIClient: boolean;
50
+ requiresOrg: boolean;
51
+ optionalOrg: boolean;
52
+ requiresRegions: boolean;
53
+ requiresRegion: boolean;
54
+ optionalRegion: boolean;
55
+ };
56
+
57
+ function normalizeReqs(def: CommandDefinition | SubcommandDefinition): Normalized {
58
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
59
+ const d: any = def as any;
60
+ const requires = d.requires as Requires | undefined;
61
+ const optional = d.optional as Optional | undefined;
62
+
63
+ const requiresAuth = requires?.auth === true;
64
+ const optionalAuthValue = optional?.auth;
65
+ const optionalAuth: false | string =
66
+ optionalAuthValue === true ? 'Continue without authentication' : optionalAuthValue || false;
67
+
68
+ const requiresProject = requires?.project === true;
69
+ const optionalProject = optional?.project === true;
70
+
71
+ const requiresOrg = requires?.org === true;
72
+ const optionalOrg = optional?.org === true;
73
+ const requiresRegions = requires?.regions === true;
74
+ const requiresRegion = requires?.region === true;
75
+ const optionalRegion = optional?.region === true;
76
+
77
+ // Implicitly require apiClient if org or region is required or optional
78
+ const requiresAPIClient =
79
+ requires?.apiClient === true ||
80
+ requiresOrg ||
81
+ optionalOrg ||
82
+ requiresRegion ||
83
+ optionalRegion ||
84
+ requiresRegions;
85
+
86
+ return {
87
+ requiresAuth,
88
+ optionalAuth,
89
+ requiresProject,
90
+ optionalProject,
91
+ requiresAPIClient,
92
+ requiresOrg,
93
+ optionalOrg,
94
+ requiresRegions,
95
+ requiresRegion,
96
+ optionalRegion,
97
+ };
98
+ }
99
+
100
+ function handleProjectConfigError(error: unknown, requiresProject: boolean, logger: Logger): never {
101
+ if (
102
+ requiresProject &&
103
+ error &&
104
+ typeof error === 'object' &&
105
+ 'name' in error &&
106
+ error.name === 'ProjectConfigNotFoundExpection'
107
+ ) {
108
+ logger.fatal(
109
+ 'invalid project folder. use --dir to specify a different directory or change to a project folder'
110
+ );
111
+ }
112
+ throw error;
113
+ }
6
114
 
7
115
  export async function createCLI(version: string): Promise<Command> {
8
116
  const program = new Command();
@@ -11,13 +119,20 @@ export async function createCLI(version: string): Promise<Command> {
11
119
  .name('agentuity')
12
120
  .description('Agentuity CLI')
13
121
  .version(version, '-V, --version', 'Display version')
14
- .helpOption('-h, --help', 'Display help');
122
+ .helpOption('-h, --help', 'Display help')
123
+ .allowUnknownOption(false)
124
+ .allowExcessArguments(false);
15
125
 
16
126
  program
17
- .option('--config <path>', 'Config file path', '~/.config/agentuity/production.yaml')
127
+ .option('--config <path>', 'Config file path')
18
128
  .option('--log-level <level>', 'Log level', process.env.AGENTUITY_LOG_LEVEL ?? 'info')
19
129
  .option('--log-timestamp', 'Show timestamps in log output', false)
20
- .option('--no-log-prefix', 'Hide log level prefixes', false)
130
+ .option('--no-log-prefix', 'Hide log level prefixes', true)
131
+ .option(
132
+ '--org-id <id>',
133
+ 'Use a specific organization when performing operations',
134
+ process.env.AGENTUITY_CLOUD_ORG_ID
135
+ )
21
136
  .option('--color-scheme <scheme>', 'Color scheme: light or dark');
22
137
 
23
138
  const skipVersionCheckOption = program.createOption(
@@ -32,9 +147,121 @@ export async function createCLI(version: string): Promise<Command> {
32
147
  program.help();
33
148
  });
34
149
 
150
+ // Handle unknown commands
151
+ program.on('command:*', (operands: string[]) => {
152
+ const unknownCommand = operands[0];
153
+ console.error(`error: unknown command '${unknownCommand}'`);
154
+ console.error();
155
+ const availableCommands = program.commands.map((cmd) => cmd.name());
156
+ if (availableCommands.length > 0) {
157
+ console.error('Available commands:');
158
+ availableCommands.forEach((name) => {
159
+ console.error(` ${name}`);
160
+ });
161
+ }
162
+ console.error();
163
+ console.error(`Run 'agentuity --help' for usage information.`);
164
+ process.exit(1);
165
+ });
166
+
167
+ // Custom error handling for argument/command parsing errors
168
+ program.configureOutput({
169
+ outputError: (str, write) => {
170
+ // Intercept commander.js error messages
171
+ if (str.includes('too many arguments') || str.includes('unknown command')) {
172
+ // Extract potential command name from error context
173
+ const match = str.match(/got (\d+)/);
174
+ if (match) {
175
+ write(`error: unknown command or subcommand\n`);
176
+ write(`\nRun 'agentuity --help' for available commands.\n`);
177
+ } else {
178
+ write(str);
179
+ }
180
+ } else {
181
+ write(str);
182
+ }
183
+ },
184
+ });
185
+
35
186
  return program;
36
187
  }
37
188
 
189
+ async function getRegion(regions: RegionList): Promise<string> {
190
+ if (regions.length === 1) {
191
+ return regions[0].region;
192
+ } else {
193
+ const response = await enquirer.prompt<{ region: string }>({
194
+ type: 'select',
195
+ name: 'region',
196
+ message: 'Select a cloud region:',
197
+ choices: regions.map((r) => ({
198
+ name: r.region,
199
+ message: `${r.description.padEnd(15, ' ')} ${tui.muted(r.region)}`,
200
+ })),
201
+ });
202
+ return response.region;
203
+ }
204
+ }
205
+
206
+ interface ResolveRegionOptions {
207
+ options: Record<string, unknown>;
208
+ apiClient: APIClientType;
209
+ logger: Logger;
210
+ required: boolean;
211
+ }
212
+
213
+ async function resolveRegion(opts: ResolveRegionOptions): Promise<string | undefined> {
214
+ const { options, apiClient, logger, required } = opts;
215
+
216
+ // Fetch regions
217
+ const regions = await listRegions(apiClient);
218
+
219
+ // No regions available
220
+ if (regions.length === 0) {
221
+ if (required) {
222
+ logger.fatal('No cloud regions available');
223
+ }
224
+ return undefined;
225
+ }
226
+
227
+ // Check if region was provided via flag
228
+ let region = options.region as string | undefined;
229
+
230
+ // Validate --region flag if provided
231
+ if (region) {
232
+ const found = regions.find((r) => r.region === region);
233
+ if (!found) {
234
+ logger.fatal(
235
+ `Invalid region '${region}'. Use one of: ${regions.map((r) => r.region).join(', ')}`
236
+ );
237
+ }
238
+ return region;
239
+ }
240
+
241
+ // Auto-select if only one region available
242
+ if (regions.length === 1) {
243
+ region = regions[0].region;
244
+ if (!process.stdin.isTTY) {
245
+ logger.trace('auto-selected region (non-TTY): %s', region);
246
+ }
247
+ return region;
248
+ }
249
+
250
+ // No flag provided - handle TTY vs non-TTY
251
+ if (required && !process.stdin.isTTY) {
252
+ logger.fatal('--region flag is required in non-interactive mode');
253
+ }
254
+
255
+ if (process.stdin.isTTY) {
256
+ // Interactive mode - prompt user
257
+ region = await getRegion(regions);
258
+ return region;
259
+ }
260
+
261
+ // Non-interactive, optional region - return undefined
262
+ return undefined;
263
+ }
264
+
38
265
  async function registerSubcommand(
39
266
  parent: Command,
40
267
  subcommand: SubcommandDefinition,
@@ -47,7 +274,37 @@ async function registerSubcommand(
47
274
  cmd.aliases(subcommand.aliases);
48
275
  }
49
276
 
50
- // Auto-generate arguments and options from schemas
277
+ // Check if this subcommand has its own subcommands (nested subcommands)
278
+ const subDef = subcommand as unknown as { subcommands?: SubcommandDefinition[] };
279
+ if (subDef.subcommands && subDef.subcommands.length > 0) {
280
+ // Register nested subcommands recursively
281
+ for (const nestedSub of subDef.subcommands) {
282
+ await registerSubcommand(cmd, nestedSub, baseCtx);
283
+ }
284
+ return;
285
+ }
286
+
287
+ const {
288
+ requiresProject,
289
+ optionalProject,
290
+ requiresOrg,
291
+ optionalOrg,
292
+ requiresRegion,
293
+ optionalRegion,
294
+ } = normalizeReqs(subcommand);
295
+
296
+ if (requiresProject || optionalProject) {
297
+ cmd.option('--dir <path>', 'project directory (default: current directory)');
298
+ }
299
+
300
+ if (requiresOrg || optionalOrg) {
301
+ cmd.option('--org-id <id>', 'organization ID');
302
+ }
303
+
304
+ if (requiresRegion || optionalRegion) {
305
+ cmd.option('--region <region>', 'cloud region');
306
+ }
307
+
51
308
  if (subcommand.schema?.args) {
52
309
  const parsed = parseArgsSchema(subcommand.schema.args);
53
310
  for (const argMeta of parsed.metadata) {
@@ -67,9 +324,7 @@ async function registerSubcommand(
67
324
  const flag = opt.name.replace(/([A-Z])/g, '-$1').toLowerCase();
68
325
  const desc = opt.description || '';
69
326
  if (opt.type === 'boolean') {
70
- // Support negatable boolean options (--no-flag) when they have a default
71
327
  if (opt.hasDefault) {
72
- // Evaluate default value (could be a function)
73
328
  const defaultValue =
74
329
  typeof opt.defaultValue === 'function' ? opt.defaultValue() : opt.defaultValue;
75
330
  cmd.option(`--no-${flag}`, desc);
@@ -90,23 +345,112 @@ async function registerSubcommand(
90
345
  const options = cmdObj.opts();
91
346
  const args = rawArgs.slice(0, -1);
92
347
 
93
- if (subcommand.requiresAuth) {
94
- const auth = await requireAuth(baseCtx as CommandContext<false>);
348
+ if (subcommand.banner) {
349
+ showBanner();
350
+ }
351
+
352
+ const normalized = normalizeReqs(subcommand);
353
+
354
+ let project: ProjectConfig | undefined;
355
+ let projectDir: string | undefined;
356
+ const dirNeeded = normalized.requiresProject || normalized.optionalProject;
357
+
358
+ if (dirNeeded) {
359
+ const dir = (options.dir as string | undefined) ?? process.cwd();
360
+ projectDir = dir;
361
+ try {
362
+ project = await loadProjectConfig(dir, baseCtx.config);
363
+ } catch (error) {
364
+ if (normalized.requiresProject) {
365
+ if (
366
+ error &&
367
+ typeof error === 'object' &&
368
+ 'name' in error &&
369
+ error.name === 'ProjectConfigNotFoundExpection'
370
+ ) {
371
+ baseCtx.logger.fatal(
372
+ 'invalid project folder. use --dir to specify a different directory or change to a project folder'
373
+ );
374
+ }
375
+ throw error;
376
+ }
377
+ // For optional projects, silently continue without project config
378
+ }
379
+ }
380
+
381
+ if (normalized.requiresAuth) {
382
+ // Create apiClient before requireAuth since login command needs it
383
+ if (normalized.requiresAPIClient) {
384
+ (baseCtx as Record<string, unknown>).apiClient = createAPIClient(
385
+ baseCtx,
386
+ baseCtx.config ?? null
387
+ );
388
+ }
389
+
390
+ const auth = await requireAuth(baseCtx as CommandContext<undefined>);
95
391
 
96
392
  if (subcommand.schema) {
97
393
  try {
98
394
  const input = buildValidationInput(subcommand.schema, args, options);
99
395
  const ctx: Record<string, unknown> = {
100
396
  ...baseCtx,
397
+ config: {
398
+ ...(baseCtx.config ?? {}),
399
+ auth: {
400
+ api_key: auth.apiKey,
401
+ user_id: auth.userId,
402
+ expires: auth.expires.getTime(),
403
+ },
404
+ },
101
405
  auth,
102
406
  };
407
+ if (project || projectDir) {
408
+ if (project) {
409
+ ctx.project = project;
410
+ }
411
+ ctx.projectDir = projectDir;
412
+ }
103
413
  if (subcommand.schema.args) {
104
414
  ctx.args = subcommand.schema.args.parse(input.args);
105
415
  }
106
416
  if (subcommand.schema.options) {
107
417
  ctx.opts = subcommand.schema.options.parse(input.options);
108
418
  }
109
- await subcommand.handler(ctx as CommandContext);
419
+ if (normalized.requiresAPIClient) {
420
+ // Recreate apiClient with auth credentials
421
+ ctx.apiClient = createAPIClient(baseCtx, ctx.config as Config | null);
422
+ }
423
+ if (normalized.requiresOrg) {
424
+ ctx.orgId = await requireOrg(
425
+ ctx as CommandContext & { apiClient: APIClientType }
426
+ );
427
+ }
428
+ if (normalized.optionalOrg && ctx.auth) {
429
+ ctx.orgId = await requireOrg(
430
+ ctx as CommandContext & { apiClient: APIClientType }
431
+ );
432
+ }
433
+ if ((normalized.requiresRegion || normalized.optionalRegion) && ctx.apiClient) {
434
+ const apiClient: APIClientType = ctx.apiClient as APIClientType;
435
+ const region = await tui.spinner({
436
+ message: 'Fetching cloud regions',
437
+ clearOnSuccess: true,
438
+ callback: async () => {
439
+ return resolveRegion({
440
+ options: options as Record<string, unknown>,
441
+ apiClient,
442
+ logger: baseCtx.logger,
443
+ required: !!normalized.requiresRegion,
444
+ });
445
+ },
446
+ });
447
+ if (region) {
448
+ ctx.region = region;
449
+ }
450
+ }
451
+ if (subcommand.handler) {
452
+ await subcommand.handler(ctx as CommandContext);
453
+ }
110
454
  } catch (error) {
111
455
  if (error && typeof error === 'object' && 'issues' in error) {
112
456
  baseCtx.logger.error('Validation error:');
@@ -119,44 +463,142 @@ async function registerSubcommand(
119
463
  }
120
464
  process.exit(1);
121
465
  }
122
- if (
123
- error &&
124
- typeof error === 'object' &&
125
- 'name' in error &&
126
- error.name === 'ProjectConfigNotFoundExpection'
127
- ) {
128
- baseCtx.logger.fatal(
129
- 'invalid project folder. use --dir to specify a different directory or change to a project folder'
130
- );
131
- }
132
- throw error;
466
+ handleProjectConfigError(error, normalized.requiresProject, baseCtx.logger);
133
467
  }
134
468
  } else {
135
- const ctx: CommandContext<true> = {
469
+ const ctx: Record<string, unknown> = {
136
470
  ...baseCtx,
471
+ config: baseCtx.config
472
+ ? {
473
+ ...baseCtx.config,
474
+ name: baseCtx.config.name ?? defaultProfileName,
475
+ auth: {
476
+ api_key: auth.apiKey,
477
+ user_id: auth.userId,
478
+ expires: auth.expires.getTime(),
479
+ },
480
+ }
481
+ : null,
137
482
  auth,
138
483
  };
139
- await subcommand.handler(ctx);
484
+ if (project || projectDir) {
485
+ if (project) {
486
+ ctx.project = project;
487
+ }
488
+ ctx.projectDir = projectDir;
489
+ }
490
+ if (normalized.requiresAPIClient) {
491
+ // Recreate apiClient with auth credentials
492
+ ctx.apiClient = createAPIClient(baseCtx, ctx.config as Config | null);
493
+ }
494
+ if (normalized.requiresOrg) {
495
+ ctx.orgId = await requireOrg(ctx as CommandContext & { apiClient: APIClientType });
496
+ }
497
+ if (normalized.optionalOrg && ctx.auth) {
498
+ ctx.orgId = await requireOrg(ctx as CommandContext & { apiClient: APIClientType });
499
+ }
500
+ if ((normalized.requiresRegion || normalized.optionalRegion) && ctx.apiClient) {
501
+ const apiClient: APIClientType = ctx.apiClient as APIClientType;
502
+ const region = await tui.spinner('Fetching cloud regions', async () => {
503
+ return resolveRegion({
504
+ options: options as Record<string, unknown>,
505
+ apiClient,
506
+ logger: baseCtx.logger,
507
+ required: !!normalized.requiresRegion,
508
+ });
509
+ });
510
+ if (region) {
511
+ ctx.region = region;
512
+ }
513
+ }
514
+ if (subcommand.handler) {
515
+ await subcommand.handler(ctx as CommandContext);
516
+ }
140
517
  }
141
- } else if (subcommand.optionalAuth) {
518
+ } else if (normalized.optionalAuth) {
142
519
  const continueText =
143
- typeof subcommand.optionalAuth === 'string' ? subcommand.optionalAuth : undefined;
144
- const auth = await optionalAuth(baseCtx as CommandContext<false>, continueText);
520
+ typeof normalized.optionalAuth === 'string' ? normalized.optionalAuth : undefined;
521
+
522
+ // Create apiClient before optionalAuth since login command needs it
523
+ if (normalized.requiresAPIClient) {
524
+ (baseCtx as Record<string, unknown>).apiClient = createAPIClient(
525
+ baseCtx,
526
+ baseCtx.config ?? null
527
+ );
528
+ }
529
+
530
+ const auth = await optionalAuth(baseCtx as CommandContext<undefined>, continueText);
145
531
 
146
532
  if (subcommand.schema) {
147
533
  try {
148
534
  const input = buildValidationInput(subcommand.schema, args, options);
149
535
  const ctx: Record<string, unknown> = {
150
536
  ...baseCtx,
537
+ config: auth
538
+ ? {
539
+ ...(baseCtx.config ?? {}),
540
+ auth: {
541
+ api_key: auth.apiKey,
542
+ user_id: auth.userId,
543
+ expires: auth.expires.getTime(),
544
+ },
545
+ }
546
+ : baseCtx.config,
151
547
  auth,
152
548
  };
549
+ if (project || projectDir) {
550
+ if (project) {
551
+ ctx.project = project;
552
+ }
553
+ ctx.projectDir = projectDir;
554
+ }
153
555
  if (subcommand.schema.args) {
154
556
  ctx.args = subcommand.schema.args.parse(input.args);
155
557
  }
156
558
  if (subcommand.schema.options) {
157
559
  ctx.opts = subcommand.schema.options.parse(input.options);
158
560
  }
159
- await subcommand.handler(ctx as CommandContext);
561
+ if (normalized.requiresAPIClient) {
562
+ // Recreate apiClient with auth credentials
563
+ ctx.apiClient = createAPIClient(baseCtx, ctx.config as Config | null);
564
+ }
565
+ baseCtx.logger.trace(
566
+ 'optionalAuth path: org=%s, region=%s, hasApiClient=%s, hasAuth=%s',
567
+ normalized.optionalOrg,
568
+ normalized.optionalRegion,
569
+ !!ctx.apiClient,
570
+ !!auth
571
+ );
572
+ if (normalized.requiresOrg && ctx.apiClient) {
573
+ ctx.orgId = await requireOrg(
574
+ ctx as CommandContext & { apiClient: APIClientType }
575
+ );
576
+ }
577
+ if (normalized.optionalOrg && ctx.apiClient && auth) {
578
+ ctx.orgId = await selectOptionalOrg(
579
+ ctx as CommandContext & { apiClient?: APIClientType; auth?: AuthData }
580
+ );
581
+ baseCtx.logger.trace('selected orgId: %s', ctx.orgId);
582
+ }
583
+ if (
584
+ (normalized.requiresRegion || normalized.optionalRegion) &&
585
+ ctx.apiClient &&
586
+ auth
587
+ ) {
588
+ const apiClient: APIClientType = ctx.apiClient as APIClientType;
589
+ const region = await resolveRegion({
590
+ options: options as Record<string, unknown>,
591
+ apiClient,
592
+ logger: baseCtx.logger,
593
+ required: !!normalized.requiresRegion,
594
+ });
595
+ if (region) {
596
+ ctx.region = region;
597
+ }
598
+ }
599
+ if (subcommand.handler) {
600
+ await subcommand.handler(ctx as CommandContext);
601
+ }
160
602
  } catch (error) {
161
603
  if (error && typeof error === 'object' && 'issues' in error) {
162
604
  baseCtx.logger.error('Validation error:');
@@ -169,21 +611,56 @@ async function registerSubcommand(
169
611
  }
170
612
  process.exit(1);
171
613
  }
172
- if (
173
- error &&
174
- typeof error === 'object' &&
175
- 'name' in error &&
176
- error.name === 'ProjectConfigNotFoundExpection'
177
- ) {
178
- baseCtx.logger.fatal(
179
- 'invalid project folder. use --dir to specify a different directory or change to a project folder'
180
- );
181
- }
182
- throw error;
614
+ handleProjectConfigError(error, normalized.requiresProject, baseCtx.logger);
183
615
  }
184
616
  } else {
185
- const ctx = { ...baseCtx, auth };
186
- await subcommand.handler(ctx as CommandContext);
617
+ const ctx: Record<string, unknown> = {
618
+ ...baseCtx,
619
+ config: auth
620
+ ? {
621
+ ...(baseCtx.config ?? {}),
622
+ auth: {
623
+ api_key: auth.apiKey,
624
+ user_id: auth.userId,
625
+ expires: auth.expires.getTime(),
626
+ },
627
+ }
628
+ : baseCtx.config,
629
+ auth,
630
+ };
631
+ if (project || projectDir) {
632
+ if (project) {
633
+ ctx.project = project;
634
+ }
635
+ ctx.projectDir = projectDir;
636
+ }
637
+ if (normalized.requiresAPIClient) {
638
+ // Recreate apiClient with auth credentials if auth was provided
639
+ ctx.apiClient = createAPIClient(baseCtx, ctx.config as Config | null);
640
+ }
641
+ if (normalized.requiresOrg && ctx.apiClient) {
642
+ ctx.orgId = await requireOrg(ctx as CommandContext & { apiClient: APIClientType });
643
+ }
644
+ if (normalized.optionalOrg && ctx.apiClient) {
645
+ ctx.orgId = await selectOptionalOrg(
646
+ ctx as CommandContext & { apiClient?: APIClientType; auth?: AuthData }
647
+ );
648
+ }
649
+ if ((normalized.requiresRegion || normalized.optionalRegion) && ctx.apiClient) {
650
+ const apiClient: APIClientType = ctx.apiClient as APIClientType;
651
+ const region = await resolveRegion({
652
+ options: options as Record<string, unknown>,
653
+ apiClient,
654
+ logger: baseCtx.logger,
655
+ required: !!normalized.requiresRegion,
656
+ });
657
+ if (region) {
658
+ ctx.region = region;
659
+ }
660
+ }
661
+ if (subcommand.handler) {
662
+ await subcommand.handler(ctx as CommandContext);
663
+ }
187
664
  }
188
665
  } else {
189
666
  if (subcommand.schema) {
@@ -192,13 +669,34 @@ async function registerSubcommand(
192
669
  const ctx: Record<string, unknown> = {
193
670
  ...baseCtx,
194
671
  };
672
+ if (project || projectDir) {
673
+ if (project) {
674
+ ctx.project = project;
675
+ }
676
+ ctx.projectDir = projectDir;
677
+ }
195
678
  if (subcommand.schema.args) {
196
679
  ctx.args = subcommand.schema.args.parse(input.args);
197
680
  }
198
681
  if (subcommand.schema.options) {
199
682
  ctx.opts = subcommand.schema.options.parse(input.options);
200
683
  }
201
- await subcommand.handler(ctx as CommandContext);
684
+ if (normalized.requiresAPIClient && !ctx.apiClient) {
685
+ ctx.apiClient = createAPIClient(baseCtx, ctx.config as Config | null);
686
+ }
687
+ if (normalized.requiresOrg && ctx.apiClient) {
688
+ ctx.orgId = await requireOrg(
689
+ ctx as CommandContext & { apiClient: APIClientType }
690
+ );
691
+ }
692
+ if (normalized.optionalOrg && ctx.apiClient && ctx.auth) {
693
+ ctx.orgId = await requireOrg(
694
+ ctx as CommandContext & { apiClient: APIClientType }
695
+ );
696
+ }
697
+ if (subcommand.handler) {
698
+ await subcommand.handler(ctx as CommandContext);
699
+ }
202
700
  } catch (error) {
203
701
  if (error && typeof error === 'object' && 'issues' in error) {
204
702
  baseCtx.logger.error('Validation error:');
@@ -211,20 +709,42 @@ async function registerSubcommand(
211
709
  }
212
710
  process.exit(1);
213
711
  }
214
- if (
215
- error &&
216
- typeof error === 'object' &&
217
- 'name' in error &&
218
- error.name === 'ProjectConfigNotFoundExpection'
219
- ) {
220
- baseCtx.logger.fatal(
221
- 'invalid project folder. use --dir to specify a different directory or change to a project folder'
222
- );
223
- }
224
- throw error;
712
+ handleProjectConfigError(error, normalized.requiresProject, baseCtx.logger);
225
713
  }
226
714
  } else {
227
- await subcommand.handler(baseCtx as CommandContext<false>);
715
+ const ctx: Record<string, unknown> = {
716
+ ...baseCtx,
717
+ };
718
+ if (project || projectDir) {
719
+ if (project) {
720
+ ctx.project = project;
721
+ }
722
+ ctx.projectDir = projectDir;
723
+ }
724
+ if (normalized.requiresAPIClient && !ctx.apiClient) {
725
+ ctx.apiClient = createAPIClient(baseCtx, ctx.config as Config | null);
726
+ }
727
+ if (normalized.requiresOrg && ctx.apiClient) {
728
+ ctx.orgId = await requireOrg(ctx as CommandContext & { apiClient: APIClientType });
729
+ }
730
+ if (normalized.optionalOrg && ctx.apiClient && ctx.auth) {
731
+ ctx.orgId = await requireOrg(ctx as CommandContext & { apiClient: APIClientType });
732
+ }
733
+ if ((normalized.requiresRegion || normalized.optionalRegion) && ctx.apiClient) {
734
+ const apiClient: APIClientType = ctx.apiClient as APIClientType;
735
+ const region = await resolveRegion({
736
+ options: options as Record<string, unknown>,
737
+ apiClient,
738
+ logger: baseCtx.logger,
739
+ required: !!normalized.requiresRegion,
740
+ });
741
+ if (region) {
742
+ ctx.region = region;
743
+ }
744
+ }
745
+ if (subcommand.handler) {
746
+ await subcommand.handler(ctx as CommandContext);
747
+ }
228
748
  }
229
749
  }
230
750
  });
@@ -247,18 +767,130 @@ export async function registerCommands(
247
767
 
248
768
  if (cmdDef.handler) {
249
769
  cmd.action(async () => {
250
- if (cmdDef.requiresAuth) {
251
- const auth = await requireAuth(baseCtx as CommandContext<false>);
252
- const ctx: CommandContext<true> = { ...baseCtx, auth };
253
- await cmdDef.handler!(ctx);
254
- } else if (cmdDef.optionalAuth) {
770
+ if (cmdDef.banner) {
771
+ showBanner();
772
+ }
773
+
774
+ const normalized = normalizeReqs(cmdDef);
775
+ if (normalized.requiresAuth) {
776
+ // Create apiClient before requireAuth since login command needs it
777
+ if (normalized.requiresAPIClient) {
778
+ (baseCtx as Record<string, unknown>).apiClient = createAPIClient(
779
+ baseCtx,
780
+ baseCtx.config ?? null
781
+ );
782
+ }
783
+
784
+ const auth = await requireAuth(baseCtx as CommandContext<undefined>);
785
+ const ctx: Record<string, unknown> = {
786
+ ...baseCtx,
787
+ config: baseCtx.config
788
+ ? {
789
+ ...baseCtx.config,
790
+ name: baseCtx.config.name ?? defaultProfileName,
791
+ auth: {
792
+ api_key: auth.apiKey,
793
+ user_id: auth.userId,
794
+ expires: auth.expires.getTime(),
795
+ },
796
+ }
797
+ : null,
798
+ auth,
799
+ };
800
+ if (normalized.requiresAPIClient) {
801
+ // Recreate apiClient with auth credentials
802
+ ctx.apiClient = createAPIClient(baseCtx, ctx.config as Config | null);
803
+ }
804
+ if ((normalized.requiresRegion || normalized.optionalRegion) && ctx.apiClient) {
805
+ const apiClient: APIClientType = ctx.apiClient as APIClientType;
806
+ const region = await resolveRegion({
807
+ options: baseCtx.options as unknown as Record<string, unknown>,
808
+ apiClient,
809
+ logger: baseCtx.logger,
810
+ required: !!normalized.requiresRegion,
811
+ });
812
+ if (region) {
813
+ ctx.region = region;
814
+ }
815
+ }
816
+ await cmdDef.handler!(ctx as CommandContext);
817
+ } else if (normalized.optionalAuth) {
255
818
  const continueText =
256
- typeof cmdDef.optionalAuth === 'string' ? cmdDef.optionalAuth : undefined;
257
- const auth = await optionalAuth(baseCtx as CommandContext<false>, continueText);
258
- const ctx = { ...baseCtx, auth };
819
+ typeof normalized.optionalAuth === 'string'
820
+ ? normalized.optionalAuth
821
+ : undefined;
822
+
823
+ // Create apiClient before optionalAuth since login command needs it
824
+ if (normalized.requiresAPIClient) {
825
+ (baseCtx as Record<string, unknown>).apiClient = createAPIClient(
826
+ baseCtx,
827
+ baseCtx.config ?? null
828
+ );
829
+ }
830
+
831
+ const auth = await optionalAuth(
832
+ baseCtx as CommandContext<undefined>,
833
+ continueText
834
+ );
835
+ const ctx: Record<string, unknown> = {
836
+ ...baseCtx,
837
+ config: auth
838
+ ? baseCtx.config
839
+ ? {
840
+ ...baseCtx.config,
841
+ auth: {
842
+ api_key: auth.apiKey,
843
+ user_id: auth.userId,
844
+ expires: auth.expires.getTime(),
845
+ },
846
+ }
847
+ : {
848
+ auth: {
849
+ api_key: auth.apiKey,
850
+ user_id: auth.userId,
851
+ expires: auth.expires.getTime(),
852
+ },
853
+ }
854
+ : baseCtx.config,
855
+ auth,
856
+ };
857
+ if (normalized.requiresAPIClient) {
858
+ // Recreate apiClient with auth credentials if auth was provided
859
+ ctx.apiClient = createAPIClient(baseCtx, ctx.config as Config | null);
860
+ }
861
+ if ((normalized.requiresRegion || normalized.optionalRegion) && ctx.apiClient) {
862
+ const apiClient: APIClientType = ctx.apiClient as APIClientType;
863
+ const region = await resolveRegion({
864
+ options: baseCtx.options as unknown as Record<string, unknown>,
865
+ apiClient,
866
+ logger: baseCtx.logger,
867
+ required: !!normalized.requiresRegion,
868
+ });
869
+ if (region) {
870
+ ctx.region = region;
871
+ }
872
+ }
259
873
  await cmdDef.handler!(ctx as CommandContext);
260
874
  } else {
261
- await cmdDef.handler!(baseCtx as CommandContext<false>);
875
+ const ctx: Record<string, unknown> = {
876
+ ...baseCtx,
877
+ };
878
+ if (normalized.requiresAPIClient && !(ctx as CommandContext).apiClient) {
879
+ ctx.apiClient = createAPIClient(baseCtx, baseCtx.config);
880
+ }
881
+ if ((normalized.requiresRegion || normalized.optionalRegion) && ctx.apiClient) {
882
+ const apiClient = ctx.apiClient as APIClientType;
883
+ const region = await resolveRegion({
884
+ options: baseCtx.options as unknown as Record<string, unknown>,
885
+ apiClient,
886
+ logger: baseCtx.logger,
887
+ required: !!normalized.requiresRegion,
888
+ });
889
+ if (region) {
890
+ ctx.region = region;
891
+ }
892
+ }
893
+ await cmdDef.handler!(ctx as CommandContext);
262
894
  }
263
895
  });
264
896
  } else {