@agentuity/cli 1.0.39 → 1.0.41

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 (63) hide show
  1. package/dist/cli.d.ts.map +1 -1
  2. package/dist/cli.js +9 -1
  3. package/dist/cli.js.map +1 -1
  4. package/dist/cmd/build/app-router-detector.d.ts +42 -0
  5. package/dist/cmd/build/app-router-detector.d.ts.map +1 -0
  6. package/dist/cmd/build/app-router-detector.js +253 -0
  7. package/dist/cmd/build/app-router-detector.js.map +1 -0
  8. package/dist/cmd/build/ast.d.ts +11 -1
  9. package/dist/cmd/build/ast.d.ts.map +1 -1
  10. package/dist/cmd/build/ast.js +273 -29
  11. package/dist/cmd/build/ast.js.map +1 -1
  12. package/dist/cmd/build/entry-generator.d.ts.map +1 -1
  13. package/dist/cmd/build/entry-generator.js +23 -16
  14. package/dist/cmd/build/entry-generator.js.map +1 -1
  15. package/dist/cmd/build/vite/registry-generator.d.ts.map +1 -1
  16. package/dist/cmd/build/vite/registry-generator.js +37 -13
  17. package/dist/cmd/build/vite/registry-generator.js.map +1 -1
  18. package/dist/cmd/build/vite/route-discovery.d.ts +17 -3
  19. package/dist/cmd/build/vite/route-discovery.d.ts.map +1 -1
  20. package/dist/cmd/build/vite/route-discovery.js +91 -3
  21. package/dist/cmd/build/vite/route-discovery.js.map +1 -1
  22. package/dist/cmd/build/vite-bundler.d.ts.map +1 -1
  23. package/dist/cmd/build/vite-bundler.js +3 -0
  24. package/dist/cmd/build/vite-bundler.js.map +1 -1
  25. package/dist/cmd/dev/index.d.ts.map +1 -1
  26. package/dist/cmd/dev/index.js +30 -0
  27. package/dist/cmd/dev/index.js.map +1 -1
  28. package/dist/cmd/project/import.d.ts.map +1 -1
  29. package/dist/cmd/project/import.js +11 -1
  30. package/dist/cmd/project/import.js.map +1 -1
  31. package/dist/cmd/project/reconcile.d.ts +8 -0
  32. package/dist/cmd/project/reconcile.d.ts.map +1 -1
  33. package/dist/cmd/project/reconcile.js +150 -61
  34. package/dist/cmd/project/reconcile.js.map +1 -1
  35. package/dist/cmd/project/remote-import.d.ts +2 -0
  36. package/dist/cmd/project/remote-import.d.ts.map +1 -1
  37. package/dist/cmd/project/remote-import.js +2 -2
  38. package/dist/cmd/project/remote-import.js.map +1 -1
  39. package/dist/config.d.ts.map +1 -1
  40. package/dist/config.js +8 -1
  41. package/dist/config.js.map +1 -1
  42. package/dist/schema-parser.d.ts.map +1 -1
  43. package/dist/schema-parser.js +6 -2
  44. package/dist/schema-parser.js.map +1 -1
  45. package/dist/utils/route-migration.d.ts +61 -0
  46. package/dist/utils/route-migration.d.ts.map +1 -0
  47. package/dist/utils/route-migration.js +662 -0
  48. package/dist/utils/route-migration.js.map +1 -0
  49. package/package.json +6 -6
  50. package/src/cli.ts +9 -1
  51. package/src/cmd/build/app-router-detector.ts +350 -0
  52. package/src/cmd/build/ast.ts +339 -36
  53. package/src/cmd/build/entry-generator.ts +23 -16
  54. package/src/cmd/build/vite/registry-generator.ts +38 -13
  55. package/src/cmd/build/vite/route-discovery.ts +151 -3
  56. package/src/cmd/build/vite-bundler.ts +4 -0
  57. package/src/cmd/dev/index.ts +34 -0
  58. package/src/cmd/project/import.ts +11 -1
  59. package/src/cmd/project/reconcile.ts +147 -61
  60. package/src/cmd/project/remote-import.ts +4 -2
  61. package/src/config.ts +8 -1
  62. package/src/schema-parser.ts +8 -2
  63. package/src/utils/route-migration.ts +793 -0
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Route Discovery - READ-ONLY AST analysis
3
3
  *
