@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.
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +9 -1
- package/dist/cli.js.map +1 -1
- package/dist/cmd/build/app-router-detector.d.ts +42 -0
- package/dist/cmd/build/app-router-detector.d.ts.map +1 -0
- package/dist/cmd/build/app-router-detector.js +253 -0
- package/dist/cmd/build/app-router-detector.js.map +1 -0
- package/dist/cmd/build/ast.d.ts +11 -1
- package/dist/cmd/build/ast.d.ts.map +1 -1
- package/dist/cmd/build/ast.js +273 -29
- package/dist/cmd/build/ast.js.map +1 -1
- package/dist/cmd/build/entry-generator.d.ts.map +1 -1
- package/dist/cmd/build/entry-generator.js +23 -16
- package/dist/cmd/build/entry-generator.js.map +1 -1
- package/dist/cmd/build/vite/registry-generator.d.ts.map +1 -1
- package/dist/cmd/build/vite/registry-generator.js +37 -13
- package/dist/cmd/build/vite/registry-generator.js.map +1 -1
- package/dist/cmd/build/vite/route-discovery.d.ts +17 -3
- package/dist/cmd/build/vite/route-discovery.d.ts.map +1 -1
- package/dist/cmd/build/vite/route-discovery.js +91 -3
- package/dist/cmd/build/vite/route-discovery.js.map +1 -1
- package/dist/cmd/build/vite-bundler.d.ts.map +1 -1
- package/dist/cmd/build/vite-bundler.js +3 -0
- package/dist/cmd/build/vite-bundler.js.map +1 -1
- package/dist/cmd/dev/index.d.ts.map +1 -1
- package/dist/cmd/dev/index.js +30 -0
- package/dist/cmd/dev/index.js.map +1 -1
- package/dist/cmd/project/import.d.ts.map +1 -1
- package/dist/cmd/project/import.js +11 -1
- package/dist/cmd/project/import.js.map +1 -1
- package/dist/cmd/project/reconcile.d.ts +8 -0
- package/dist/cmd/project/reconcile.d.ts.map +1 -1
- package/dist/cmd/project/reconcile.js +150 -61
- package/dist/cmd/project/reconcile.js.map +1 -1
- package/dist/cmd/project/remote-import.d.ts +2 -0
- package/dist/cmd/project/remote-import.d.ts.map +1 -1
- package/dist/cmd/project/remote-import.js +2 -2
- package/dist/cmd/project/remote-import.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -1
- package/dist/config.js.map +1 -1
- package/dist/schema-parser.d.ts.map +1 -1
- package/dist/schema-parser.js +6 -2
- package/dist/schema-parser.js.map +1 -1
- package/dist/utils/route-migration.d.ts +61 -0
- package/dist/utils/route-migration.d.ts.map +1 -0
- package/dist/utils/route-migration.js +662 -0
- package/dist/utils/route-migration.js.map +1 -0
- package/package.json +6 -6
- package/src/cli.ts +9 -1
- package/src/cmd/build/app-router-detector.ts +350 -0
- package/src/cmd/build/ast.ts +339 -36
- package/src/cmd/build/entry-generator.ts +23 -16
- package/src/cmd/build/vite/registry-generator.ts +38 -13
- package/src/cmd/build/vite/route-discovery.ts +151 -3
- package/src/cmd/build/vite-bundler.ts +4 -0
- package/src/cmd/dev/index.ts +34 -0
- package/src/cmd/project/import.ts +11 -1
- package/src/cmd/project/reconcile.ts +147 -61
- package/src/cmd/project/remote-import.ts +4 -2
- package/src/config.ts +8 -1
- package/src/schema-parser.ts +8 -2
- 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
|
-
*
|
|
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
|
|
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...');
|
package/src/cmd/dev/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
411
|
-
|
|
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
|
-
|
|
462
|
+
if (!shouldCreate) {
|
|
463
|
+
return { status: 'skipped', message: 'Project registration cancelled.' };
|
|
464
|
+
}
|
|
414
465
|
|
|
415
|
-
|
|
416
|
-
return { status: 'skipped', message: 'Project registration cancelled.' };
|
|
466
|
+
tui.newline();
|
|
417
467
|
}
|
|
418
468
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
|
|
429
|
-
|
|
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
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
|
|
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);
|
package/src/schema-parser.ts
CHANGED
|
@@ -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
|
|
447
|
-
|
|
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
|
|