@agentuity/cli 1.0.40 → 1.0.42
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/ast.d.ts.map +1 -1
- package/dist/cmd/build/ast.js +3 -3
- package/dist/cmd/build/ast.js.map +1 -1
- package/dist/cmd/build/typecheck.d.ts.map +1 -1
- package/dist/cmd/build/typecheck.js +52 -1
- package/dist/cmd/build/typecheck.js.map +1 -1
- package/dist/cmd/build/vite/static-renderer.d.ts.map +1 -1
- package/dist/cmd/build/vite/static-renderer.js +22 -8
- package/dist/cmd/build/vite/static-renderer.js.map +1 -1
- package/dist/cmd/cloud/index.d.ts.map +1 -1
- package/dist/cmd/cloud/index.js +4 -0
- package/dist/cmd/cloud/index.js.map +1 -1
- package/dist/cmd/cloud/monitor.d.ts +3 -0
- package/dist/cmd/cloud/monitor.d.ts.map +1 -0
- package/dist/cmd/cloud/monitor.js +300 -0
- package/dist/cmd/cloud/monitor.js.map +1 -0
- package/dist/cmd/cloud/oidc/activity.d.ts +2 -0
- package/dist/cmd/cloud/oidc/activity.d.ts.map +1 -0
- package/dist/cmd/cloud/oidc/activity.js +54 -0
- package/dist/cmd/cloud/oidc/activity.js.map +1 -0
- package/dist/cmd/cloud/oidc/create.d.ts +2 -0
- package/dist/cmd/cloud/oidc/create.d.ts.map +1 -0
- package/dist/cmd/cloud/oidc/create.js +201 -0
- package/dist/cmd/cloud/oidc/create.js.map +1 -0
- package/dist/cmd/cloud/oidc/delete.d.ts +2 -0
- package/dist/cmd/cloud/oidc/delete.d.ts.map +1 -0
- package/dist/cmd/cloud/oidc/delete.js +56 -0
- package/dist/cmd/cloud/oidc/delete.js.map +1 -0
- package/dist/cmd/cloud/oidc/get.d.ts +2 -0
- package/dist/cmd/cloud/oidc/get.d.ts.map +1 -0
- package/dist/cmd/cloud/oidc/get.js +59 -0
- package/dist/cmd/cloud/oidc/get.js.map +1 -0
- package/dist/cmd/cloud/oidc/index.d.ts +3 -0
- package/dist/cmd/cloud/oidc/index.d.ts.map +1 -0
- package/dist/cmd/cloud/oidc/index.js +32 -0
- package/dist/cmd/cloud/oidc/index.js.map +1 -0
- package/dist/cmd/cloud/oidc/list.d.ts +2 -0
- package/dist/cmd/cloud/oidc/list.d.ts.map +1 -0
- package/dist/cmd/cloud/oidc/list.js +45 -0
- package/dist/cmd/cloud/oidc/list.js.map +1 -0
- package/dist/cmd/cloud/oidc/rotate-secret.d.ts +2 -0
- package/dist/cmd/cloud/oidc/rotate-secret.d.ts.map +1 -0
- package/dist/cmd/cloud/oidc/rotate-secret.js +63 -0
- package/dist/cmd/cloud/oidc/rotate-secret.js.map +1 -0
- package/dist/cmd/cloud/oidc/users.d.ts +2 -0
- package/dist/cmd/cloud/oidc/users.d.ts.map +1 -0
- package/dist/cmd/cloud/oidc/users.js +50 -0
- package/dist/cmd/cloud/oidc/users.js.map +1 -0
- 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 +22 -6
- 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/jsonc.d.ts +13 -0
- package/dist/utils/jsonc.d.ts.map +1 -0
- package/dist/utils/jsonc.js +63 -0
- package/dist/utils/jsonc.js.map +1 -0
- package/dist/utils/route-migration.d.ts +2 -1
- package/dist/utils/route-migration.d.ts.map +1 -1
- package/dist/utils/route-migration.js +23 -32
- package/dist/utils/route-migration.js.map +1 -1
- package/dist/utils/zip.d.ts.map +1 -1
- package/dist/utils/zip.js +18 -2
- package/dist/utils/zip.js.map +1 -1
- package/package.json +6 -7
- package/src/cli.ts +9 -1
- package/src/cmd/build/ast.ts +6 -3
- package/src/cmd/build/typecheck.ts +60 -1
- package/src/cmd/build/vite/static-renderer.ts +24 -8
- package/src/cmd/cloud/index.ts +4 -0
- package/src/cmd/cloud/monitor.ts +375 -0
- package/src/cmd/cloud/oidc/activity.ts +61 -0
- package/src/cmd/cloud/oidc/create.ts +232 -0
- package/src/cmd/cloud/oidc/delete.ts +63 -0
- package/src/cmd/cloud/oidc/get.ts +65 -0
- package/src/cmd/cloud/oidc/index.ts +35 -0
- package/src/cmd/cloud/oidc/list.ts +50 -0
- package/src/cmd/cloud/oidc/rotate-secret.ts +77 -0
- package/src/cmd/cloud/oidc/users.ts +57 -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 +24 -6
- package/src/schema-parser.ts +8 -2
- package/src/utils/jsonc.ts +67 -0
- package/src/utils/route-migration.ts +29 -40
- package/src/utils/zip.ts +17 -2
|
@@ -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
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
APIClient as ServerAPIClient,
|
|
11
11
|
} from '@agentuity/server';
|
|
12
12
|
import { YAML } from 'bun';
|
|
13
|
-
import
|
|
13
|
+
import { parseJSONC } from './utils/jsonc';
|
|
14
14
|
import { z } from 'zod';
|
|
15
15
|
import { clearProfileCache } from './cache';
|
|
16
16
|
import { getCatalystUrl } from './catalyst';
|
|
@@ -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
|
}
|
|
@@ -261,8 +265,19 @@ function formatYAML(obj: unknown, indent = 0): string {
|
|
|
261
265
|
} else if (typeof value === 'string') {
|
|
262
266
|
if (value === '') {
|
|
263
267
|
lines.push(`${spaces}${key}: ""`);
|
|
264
|
-
} else if (
|
|
265
|
-
|
|
268
|
+
} else if (
|
|
269
|
+
value.includes(':') ||
|
|
270
|
+
value.includes('#') ||
|
|
271
|
+
value.includes(' ') ||
|
|
272
|
+
value.includes('\\')
|
|
273
|
+
) {
|
|
274
|
+
// Use single quotes to avoid YAML escape-sequence processing.
|
|
275
|
+
// Double-quoted YAML strings interpret backslash sequences (\n, \t, etc.),
|
|
276
|
+
// which breaks Windows paths like C:\Users\... where \U would be invalid.
|
|
277
|
+
// Single-quoted strings treat backslashes literally.
|
|
278
|
+
// Escape any embedded single quotes by doubling them (YAML spec).
|
|
279
|
+
const escaped = value.replace(/'/g, "''");
|
|
280
|
+
lines.push(`${spaces}${key}: '${escaped}'`);
|
|
266
281
|
} else {
|
|
267
282
|
lines.push(`${spaces}${key}: ${value}`);
|
|
268
283
|
}
|
|
@@ -275,7 +290,10 @@ function formatYAML(obj: unknown, indent = 0): string {
|
|
|
275
290
|
}
|
|
276
291
|
|
|
277
292
|
export async function saveConfig(config: Config, customPath?: string): Promise<void> {
|
|
278
|
-
|
|
293
|
+
// Use the path the config was originally loaded from (cachedConfigPath) so that
|
|
294
|
+
// saves go back to the correct profile even when --profile was used to load it.
|
|
295
|
+
// Falls back to getProfile() if no config has been loaded yet.
|
|
296
|
+
const configPath = customPath || cachedConfigPath || (await getProfile());
|
|
279
297
|
await ensureConfigDir();
|
|
280
298
|
|
|
281
299
|
const content = formatYAML(config);
|
|
@@ -595,7 +613,7 @@ export async function loadProjectConfig(
|
|
|
595
613
|
throw new ProjectConfigNotFoundException({ message: 'project config not found' });
|
|
596
614
|
}
|
|
597
615
|
const text = await file.text();
|
|
598
|
-
const parsedConfig =
|
|
616
|
+
const parsedConfig = parseJSONC(text);
|
|
599
617
|
const result = ProjectSchema.safeParse(parsedConfig);
|
|
600
618
|
if (!result.success) {
|
|
601
619
|
tui.error(`Invalid project config at ${configPath}:`);
|
|
@@ -700,7 +718,7 @@ export async function updateProjectConfig(
|
|
|
700
718
|
}
|
|
701
719
|
|
|
702
720
|
const text = await file.text();
|
|
703
|
-
const existing =
|
|
721
|
+
const existing = parseJSONC(text) as Record<string, unknown>;
|
|
704
722
|
const updated = { ...existing, ...updates };
|
|
705
723
|
|
|
706
724
|
const result = ProjectSchema.safeParse(updated);
|
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
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse JSON with Comments (JSONC).
|
|
3
|
+
*
|
|
4
|
+
* Strips single-line (`//`) and block (`/* */`) comments as well as trailing
|
|
5
|
+
* commas that appear before `}` or `]`, then delegates to the built-in
|
|
6
|
+
* `JSON.parse`. This covers the comment syntax used by `tsconfig.json` and
|
|
7
|
+
* similar config files without pulling in a full JSON5 parser.
|
|
8
|
+
*
|
|
9
|
+
* String literals are respected — comments and trailing commas inside quoted
|
|
10
|
+
* strings are left untouched.
|
|
11
|
+
*/
|
|
12
|
+
export function parseJSONC(text: string): unknown {
|
|
13
|
+
let result = '';
|
|
14
|
+
let i = 0;
|
|
15
|
+
const len = text.length;
|
|
16
|
+
|
|
17
|
+
while (i < len) {
|
|
18
|
+
const ch = text[i];
|
|
19
|
+
|
|
20
|
+
// --- quoted string: copy verbatim, including any escape sequences ---
|
|
21
|
+
if (ch === '"') {
|
|
22
|
+
const start = i;
|
|
23
|
+
i++; // skip opening quote
|
|
24
|
+
while (i < len) {
|
|
25
|
+
if (text[i] === '\\') {
|
|
26
|
+
i += i + 1 < len ? 2 : 1; // skip escaped character (guard end-of-input)
|
|
27
|
+
} else if (text[i] === '"') {
|
|
28
|
+
i++; // skip closing quote
|
|
29
|
+
break;
|
|
30
|
+
} else {
|
|
31
|
+
i++;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
result += text.slice(start, i);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// --- single-line comment: skip to end of line ---
|
|
39
|
+
if (ch === '/' && text[i + 1] === '/') {
|
|
40
|
+
i += 2;
|
|
41
|
+
while (i < len && text[i] !== '\n') {
|
|
42
|
+
i++;
|
|
43
|
+
}
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// --- block comment: skip to closing */ ---
|
|
48
|
+
if (ch === '/' && text[i + 1] === '*') {
|
|
49
|
+
i += 2;
|
|
50
|
+
while (i < len && !(text[i] === '*' && text[i + 1] === '/')) {
|
|
51
|
+
i++;
|
|
52
|
+
}
|
|
53
|
+
if (i < len) {
|
|
54
|
+
i += 2; // skip closing */
|
|
55
|
+
}
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
result += ch;
|
|
60
|
+
i++;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Strip trailing commas before } or ] (with optional whitespace between).
|
|
64
|
+
result = result.replace(/,(\s*[}\]])/g, '$1');
|
|
65
|
+
|
|
66
|
+
return JSON.parse(result);
|
|
67
|
+
}
|
|
@@ -697,7 +697,8 @@ export function performMigration(rootDir: string, routeFiles: string[]): Migrati
|
|
|
697
697
|
* Show the migration notice and optionally perform migration.
|
|
698
698
|
*
|
|
699
699
|
* Called during `dev` and `build` after dependency upgrades.
|
|
700
|
-
*
|
|
700
|
+
* Only prompts in interactive TTY sessions and only once — if the user
|
|
701
|
+
* dismisses the prompt, it won't be shown again.
|
|
701
702
|
*
|
|
702
703
|
* @returns true if migration was performed, false otherwise
|
|
703
704
|
*/
|
|
@@ -707,6 +708,12 @@ export async function promptRouteMigration(
|
|
|
707
708
|
options?: { interactive?: boolean }
|
|
708
709
|
): Promise<boolean> {
|
|
709
710
|
const interactive = options?.interactive ?? process.stdin.isTTY;
|
|
711
|
+
|
|
712
|
+
// Only show the interactive migration prompt in TTY sessions
|
|
713
|
+
if (!interactive) {
|
|
714
|
+
return false;
|
|
715
|
+
}
|
|
716
|
+
|
|
710
717
|
const eligibility = checkMigrationEligibility(rootDir);
|
|
711
718
|
|
|
712
719
|
if (!eligibility.available) {
|
|
@@ -715,48 +722,30 @@ export async function promptRouteMigration(
|
|
|
715
722
|
|
|
716
723
|
const { routeFiles, alreadyNotified } = eligibility;
|
|
717
724
|
|
|
718
|
-
//
|
|
719
|
-
if (
|
|
720
|
-
if (!alreadyNotified) {
|
|
721
|
-
logger.info(
|
|
722
|
-
'[migration] This project uses file-based routing with %d route files in src/api/. ' +
|
|
723
|
-
'Agentuity is moving to explicit routing, which will become the default in the next major release. ' +
|
|
724
|
-
'Run `agentuity dev --migrate-routes` to migrate.',
|
|
725
|
-
routeFiles.length
|
|
726
|
-
);
|
|
727
|
-
writeMigrationState(rootDir, 'notified');
|
|
728
|
-
}
|
|
725
|
+
// Only prompt once — if the user has already been notified or dismissed, don't ask again
|
|
726
|
+
if (alreadyNotified) {
|
|
729
727
|
return false;
|
|
730
728
|
}
|
|
731
729
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
'
|
|
737
|
-
'
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
{ centerTitle: false }
|
|
752
|
-
);
|
|
753
|
-
} else {
|
|
754
|
-
// Subsequent runs: shorter reminder
|
|
755
|
-
tui.newline();
|
|
756
|
-
tui.info(
|
|
757
|
-
`${tui.bold('Explicit routing migration available')} — run with ${tui.muted('--migrate-routes')} or choose below.`
|
|
758
|
-
);
|
|
759
|
-
}
|
|
730
|
+
tui.newline();
|
|
731
|
+
tui.banner(
|
|
732
|
+
'✨ Migrate to Explicit Routing',
|
|
733
|
+
'Agentuity is moving to explicit routing, which will become the\n' +
|
|
734
|
+
'default in the next major release. File-based route discovery\n' +
|
|
735
|
+
'will be deprecated.\n' +
|
|
736
|
+
'\n' +
|
|
737
|
+
`Your project has ${routeFiles.length} route files in src/api/ that are\n` +
|
|
738
|
+
'auto-discovered at build time. Explicit routing gives you a single\n' +
|
|
739
|
+
'src/api/index.ts that imports and mounts all sub-routers — just\n' +
|
|
740
|
+
'like a standard Hono application.\n' +
|
|
741
|
+
'\n' +
|
|
742
|
+
`${tui.muted('Before:')} ${routeFiles.length} files auto-discovered from src/api/**/*.ts\n` +
|
|
743
|
+
`${tui.muted('After:')} One src/api/index.ts that imports and mounts them\n` +
|
|
744
|
+
'\n' +
|
|
745
|
+
'Your existing route files are not modified. Your app.ts will be\n' +
|
|
746
|
+
'updated to import the router and pass it to createApp({ router }).',
|
|
747
|
+
{ centerTitle: false }
|
|
748
|
+
);
|
|
760
749
|
|
|
761
750
|
tui.newline();
|
|
762
751
|
|
package/src/utils/zip.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readFileSync, lstatSync } from 'node:fs';
|
|
1
2
|
import { relative } from 'node:path';
|
|
2
3
|
import { Glob } from 'bun';
|
|
3
4
|
import AdmZip from 'adm-zip';
|
|
@@ -11,7 +12,7 @@ interface Options {
|
|
|
11
12
|
export async function zipDir(dir: string, outdir: string, options?: Options) {
|
|
12
13
|
const zip = new AdmZip();
|
|
13
14
|
const files = await Array.fromAsync(
|
|
14
|
-
new Glob('**/*').scan({ cwd: dir, absolute: true, dot: true })
|
|
15
|
+
new Glob('**/*').scan({ cwd: dir, absolute: true, dot: true, followSymlinks: false })
|
|
15
16
|
);
|
|
16
17
|
const total = files.length;
|
|
17
18
|
let count = 0;
|
|
@@ -24,7 +25,21 @@ export async function zipDir(dir: string, outdir: string, options?: Options) {
|
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
27
|
if (!skip) {
|
|
27
|
-
|
|
28
|
+
try {
|
|
29
|
+
// Skip symlinks and directories — symlinks are workspace artefacts
|
|
30
|
+
// (e.g. bun's node_modules links) that cannot be resolved portably
|
|
31
|
+
// across machines and would cause EISDIR errors on extraction.
|
|
32
|
+
const stat = lstatSync(file);
|
|
33
|
+
if (!stat.isSymbolicLink() && !stat.isDirectory()) {
|
|
34
|
+
// Use addFile with explicit Unix permissions (0o644) instead of addLocalFile.
|
|
35
|
+
// On Windows, addLocalFile relies on OS file stats which may produce zip entries
|
|
36
|
+
// with incorrect Unix permission bits, causing EACCES errors when extracted on Linux.
|
|
37
|
+
const data = readFileSync(file);
|
|
38
|
+
zip.addFile(rel, data, '', 0o644);
|
|
39
|
+
}
|
|
40
|
+
} catch (err) {
|
|
41
|
+
throw new Error(`Failed to add file to zip: ${rel} (${file})`, { cause: err });
|
|
42
|
+
}
|
|
28
43
|
}
|
|
29
44
|
count++;
|
|
30
45
|
if (options?.progress) {
|