4
- * Discovers routes by scanning src/api/**\/*.ts files
5
- * Extracts route definitions WITHOUT mutating source files
4
+ * Discovers routes by scanning src/api/**\/*.ts files or by following
5
+ * explicit router mounts from createApp({ router }).
6
+ * Extracts route definitions WITHOUT mutating source files.
6
7
  */
7
8
 
8
9
  import { join, relative } from 'node:path';
@@ -10,6 +11,7 @@ import { existsSync } from 'node:fs';
10
11
  import type { Logger } from '../../../types';
11
12
  import { parseRoute } from '../ast';
12
13
  import { toForwardSlash } from '../../../utils/normalize-path';
14
+ import { detectExplicitRouter, type AppRouterDetection } from '../app-router-detector';
13
15
 
14
16
  export interface RouteMetadata {
15
17
  id: string;
@@ -46,6 +48,12 @@ export interface RouteInfo {
46
48
  outputSchemaCode?: string;
47
49
  stream?: boolean;
48
50
  pathParams?: string[];
51
+ /**
52
+ * When a route is mounted via .route(), its filename is set to the parent file
53
+ * (for dedup filtering). schemaSourceFile preserves the actual file where the
54
+ * route's schema variables are defined/exported, so registry imports resolve correctly.
55
+ */
56
+ schemaSourceFile?: string;
49
57
  }
50
58
 
51
59
  /**
@@ -66,13 +74,152 @@ export function extractPathParams(path: string): string[] {
66
74
  }
67
75
 
68
76
  /**
69
- * Discover all routes in src/api directory (READ-ONLY)
77
+ * Discover all routes tries explicit router detection first, falls back to file-based.
78
+ *
79
+ * When `createApp({ router })` is detected in app.ts, routes are discovered by
80
+ * following the router imports with code-derived mount paths. Otherwise, falls back
81
+ * to scanning src/api/**\/*.ts with filesystem-derived paths.
70
82
  */
71
83
  export async function discoverRoutes(
72
84
  srcDir: string,
73
85
  projectId: string,
74
86
  deploymentId: string,
75
87
  logger: Logger
88
+ ): Promise<{
89
+ routes: RouteMetadata[];
90
+ routeInfoList: RouteInfo[];
91
+ /** Whether explicit router was detected (vs file-based fallback) */
92
+ explicitRouter?: AppRouterDetection;
93
+ }> {
94
+ const rootDir = join(srcDir, '..');
95
+
96
+ // Try explicit router detection first
97
+ const detection = await detectExplicitRouter(rootDir, logger);
98
+ if (detection.detected && detection.mounts.length > 0) {
99
+ logger.debug(
100
+ 'Using explicit router detection (%d mount(s) from createApp)',
101
+ detection.mounts.length
102
+ );
103
+ const result = await discoverExplicitRoutes(
104
+ rootDir,
105
+ srcDir,
106
+ projectId,
107
+ deploymentId,
108
+ detection,
109
+ logger
110
+ );
111
+ return { ...result, explicitRouter: detection };
112
+ }
113
+
114
+ // Fall back to file-based discovery
115
+ return discoverFileBasedRoutes(srcDir, projectId, deploymentId, logger);
116
+ }
117
+
118
+ /**
119
+ * Discover routes from explicit router mounts detected in app.ts.
120
+ * Parses each router file with its code-derived mount prefix.
121
+ */
122
+ async function discoverExplicitRoutes(
123
+ rootDir: string,
124
+ srcDir: string,
125
+ projectId: string,
126
+ deploymentId: string,
127
+ detection: AppRouterDetection,
128
+ logger: Logger
129
+ ): Promise<{ routes: RouteMetadata[]; routeInfoList: RouteInfo[] }> {
130
+ const routes: RouteMetadata[] = [];
131
+ const routeInfoList: RouteInfo[] = [];
132
+ const visited = new Set<string>();
133
+ const mountedSubrouters = new Set<string>();
134
+
135
+ for (const mount of detection.mounts) {
136
+ try {
137
+ const parsedRoutes = await parseRoute(rootDir, mount.routerFile, projectId, deploymentId, {
138
+ visitedFiles: visited,
139
+ mountedSubrouters,
140
+ mountPrefix: mount.path,
141
+ });
142
+
143
+ if (parsedRoutes.length > 0) {
144
+ const relFile = './' + toForwardSlash(relative(srcDir, mount.routerFile));
145
+ logger.trace(
146
+ 'Discovered %d route(s) from explicit mount at %s (%s)',
147
+ parsedRoutes.length,
148
+ mount.path,
149
+ relFile
150
+ );
151
+ routes.push(...parsedRoutes);
152
+
153
+ for (const route of parsedRoutes) {
154
+ const pathParams = extractPathParams(route.path);
155
+ routeInfoList.push({
156
+ method: route.method.toUpperCase(),
157
+ path: route.path,
158
+ filename: route.filename,
159
+ hasValidator: route.config?.hasValidator === true,
160
+ routeType: route.type || 'api',
161
+ agentVariable: route.config?.agentVariable as string | undefined,
162
+ agentImportPath: route.config?.agentImportPath as string | undefined,
163
+ inputSchemaVariable: route.config?.inputSchemaVariable as string | undefined,
164
+ outputSchemaVariable: route.config?.outputSchemaVariable as string | undefined,
165
+ inputSchemaImportPath: route.config?.inputSchemaImportPath as string | undefined,
166
+ inputSchemaImportedName: route.config?.inputSchemaImportedName as
167
+ | string
168
+ | undefined,
169
+ outputSchemaImportPath: route.config?.outputSchemaImportPath as
170
+ | string
171
+ | undefined,
172
+ outputSchemaImportedName: route.config?.outputSchemaImportedName as
173
+ | string
174
+ | undefined,
175
+ stream:
176
+ route.config?.stream !== undefined && route.config.stream !== null
177
+ ? Boolean(route.config.stream)
178
+ : route.type === 'stream'
179
+ ? true
180
+ : undefined,
181
+ pathParams: pathParams.length > 0 ? pathParams : undefined,
182
+ schemaSourceFile: route.config?.schemaSourceFile as string | undefined,
183
+ });
184
+ }
185
+ }
186
+ } catch (error) {
187
+ logger.warn(
188
+ 'Failed to parse explicit router at %s: %s',
189
+ mount.routerFile,
190
+ error instanceof Error ? error.message : String(error)
191
+ );
192
+ }
193
+ }
194
+
195
+ logger.debug('Discovered %d route(s) via explicit router detection', routes.length);
196
+
197
+ // Check for route conflicts
198
+ const conflicts = detectRouteConflicts(routeInfoList);
199
+ if (conflicts.length > 0) {
200
+ logger.error('Route conflicts detected:');
201
+ for (const conflict of conflicts) {
202
+ logger.error(' %s', conflict.message);
203
+ for (const route of conflict.routes) {
204
+ logger.error(' - %s %s in %s', route.method, route.path, route.filename);
205
+ }
206
+ }
207
+ throw new Error(
208
+ `Found ${conflicts.length} route conflict(s). Fix the conflicts and try again.`
209
+ );
210
+ }
211
+
212
+ return { routes, routeInfoList };
213
+ }
214
+
215
+ /**
216
+ * Discover routes by scanning src/api directory (original file-based approach).
217
+ */
218
+ async function discoverFileBasedRoutes(
219
+ srcDir: string,
220
+ projectId: string,
221
+ deploymentId: string,
222
+ logger: Logger
76
223
  ): Promise<{ routes: RouteMetadata[]; routeInfoList: RouteInfo[] }> {
77
224
  const apiDir = join(srcDir, 'api');
78
225
  const routes: RouteMetadata[] = [];
@@ -154,6 +301,7 @@ export async function discoverRoutes(
154
301
  ? true
155
302
  : undefined,
156
303
  pathParams: pathParams.length > 0 ? pathParams : undefined,
304
+ schemaSourceFile: route.config?.schemaSourceFile as string | undefined,
157
305
  });
158
306
  }
159
307
  }
@@ -10,6 +10,7 @@ import { StructuredError } from '@agentuity/core';
10
10
  import type { Logger, DeployOptions } from '../../types';
11
11
  import { runAllBuilds } from './vite/vite-builder';
12
12
  import { checkAndUpgradeDependencies } from '../../utils/dependency-checker';
13
+ import { promptRouteMigration } from '../../utils/route-migration';
13
14
  import { checkBunVersion } from '../../utils/bun-version-checker';
14
15
  import * as tui from '../../tui';
15
16
  import type { BuildReportCollector } from '../../build-report';
@@ -89,6 +90,9 @@ export async function viteBundle(options: ViteBundleOptions): Promise<{ output:
89
90
  });
90
91
  }
91
92
 
93
+ // Check if project can migrate from file-based to explicit routing
94
+ await promptRouteMigration(rootDir, logger);
95
+
92
96
  try {
93
97
  // Run all builds (client -> workbench -> server)
94
98
  logger.debug('Starting builds...');
@@ -20,6 +20,11 @@ import { isTTY, hasLoggedInBefore } from '../../auth';
20
20
  import { createFileWatcher } from './file-watcher';
21
21
  import { prepareDevLock, releaseLockSync } from './dev-lock';
22
22
  import { checkAndUpgradeDependencies } from '../../utils/dependency-checker';
23
+ import {
24
+ promptRouteMigration,
25
+ performMigration,
26
+ checkMigrationEligibility,
27
+ } from '../../utils/route-migration';
23
28
  import { ErrorCode } from '../../errors';
24
29
 
25
30
  const DEFAULT_PORT = 3500;
@@ -232,6 +237,12 @@ export const command = createCommand({
232
237
  .boolean()
233
238
  .optional()
234
239
  .describe('Skip TypeScript type checking on startup and restarts'),
240
+ migrateRoutes: z
241
+ .boolean()
242
+ .optional()
243
+ .describe(
244
+ 'Migrate file-based routes to explicit routing (src/api/index.ts root router)'
245
+ ),
235
246
  resume: z.string().optional().describe('Resume a paused Hub session by ID'),
236
247
  }),
237
248
  },
@@ -414,6 +425,29 @@ export const command = createCommand({
414
425
  );
415
426
  }
416
427
 
428
+ // Check if project can migrate to explicit routing
429
+ if (opts.migrateRoutes) {
430
+ const eligibility = checkMigrationEligibility(rootDir);
431
+ if (eligibility.available) {
432
+ const result = performMigration(rootDir, eligibility.routeFiles);
433
+ if (result.success) {
434
+ tui.success(result.message);
435
+ if (result.filesCreated.length > 0) {
436
+ tui.info(`Created: ${result.filesCreated.map((f) => tui.muted(f)).join(', ')}`);
437
+ }
438
+ tui.newline();
439
+ } else {
440
+ tui.warning(result.message);
441
+ tui.newline();
442
+ }
443
+ } else {
444
+ tui.info('No migration needed — already using explicit routing.');
445
+ tui.newline();
446
+ }
447
+ } else {
448
+ await promptRouteMigration(rootDir, logger, { interactive });
449
+ }
450
+
417
451
  try {
418
452
  // Setup devmode and gravity (if using public URL)
419
453
  const useMockService = process.env.DEVMODE_SYNC_SERVICE_MOCK === 'true';
@@ -46,6 +46,10 @@ export const importSubcommand = createSubcommand({
46
46
  ),
47
47
  description: 'Import with resource provisioning and push to new repo',
48
48
  },
49
+ {
50
+ command: getCommand('project import --name my-agent --confirm'),
51
+ description: 'Import project non-interactively, skipping prompts',
52
+ },
49
53
  ],
50
54
  requires: { auth: true, apiClient: true },
51
55
  optional: { region: true, org: true },
@@ -71,6 +75,7 @@ export const importSubcommand = createSubcommand({
71
75
  .optional()
72
76
  .describe('Target GitHub repo (owner/repo) to push imported code to'),
73
77
  name: z.string().optional().describe('Project name (for non-interactive mode)'),
78
+ confirm: z.boolean().optional().describe('Skip confirmation prompts'),
74
79
  env: z
75
80
  .array(z.string())
76
81
  .optional()
@@ -100,6 +105,7 @@ export const importSubcommand = createSubcommand({
100
105
  env: opts.env,
101
106
  org: orgId,
102
107
  region,
108
+ confirm: opts.confirm,
103
109
  apiClient,
104
110
  auth,
105
111
  config,
@@ -123,8 +129,12 @@ export const importSubcommand = createSubcommand({
123
129
  apiClient,
124
130
  config,
125
131
  logger,
126
- interactive: validateOnly ? false : isTTY(),
132
+ interactive: validateOnly ? false : opts.confirm ? false : isTTY(),
127
133
  validateOnly,
134
+ confirm: opts.confirm === true,
135
+ orgId,
136
+ region,
137
+ name: opts.name,
128
138
  });
129
139
 
130
140
  if (result.status === 'error') {
@@ -40,6 +40,14 @@ export interface ReconcileOptions {
40
40
  interactive?: boolean;
41
41
  /** If true, skip prompts and just validate */
42
42
  validateOnly?: boolean;
43
+ /** If true, auto-confirm all prompts (--confirm flag) */
44
+ confirm?: boolean;
45
+ /** Pre-selected organization ID (skips org selection prompt) */
46
+ orgId?: string;
47
+ /** Pre-selected region (skips region selection prompt) */
48
+ region?: string;
49
+ /** Project name from --name flag */
50
+ name?: string;
43
51
  }
44
52
 
45
53
  /**
@@ -315,7 +323,7 @@ async function importExistingProject(
315
323
  ): Promise<ReconcileResult> {
316
324
  const { dir, apiClient, config, logger } = opts;
317
325
 
318
- if (!options?.skipPrompt) {
326
+ if (!options?.skipPrompt && opts.interactive !== false) {
319
327
  tui.warning(
320
328
  "You don't have access to this project. It may have been deleted or transferred to another organization."
321
329
  );
@@ -333,29 +341,67 @@ async function importExistingProject(
333
341
  tui.newline();
334
342
  }
335
343
 
336
- // Select org
337
- const orgId = await selectOrg(orgs, config, existingConfig.orgId);
344
+ // Select org - use --org-id if provided, otherwise auto-select or prompt
345
+ let orgId: string;
346
+ if (opts.orgId) {
347
+ orgId = opts.orgId;
348
+ } else if (opts.confirm) {
349
+ orgId = await tui.selectOrganization(orgs, config.preferences?.orgId, true);
350
+ } else {
351
+ orgId = await selectOrg(orgs, config, existingConfig.orgId);
352
+ }
338
353
 
339
- // Fetch regions and select
340
- const regions = await tui.spinner({
341
- message: 'Fetching regions',
342
- clearOnSuccess: true,
343
- callback: () => fetchRegionsWithCache(config.name, apiClient, logger),
344
- });
345
- const region = await selectRegion(regions, existingConfig.region);
354
+ // Select region (use pre-selected if available, otherwise fetch and prompt/auto-select)
355
+ let region: string;
356
+ if (opts.region) {
357
+ region = opts.region;
358
+ } else {
359
+ const regions = await tui.spinner({
360
+ message: 'Fetching regions',
361
+ clearOnSuccess: true,
362
+ callback: () => fetchRegionsWithCache(config.name, apiClient, logger),
363
+ });
364
+
365
+ if (opts.confirm) {
366
+ // Auto-select: use existing config region if valid, otherwise first available
367
+ const defaultRegion = existingConfig.region;
368
+ if (defaultRegion && regions.some((r) => r.region === defaultRegion)) {
369
+ region = defaultRegion;
370
+ } else {
371
+ const firstRegion = regions[0];
372
+ if (!firstRegion) {
373
+ return { status: 'error', message: 'No cloud regions available.' };
374
+ }
375
+ region = firstRegion.region;
376
+ }
377
+ } else {
378
+ region = await selectRegion(regions, existingConfig.region);
379
+ }
380
+ }
346
381
 
347
382
  // Get project name
348
383
  const defaultName = await getDefaultProjectName(dir);
349
- const projectName = await textPrompt({
350
- message: 'Project name:',
351
- initial: defaultName,
352
- validate: (value: string) => {
353
- if (!value || value.trim().length === 0) {
354
- return 'Project name is required';
355
- }
356
- return true;
357
- },
358
- });
384
+ let projectName: string;
385
+ if (opts.name) {
386
+ const trimmed = opts.name.trim();
387
+ if (trimmed.length === 0) {
388
+ return { status: 'error', message: 'Project name is required.' };
389
+ }
390
+ projectName = trimmed;
391
+ } else if (opts.confirm) {
392
+ projectName = defaultName;
393
+ } else {
394
+ projectName = await textPrompt({
395
+ message: 'Project name:',
396
+ initial: defaultName,
397
+ validate: (value: string) => {
398
+ if (!value || value.trim().length === 0) {
399
+ return 'Project name is required';
400
+ }
401
+ return true;
402
+ },
403
+ });
404
+ }
359
405
 
360
406
  // Create the project
361
407
  const newProject = await tui.spinner({
@@ -407,51 +453,87 @@ async function importExistingProject(
407
453
  async function createNewProject(opts: ReconcileOptions): Promise<ReconcileResult> {
408
454
  const { dir, apiClient, config, logger } = opts;
409
455
 
410
- tui.warning('This project is not registered with Agentuity Cloud.');
411
- tui.newline();
456
+ if (opts.interactive !== false) {
457
+ tui.warning('This project is not registered with Agentuity Cloud.');
458
+ tui.newline();
459
+
460
+ const shouldCreate = await tui.confirm('Would you like to register it now?', true);
412
461
 
413
- const shouldCreate = await tui.confirm('Would you like to register it now?', true);
462
+ if (!shouldCreate) {
463
+ return { status: 'skipped', message: 'Project registration cancelled.' };
464
+ }
414
465
 
415
- if (!shouldCreate) {
416
- return { status: 'skipped', message: 'Project registration cancelled.' };
466
+ tui.newline();
417
467
  }
418
468
 
419
- tui.newline();
420
-
421
- // Fetch user's orgs
422
- const orgs = await tui.spinner({
423
- message: 'Fetching organizations',
424
- clearOnSuccess: true,
425
- callback: () => listOrganizations(apiClient),
426
- });
469
+ // Select org - use --org-id if provided, otherwise fetch and select/auto-select
470
+ let orgId: string;
471
+ if (opts.orgId) {
472
+ orgId = opts.orgId;
473
+ } else {
474
+ // Fetch user's orgs
475
+ const orgs = await tui.spinner({
476
+ message: 'Fetching organizations',
477
+ clearOnSuccess: true,
478
+ callback: () => listOrganizations(apiClient),
479
+ });
480
+
481
+ if (orgs.length === 0) {
482
+ return { status: 'error', message: 'No organizations found for your account.' };
483
+ }
427
484
 
428
- if (orgs.length === 0) {
429
- return { status: 'error', message: 'No organizations found for your account.' };
485
+ if (opts.confirm) {
486
+ orgId = await tui.selectOrganization(orgs, config.preferences?.orgId, true);
487
+ } else {
488
+ orgId = await selectOrg(orgs, config);
489
+ }
430
490
  }
431
491
 
432
- // Select org
433
- const orgId = await selectOrg(orgs, config);
434
-
435
- // Fetch regions and select
436
- const regions = await tui.spinner({
437
- message: 'Fetching regions',
438
- clearOnSuccess: true,
439
- callback: () => fetchRegionsWithCache(config.name, apiClient, logger),
440
- });
441
- const region = await selectRegion(regions);
492
+ // Select region (use pre-selected if available, otherwise fetch and prompt/auto-select)
493
+ let region: string;
494
+ if (opts.region) {
495
+ region = opts.region;
496
+ } else {
497
+ const regions = await tui.spinner({
498
+ message: 'Fetching regions',
499
+ clearOnSuccess: true,
500
+ callback: () => fetchRegionsWithCache(config.name, apiClient, logger),
501
+ });
502
+
503
+ if (opts.confirm) {
504
+ const firstRegion = regions[0];
505
+ if (!firstRegion) {
506
+ return { status: 'error', message: 'No cloud regions available.' };
507
+ }
508
+ region = firstRegion.region;
509
+ } else {
510
+ region = await selectRegion(regions);
511
+ }
512
+ }
442
513
 
443
514
  // Get project name from package.json or prompt
444
515
  const defaultName = await getDefaultProjectName(dir);
445
- const projectName = await textPrompt({
446
- message: 'Project name:',
447
- initial: defaultName,
448
- validate: (value: string) => {
449
- if (!value || value.trim().length === 0) {
450
- return 'Project name is required';
451
- }
452
- return true;
453
- },
454
- });
516
+ let projectName: string;
517
+ if (opts.name) {
518
+ const trimmed = opts.name.trim();
519
+ if (trimmed.length === 0) {
520
+ return { status: 'error', message: 'Project name is required.' };
521
+ }
522
+ projectName = trimmed;
523
+ } else if (opts.confirm) {
524
+ projectName = defaultName;
525
+ } else {
526
+ projectName = await textPrompt({
527
+ message: 'Project name:',
528
+ initial: defaultName,
529
+ validate: (value: string) => {
530
+ if (!value || value.trim().length === 0) {
531
+ return 'Project name is required';
532
+ }
533
+ return true;
534
+ },
535
+ });
536
+ }
455
537
 
456
538
  // Create the project
457
539
  const newProject = await tui.spinner({
@@ -627,7 +709,7 @@ export async function runProjectImport(opts: ReconcileOptions): Promise<Reconcil
627
709
  }
628
710
 
629
711
  // Has agentuity.json but no access - offer to import
630
- if (!interactive) {
712
+ if (!interactive && !opts.confirm) {
631
713
  return {
632
714
  status: 'error',
633
715
  message:
@@ -635,10 +717,12 @@ export async function runProjectImport(opts: ReconcileOptions): Promise<Reconcil
635
717
  };
636
718
  }
637
719
 
638
- return await importExistingProject(opts, projectConfig, userOrgs);
720
+ return await importExistingProject(opts, projectConfig, userOrgs, {
721
+ skipPrompt: opts.confirm,
722
+ });
639
723
  } catch {
640
724
  // Project doesn't exist - offer to import
641
- if (!interactive) {
725
+ if (!interactive && !opts.confirm) {
642
726
  return {
643
727
  status: 'error',
644
728
  message: 'Project not found. Run interactively to import it to your organization.',
@@ -646,14 +730,16 @@ export async function runProjectImport(opts: ReconcileOptions): Promise<Reconcil
646
730
  }
647
731
 
648
732
  const userOrgs = await listOrganizations(apiClient);
649
- return await importExistingProject(opts, projectConfig, userOrgs);
733
+ return await importExistingProject(opts, projectConfig, userOrgs, {
734
+ skipPrompt: opts.confirm,
735
+ });
650
736
  }
651
737
  }
652
738
 
653
739
  // No agentuity.json - validate structure and create new project
654
740
  const isValid = await isValidProjectStructure(dir);
655
741
 
656
- if (!isValid) {
742
+ if (!isValid && !opts.confirm) {
657
743
  return {
658
744
  status: 'error',
659
745
  message:
@@ -670,7 +756,7 @@ export async function runProjectImport(opts: ReconcileOptions): Promise<Reconcil
670
756
  };
671
757
  }
672
758
 
673
- if (!interactive) {
759
+ if (!interactive && !opts.confirm) {
674
760
  return {
675
761
  status: 'error',
676
762
  message: 'Project import requires interactive mode.',
@@ -62,6 +62,8 @@ export interface RemoteImportOptions {
62
62
  env?: string[];
63
63
  org?: string;
64
64
  region?: string;
65
+ /** If true, skip confirmation prompts (--confirm flag) */
66
+ confirm?: boolean;
65
67
  apiClient: APIClient;
66
68
  auth: AuthData;
67
69
  config: Config;
@@ -744,7 +746,7 @@ export async function runRemoteImport(options: RemoteImportOptions): Promise<voi
744
746
  optRegion,
745
747
  org
746
748
  );
747
- } else if (isTTY()) {
749
+ } else if (isTTY() && !options.confirm) {
748
750
  // Interactive mode: prompt for org/region/name
749
751
  projectInfo = await createProjectInteractive(apiClient, config, logger, parsed.repo);
750
752
  } else {
@@ -893,7 +895,7 @@ export async function runRemoteImport(options: RemoteImportOptions): Promise<voi
893
895
  }
894
896
 
895
897
  const resourceEnvVars: Record<string, string> = {};
896
- const interactive = isTTY();
898
+ const interactive = isTTY() && !options.confirm;
897
899
  const templateResources = template?.requirements?.resources ?? [];
898
900
  const templateEnvVars = template?.requirements?.env ?? [];
899
901
 
package/src/config.ts CHANGED
@@ -141,6 +141,8 @@ function expandTilde(path: string): string {
141
141
  }
142
142
 
143
143
  let cachedConfig: Config | null | undefined;
144
+ // Track the resolved config path so saveConfig writes back to the same file
145
+ let cachedConfigPath: string | undefined;
144
146
 
145
147
  export async function loadConfig(
146
148
  customPath?: string,
@@ -217,6 +219,7 @@ export async function loadConfig(
217
219
  // This ensures --config flag is respected across all commands
218
220
  if (!skipCache) {
219
221
  cachedConfig = result.data;
222
+ cachedConfigPath = configPath;
220
223
  }
221
224
  return result.data;
222
225
  } catch (error) {
@@ -227,6 +230,7 @@ export async function loadConfig(
227
230
  // Note: For long-running processes, consider time-based cache expiry for transient failures.
228
231
  if (!skipCache) {
229
232
  cachedConfig = null;
233
+ cachedConfigPath = configPath;
230
234
  }
231
235
  return null;
232
236
  }
@@ -275,7 +279,10 @@ function formatYAML(obj: unknown, indent = 0): string {
275
279
  }
276
280
 
277
281
  export async function saveConfig(config: Config, customPath?: string): Promise<void> {
278
- const configPath = customPath || (await getProfile());
282
+ // Use the path the config was originally loaded from (cachedConfigPath) so that
283
+ // saves go back to the correct profile even when --profile was used to load it.
284
+ // Falls back to getProfile() if no config has been loaded yet.
285
+ const configPath = customPath || cachedConfigPath || (await getProfile());
279
286
  await ensureConfigDir();
280
287
 
281
288
  const content = formatYAML(config);
@@ -443,8 +443,14 @@ export function buildValidationInput(
443
443
  const camelCaseName = opt.name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
444
444
  let value = rawOptions[opt.name] ?? rawOptions[camelCaseName];
445
445
 
446
- // Handle --yes alias for --confirm: if confirm is not set but yes is, use yes value
447
- if (opt.name === 'confirm' && value === undefined && rawOptions.yes === true) {
446
+ // Handle --yes and --force aliases for --confirm: if confirm is not set but yes/force is, use that value
447
+ // Only treat --force as a --confirm alias if the schema does NOT declare a separate 'force' option
448
+ if (
449
+ opt.name === 'confirm' &&
450
+ value === undefined &&
451
+ (rawOptions.yes === true ||
452
+ (rawOptions.force === true && !parsed.some((o) => o.name === 'force')))
453
+ ) {
448
454
  value = true;
449
455
  }
450
456