@agentuity/cli 2.0.12 → 2.0.13
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 +15 -8
- package/dist/cli.js.map +1 -1
- package/dist/cmd/cloud/sandbox/snapshot/create.js +4 -4
- package/dist/cmd/cloud/sandbox/snapshot/create.js.map +1 -1
- package/dist/cmd/coder/workspace/common.d.ts +29 -0
- package/dist/cmd/coder/workspace/common.d.ts.map +1 -0
- package/dist/cmd/coder/workspace/common.js +83 -0
- package/dist/cmd/coder/workspace/common.js.map +1 -0
- package/dist/cmd/coder/workspace/create.d.ts.map +1 -1
- package/dist/cmd/coder/workspace/create.js +34 -37
- package/dist/cmd/coder/workspace/create.js.map +1 -1
- package/dist/cmd/coder/workspace/get.d.ts.map +1 -1
- package/dist/cmd/coder/workspace/get.js +2 -5
- package/dist/cmd/coder/workspace/get.js.map +1 -1
- package/dist/cmd/coder/workspace/index.d.ts.map +1 -1
- package/dist/cmd/coder/workspace/index.js +10 -0
- package/dist/cmd/coder/workspace/index.js.map +1 -1
- package/dist/cmd/coder/workspace/list.d.ts.map +1 -1
- package/dist/cmd/coder/workspace/list.js +4 -0
- package/dist/cmd/coder/workspace/list.js.map +1 -1
- package/dist/cmd/coder/workspace/refresh.d.ts +2 -0
- package/dist/cmd/coder/workspace/refresh.d.ts.map +1 -0
- package/dist/cmd/coder/workspace/refresh.js +59 -0
- package/dist/cmd/coder/workspace/refresh.js.map +1 -0
- package/dist/cmd/coder/workspace/update.d.ts +2 -0
- package/dist/cmd/coder/workspace/update.d.ts.map +1 -0
- package/dist/cmd/coder/workspace/update.js +131 -0
- package/dist/cmd/coder/workspace/update.js.map +1 -0
- package/dist/cmd/coder/workspace/validate-dependencies.d.ts +2 -0
- package/dist/cmd/coder/workspace/validate-dependencies.d.ts.map +1 -0
- package/dist/cmd/coder/workspace/validate-dependencies.js +70 -0
- package/dist/cmd/coder/workspace/validate-dependencies.js.map +1 -0
- package/dist/cmd/project/random-name.d.ts +17 -0
- package/dist/cmd/project/random-name.d.ts.map +1 -0
- package/dist/cmd/project/random-name.js +144 -0
- package/dist/cmd/project/random-name.js.map +1 -0
- package/dist/cmd/project/template-flow.d.ts.map +1 -1
- package/dist/cmd/project/template-flow.js +181 -153
- package/dist/cmd/project/template-flow.js.map +1 -1
- package/dist/composite-logger.d.ts.map +1 -1
- package/dist/composite-logger.js +19 -0
- package/dist/composite-logger.js.map +1 -1
- package/dist/config.d.ts +18 -16
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +46 -16
- package/dist/config.js.map +1 -1
- package/dist/tui/prompt.d.ts +29 -0
- package/dist/tui/prompt.d.ts.map +1 -1
- package/dist/tui/prompt.js +180 -8
- package/dist/tui/prompt.js.map +1 -1
- package/package.json +7 -7
- package/src/cli.ts +30 -8
- package/src/cmd/cloud/sandbox/snapshot/create.ts +6 -6
- package/src/cmd/coder/workspace/common.ts +103 -0
- package/src/cmd/coder/workspace/create.ts +39 -43
- package/src/cmd/coder/workspace/get.ts +2 -5
- package/src/cmd/coder/workspace/index.ts +10 -0
- package/src/cmd/coder/workspace/list.ts +4 -0
- package/src/cmd/coder/workspace/refresh.ts +63 -0
- package/src/cmd/coder/workspace/update.ts +154 -0
- package/src/cmd/coder/workspace/validate-dependencies.ts +75 -0
- package/src/cmd/project/random-name.ts +152 -0
- package/src/cmd/project/template-flow.ts +199 -161
- package/src/composite-logger.ts +20 -0
- package/src/config.ts +69 -19
- package/src/tui/prompt.ts +214 -8
|
@@ -32,8 +32,13 @@ import { createPrompt, note } from '../../tui';
|
|
|
32
32
|
import type { AuthData, Config } from '../../types';
|
|
33
33
|
import { getGithubBotIdentity } from '../git/api';
|
|
34
34
|
import { downloadTemplate, initGitRepo, setupProject } from './download';
|
|
35
|
+
import { suggestBucketName, suggestDatabaseName } from './random-name';
|
|
35
36
|
import { fetchTemplates, type TemplateInfo } from './templates';
|
|
36
37
|
|
|
38
|
+
// Domain validator shared between the multi-select branch and the standalone prompt.
|
|
39
|
+
const DOMAIN_REGEX =
|
|
40
|
+
/^(?=.{1,253}$)(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[A-Za-z]{2,63}$/;
|
|
41
|
+
|
|
37
42
|
interface CreateFlowOptions {
|
|
38
43
|
projectName?: string;
|
|
39
44
|
dir?: string;
|
|
@@ -281,7 +286,7 @@ export async function runCreateFlow(options: CreateFlowOptions): Promise<CreateF
|
|
|
281
286
|
};
|
|
282
287
|
}
|
|
283
288
|
|
|
284
|
-
//
|
|
289
|
+
// Resource provisioning gates
|
|
285
290
|
const canProvision = auth && apiClient && catalystClient && orgId && region;
|
|
286
291
|
// Only count as resource flags if actually requesting provisioning (not explicit skip)
|
|
287
292
|
const hasResourceFlags =
|
|
@@ -306,13 +311,62 @@ export async function runCreateFlow(options: CreateFlowOptions): Promise<CreateF
|
|
|
306
311
|
}
|
|
307
312
|
|
|
308
313
|
if (canProvision) {
|
|
309
|
-
//
|
|
310
|
-
|
|
314
|
+
// CLI flags pre-resolve their respective per-resource decision; the multi-select
|
|
315
|
+
// is only used for resources where the user didn't pass a flag.
|
|
316
|
+
const dbFlagAction = resolveFlagAction(databaseOption, 'database');
|
|
317
|
+
const storageFlagAction = resolveFlagAction(storageOption, 'storage');
|
|
318
|
+
const domainFlagProvided = (domains?.length ?? 0) > 0;
|
|
319
|
+
|
|
320
|
+
// Determine which resources should run through the configuration phase.
|
|
321
|
+
// In interactive mode, ask the user via a single multi-select.
|
|
322
|
+
// In headless / non-interactive mode, only flagged resources are considered.
|
|
323
|
+
let wantDb = dbFlagAction !== undefined && dbFlagAction !== 'Skip';
|
|
324
|
+
let wantStorage = storageFlagAction !== undefined && storageFlagAction !== 'Skip';
|
|
325
|
+
let wantDomain = domainFlagProvided;
|
|
326
|
+
|
|
327
|
+
if (isInteractive) {
|
|
328
|
+
// Build multi-select options dynamically: only show resources the user hasn't
|
|
329
|
+
// already decided about via CLI flags. If all three came from flags, skip the prompt.
|
|
330
|
+
const msOptions: {
|
|
331
|
+
value: 'database' | 'storage' | 'domain';
|
|
332
|
+
label: string;
|
|
333
|
+
hint?: string;
|
|
334
|
+
}[] = [];
|
|
335
|
+
if (dbFlagAction === undefined) {
|
|
336
|
+
msOptions.push({ value: 'database', label: 'SQL Database', hint: 'PostgreSQL' });
|
|
337
|
+
}
|
|
338
|
+
if (storageFlagAction === undefined) {
|
|
339
|
+
msOptions.push({ value: 'storage', label: 'Storage Bucket', hint: 'S3-compatible' });
|
|
340
|
+
}
|
|
341
|
+
if (!domainFlagProvided) {
|
|
342
|
+
msOptions.push({ value: 'domain', label: 'Custom Domain', hint: 'BYO domain' });
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (msOptions.length > 0) {
|
|
346
|
+
const picked = await prompt.multiselect<'database' | 'storage' | 'domain'>({
|
|
347
|
+
message: 'What would you like to set up? (all optional)',
|
|
348
|
+
options: msOptions,
|
|
349
|
+
initial: [],
|
|
350
|
+
});
|
|
351
|
+
if (dbFlagAction === undefined) wantDb = picked.includes('database');
|
|
352
|
+
if (storageFlagAction === undefined) wantStorage = picked.includes('storage');
|
|
353
|
+
if (!domainFlagProvided) wantDomain = picked.includes('domain');
|
|
354
|
+
}
|
|
355
|
+
}
|
|
311
356
|
|
|
357
|
+
// Fetch existing resources only if we'll actually need them.
|
|
358
|
+
// Need them when:
|
|
359
|
+
// - user wants db/storage in interactive mode (to offer "use existing")
|
|
360
|
+
// - a CLI flag pointed at an existing resource by name
|
|
361
|
+
let resources: Awaited<ReturnType<typeof listResources>> | undefined;
|
|
312
362
|
const needResources =
|
|
313
|
-
isInteractive ||
|
|
314
|
-
(databaseOption
|
|
315
|
-
|
|
363
|
+
(isInteractive && (wantDb || wantStorage)) ||
|
|
364
|
+
(databaseOption !== undefined &&
|
|
365
|
+
dbFlagAction !== 'Create New' &&
|
|
366
|
+
dbFlagAction !== 'Skip') ||
|
|
367
|
+
(storageOption !== undefined &&
|
|
368
|
+
storageFlagAction !== 'Create New' &&
|
|
369
|
+
storageFlagAction !== 'Skip');
|
|
316
370
|
|
|
317
371
|
if (needResources) {
|
|
318
372
|
resources = await tui.spinner({
|
|
@@ -330,212 +384,174 @@ export async function runCreateFlow(options: CreateFlowOptions): Promise<CreateF
|
|
|
330
384
|
logger.debug(
|
|
331
385
|
`Storage buckets: ${resources.s3.map((b) => b.bucket_name).join(', ') || '(none)'}`
|
|
332
386
|
);
|
|
333
|
-
}
|
|
334
387
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
if (!existingDb) {
|
|
347
|
-
logger.fatal(
|
|
348
|
-
`Database '${databaseOption}' not found. Use 'new' to create a new database or 'skip' to skip.`,
|
|
349
|
-
ErrorCode.RESOURCE_NOT_FOUND
|
|
350
|
-
);
|
|
351
|
-
}
|
|
352
|
-
db_action = databaseOption;
|
|
388
|
+
// Validate flag-supplied resource names against the fetched list.
|
|
389
|
+
if (
|
|
390
|
+
databaseOption !== undefined &&
|
|
391
|
+
dbFlagAction !== 'Create New' &&
|
|
392
|
+
dbFlagAction !== 'Skip' &&
|
|
393
|
+
!resources.db.find((d) => d.name === dbFlagAction)
|
|
394
|
+
) {
|
|
395
|
+
logger.fatal(
|
|
396
|
+
`Database '${databaseOption}' not found. Use 'new' to create a new database or 'skip' to skip.`,
|
|
397
|
+
ErrorCode.RESOURCE_NOT_FOUND
|
|
398
|
+
);
|
|
353
399
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
],
|
|
365
|
-
});
|
|
366
|
-
} else {
|
|
367
|
-
// Headless without flag - skip
|
|
368
|
-
db_action = 'Skip';
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// Determine storage action: CLI flag > interactive prompt > skip (headless)
|
|
372
|
-
let s3_action: string;
|
|
373
|
-
if (storageOption !== undefined) {
|
|
374
|
-
// CLI flag provided - normalize to expected values
|
|
375
|
-
if (storageOption.toLowerCase() === 'new') {
|
|
376
|
-
s3_action = 'Create New';
|
|
377
|
-
} else if (storageOption.toLowerCase() === 'skip') {
|
|
378
|
-
s3_action = 'Skip';
|
|
379
|
-
} else {
|
|
380
|
-
// Existing bucket name - validate it exists
|
|
381
|
-
const existingBucket = resources?.s3.find((b) => b.bucket_name === storageOption);
|
|
382
|
-
if (!existingBucket) {
|
|
383
|
-
logger.fatal(
|
|
384
|
-
`Storage bucket '${storageOption}' not found. Use 'new' to create a new bucket or 'skip' to skip.`,
|
|
385
|
-
ErrorCode.RESOURCE_NOT_FOUND
|
|
386
|
-
);
|
|
387
|
-
}
|
|
388
|
-
s3_action = storageOption;
|
|
400
|
+
if (
|
|
401
|
+
storageOption !== undefined &&
|
|
402
|
+
storageFlagAction !== 'Create New' &&
|
|
403
|
+
storageFlagAction !== 'Skip' &&
|
|
404
|
+
!resources.s3.find((b) => b.bucket_name === storageFlagAction)
|
|
405
|
+
) {
|
|
406
|
+
logger.fatal(
|
|
407
|
+
`Storage bucket '${storageOption}' not found. Use 'new' to create a new bucket or 'skip' to skip.`,
|
|
408
|
+
ErrorCode.RESOURCE_NOT_FOUND
|
|
409
|
+
);
|
|
389
410
|
}
|
|
390
|
-
} else if (isInteractive) {
|
|
391
|
-
s3_action = await prompt.select({
|
|
392
|
-
message: 'Create Storage Bucket?',
|
|
393
|
-
options: [
|
|
394
|
-
{ value: 'Skip', label: 'Skip or Setup later' },
|
|
395
|
-
{ value: 'Create New', label: 'Create a new bucket' },
|
|
396
|
-
...resources!.s3.map((bucket) => ({
|
|
397
|
-
value: bucket.bucket_name,
|
|
398
|
-
label: `Use bucket: ${tui.tuiColors.primary(bucket.bucket_name)}`,
|
|
399
|
-
})),
|
|
400
|
-
],
|
|
401
|
-
});
|
|
402
|
-
} else {
|
|
403
|
-
// Headless without flag - skip
|
|
404
|
-
s3_action = 'Skip';
|
|
405
411
|
}
|
|
406
412
|
|
|
407
|
-
//
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
413
|
+
// === Configure each selected resource: Database → Storage → Domain ===
|
|
414
|
+
|
|
415
|
+
// Database
|
|
416
|
+
if (wantDb) {
|
|
417
|
+
let dbAction = dbFlagAction;
|
|
418
|
+
if (dbAction === undefined && isInteractive) {
|
|
419
|
+
const existing = resources?.db ?? [];
|
|
420
|
+
if (existing.length > 0) {
|
|
421
|
+
dbAction = await prompt.select<string>({
|
|
422
|
+
message: 'SQL Database',
|
|
423
|
+
options: [
|
|
424
|
+
{ value: 'Create New', label: 'Create a new database' },
|
|
425
|
+
...existing.map((db) => ({
|
|
426
|
+
value: db.name,
|
|
427
|
+
label: `Use database: ${tui.tuiColors.primary(db.name)}`,
|
|
428
|
+
})),
|
|
429
|
+
],
|
|
430
|
+
});
|
|
431
|
+
} else {
|
|
432
|
+
// No existing databases — user already opted in via the multi-select, so create new.
|
|
433
|
+
dbAction = 'Create New';
|
|
434
|
+
}
|
|
421
435
|
}
|
|
422
|
-
}
|
|
423
436
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
let bucketName: string | undefined;
|
|
428
|
-
let bucketDescription: string | undefined;
|
|
437
|
+
if (dbAction === 'Create New') {
|
|
438
|
+
let dbName: string | undefined;
|
|
439
|
+
let dbDescription: string | undefined;
|
|
429
440
|
|
|
430
|
-
// Only prompt for name/description in interactive mode
|
|
431
441
|
if (isInteractive) {
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
442
|
+
const suggestion = suggestDatabaseName(projectName);
|
|
443
|
+
const dbNameInput = await prompt.text({
|
|
444
|
+
message: 'Database name',
|
|
445
|
+
hint: 'Optional · lowercase letters, digits, underscores',
|
|
446
|
+
placeholder: suggestion,
|
|
435
447
|
validate: (value: string) => {
|
|
436
448
|
const trimmed = value.trim();
|
|
437
449
|
if (trimmed === '') return true;
|
|
438
|
-
const result =
|
|
450
|
+
const result = validateDatabaseName(trimmed);
|
|
439
451
|
return result.valid ? true : result.error!;
|
|
440
452
|
},
|
|
441
453
|
});
|
|
442
|
-
|
|
443
|
-
|
|
454
|
+
dbName = dbNameInput.trim() || undefined;
|
|
455
|
+
dbDescription =
|
|
444
456
|
(await prompt.text({
|
|
445
|
-
message: '
|
|
446
|
-
hint: 'Optional
|
|
457
|
+
message: 'Database description',
|
|
458
|
+
hint: 'Optional · press Enter to skip',
|
|
447
459
|
})) || undefined;
|
|
448
460
|
}
|
|
449
461
|
|
|
450
462
|
const created = await tui.spinner({
|
|
451
|
-
message: 'Provisioning New
|
|
463
|
+
message: 'Provisioning New SQL Database',
|
|
452
464
|
clearOnSuccess: true,
|
|
453
465
|
callback: async () => {
|
|
454
466
|
return createResources(catalystClient!, orgId!, region!, [
|
|
455
|
-
{
|
|
456
|
-
type: 's3',
|
|
457
|
-
name: bucketName,
|
|
458
|
-
description: bucketDescription,
|
|
459
|
-
},
|
|
467
|
+
{ type: 'db', name: dbName, description: dbDescription },
|
|
460
468
|
]);
|
|
461
469
|
},
|
|
462
470
|
});
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
}
|
|
469
|
-
case 'Skip': {
|
|
470
|
-
break;
|
|
471
|
+
if (created[0]?.env) Object.assign(resourceEnvVars, created[0].env);
|
|
472
|
+
} else if (dbAction && dbAction !== 'Skip') {
|
|
473
|
+
// Existing database selected — reuse its env vars.
|
|
474
|
+
const selectedDb = resources?.db.find((d) => d.name === dbAction);
|
|
475
|
+
if (selectedDb?.env) Object.assign(resourceEnvVars, selectedDb.env);
|
|
471
476
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Storage
|
|
480
|
+
if (wantStorage) {
|
|
481
|
+
let s3Action = storageFlagAction;
|
|
482
|
+
if (s3Action === undefined && isInteractive) {
|
|
483
|
+
const existing = resources?.s3 ?? [];
|
|
484
|
+
if (existing.length > 0) {
|
|
485
|
+
s3Action = await prompt.select<string>({
|
|
486
|
+
message: 'Storage Bucket',
|
|
487
|
+
options: [
|
|
488
|
+
{ value: 'Create New', label: 'Create a new bucket' },
|
|
489
|
+
...existing.map((bucket) => ({
|
|
490
|
+
value: bucket.bucket_name,
|
|
491
|
+
label: `Use bucket: ${tui.tuiColors.primary(bucket.bucket_name)}`,
|
|
492
|
+
})),
|
|
493
|
+
],
|
|
494
|
+
});
|
|
495
|
+
} else {
|
|
496
|
+
s3Action = 'Create New';
|
|
477
497
|
}
|
|
478
|
-
break;
|
|
479
498
|
}
|
|
480
|
-
}
|
|
481
499
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
let dbName: string | undefined;
|
|
486
|
-
let dbDescription: string | undefined;
|
|
500
|
+
if (s3Action === 'Create New') {
|
|
501
|
+
let bucketName: string | undefined;
|
|
502
|
+
let bucketDescription: string | undefined;
|
|
487
503
|
|
|
488
|
-
// Only prompt for name/description in interactive mode
|
|
489
504
|
if (isInteractive) {
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
505
|
+
const suggestion = suggestBucketName(projectName);
|
|
506
|
+
const bucketNameInput = await prompt.text({
|
|
507
|
+
message: 'Bucket name',
|
|
508
|
+
hint: 'Optional · lowercase letters, digits, hyphens',
|
|
509
|
+
placeholder: suggestion,
|
|
493
510
|
validate: (value: string) => {
|
|
494
511
|
const trimmed = value.trim();
|
|
495
512
|
if (trimmed === '') return true;
|
|
496
|
-
const result =
|
|
513
|
+
const result = validateBucketName(trimmed);
|
|
497
514
|
return result.valid ? true : result.error!;
|
|
498
515
|
},
|
|
499
516
|
});
|
|
500
|
-
|
|
501
|
-
|
|
517
|
+
bucketName = bucketNameInput.trim() || undefined;
|
|
518
|
+
bucketDescription =
|
|
502
519
|
(await prompt.text({
|
|
503
|
-
message: '
|
|
504
|
-
hint: 'Optional
|
|
520
|
+
message: 'Bucket description',
|
|
521
|
+
hint: 'Optional · press Enter to skip',
|
|
505
522
|
})) || undefined;
|
|
506
523
|
}
|
|
507
524
|
|
|
508
525
|
const created = await tui.spinner({
|
|
509
|
-
message: 'Provisioning New
|
|
526
|
+
message: 'Provisioning New Bucket',
|
|
510
527
|
clearOnSuccess: true,
|
|
511
528
|
callback: async () => {
|
|
512
529
|
return createResources(catalystClient!, orgId!, region!, [
|
|
513
|
-
{
|
|
514
|
-
type: 'db',
|
|
515
|
-
name: dbName,
|
|
516
|
-
description: dbDescription,
|
|
517
|
-
},
|
|
530
|
+
{ type: 's3', name: bucketName, description: bucketDescription },
|
|
518
531
|
]);
|
|
519
532
|
},
|
|
520
533
|
});
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
break;
|
|
526
|
-
}
|
|
527
|
-
case 'Skip': {
|
|
528
|
-
break;
|
|
529
|
-
}
|
|
530
|
-
default: {
|
|
531
|
-
// User selected an existing database - get env vars from the resources list
|
|
532
|
-
const selectedDb = resources?.db.find((d) => d.name === db_action);
|
|
533
|
-
if (selectedDb?.env) {
|
|
534
|
-
Object.assign(resourceEnvVars, selectedDb.env);
|
|
535
|
-
}
|
|
536
|
-
break;
|
|
534
|
+
if (created[0]?.env) Object.assign(resourceEnvVars, created[0].env);
|
|
535
|
+
} else if (s3Action && s3Action !== 'Skip') {
|
|
536
|
+
const selectedBucket = resources?.s3.find((b) => b.bucket_name === s3Action);
|
|
537
|
+
if (selectedBucket?.env) Object.assign(resourceEnvVars, selectedBucket.env);
|
|
537
538
|
}
|
|
538
539
|
}
|
|
540
|
+
|
|
541
|
+
// Custom Domain
|
|
542
|
+
if (wantDomain && !domainFlagProvided && isInteractive) {
|
|
543
|
+
const customDns = await prompt.text({
|
|
544
|
+
message: 'Custom domain',
|
|
545
|
+
hint: 'e.g. agents.example.com',
|
|
546
|
+
validate: (val: string) =>
|
|
547
|
+
val === ''
|
|
548
|
+
? 'Domain is required (or go back and uncheck Custom Domain)'
|
|
549
|
+
: DOMAIN_REGEX.test(val)
|
|
550
|
+
? true
|
|
551
|
+
: 'Invalid domain',
|
|
552
|
+
});
|
|
553
|
+
if (customDns) _domains = [customDns];
|
|
554
|
+
}
|
|
539
555
|
}
|
|
540
556
|
|
|
541
557
|
let projectId: string | undefined;
|
|
@@ -691,6 +707,28 @@ export async function runCreateFlow(options: CreateFlowOptions): Promise<CreateF
|
|
|
691
707
|
};
|
|
692
708
|
}
|
|
693
709
|
|
|
710
|
+
/**
|
|
711
|
+
* Normalize a CLI flag value (`--database` / `--storage`) into the same
|
|
712
|
+
* action vocabulary the interactive flow uses:
|
|
713
|
+
* - 'new' -> 'Create New'
|
|
714
|
+
* - 'skip' -> 'Skip'
|
|
715
|
+
* - any other string -> treated as an existing-resource name (returned as-is)
|
|
716
|
+
* - undefined -> undefined (no flag passed; multi-select decides)
|
|
717
|
+
*
|
|
718
|
+
* The existence check for named resources happens later, after the resource
|
|
719
|
+
* list is fetched.
|
|
720
|
+
*/
|
|
721
|
+
function resolveFlagAction(
|
|
722
|
+
flag: string | undefined,
|
|
723
|
+
_kind: 'database' | 'storage'
|
|
724
|
+
): string | undefined {
|
|
725
|
+
if (flag === undefined) return undefined;
|
|
726
|
+
const lower = flag.toLowerCase();
|
|
727
|
+
if (lower === 'new') return 'Create New';
|
|
728
|
+
if (lower === 'skip') return 'Skip';
|
|
729
|
+
return flag;
|
|
730
|
+
}
|
|
731
|
+
|
|
694
732
|
/**
|
|
695
733
|
* Sanitize a project name to create a safe directory/package name
|
|
696
734
|
* - Converts to lowercase
|
package/src/composite-logger.ts
CHANGED
|
@@ -7,6 +7,12 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { Logger } from '@agentuity/core';
|
|
10
|
+
import { format } from 'node:util';
|
|
11
|
+
import { ErrorCode, getExitCode } from './errors';
|
|
12
|
+
|
|
13
|
+
function isErrorCode(value: unknown): value is ErrorCode {
|
|
14
|
+
return typeof value === 'string' && Object.values(ErrorCode).includes(value as ErrorCode);
|
|
15
|
+
}
|
|
10
16
|
|
|
11
17
|
/**
|
|
12
18
|
* A logger that delegates to multiple child loggers
|
|
@@ -45,6 +51,20 @@ export class CompositeLogger implements Logger {
|
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
fatal(message: unknown, ...args: unknown[]): never {
|
|
54
|
+
const maybeErrorCode = args[args.length - 1];
|
|
55
|
+
if (isErrorCode(maybeErrorCode)) {
|
|
56
|
+
const formatArgs = args.slice(0, -1);
|
|
57
|
+
const formattedMessage = format(message, ...formatArgs);
|
|
58
|
+
for (const logger of this.loggers) {
|
|
59
|
+
try {
|
|
60
|
+
logger.error(formattedMessage);
|
|
61
|
+
} catch {
|
|
62
|
+
// Keep fatal exits reliable even if one delegate cannot write.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
process.exit(getExitCode(maybeErrorCode));
|
|
66
|
+
}
|
|
67
|
+
|
|
48
68
|
// Call fatal on all loggers, but only the first one will exit
|
|
49
69
|
for (const logger of this.loggers) {
|
|
50
70
|
try {
|
package/src/config.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
2
|
-
import { chmod, mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { chmod, mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { basename, extname, join, normalize, resolve } from 'node:path';
|
|
4
|
+
import { basename, dirname, extname, join, normalize, resolve } from 'node:path';
|
|
5
5
|
import { type Logger, StructuredError } from '@agentuity/core';
|
|
6
6
|
import {
|
|
7
7
|
type BuildMetadata,
|
|
@@ -15,6 +15,7 @@ import { z } from 'zod';
|
|
|
15
15
|
import { clearProfileCache } from './cache';
|
|
16
16
|
import { getCatalystUrl } from './catalyst';
|
|
17
17
|
import { readEnvFile, writeEnvFile } from './env-util';
|
|
18
|
+
import { ErrorCode } from './errors';
|
|
18
19
|
import {
|
|
19
20
|
deleteAuthFromKeychain,
|
|
20
21
|
getAuthFromKeychain,
|
|
@@ -151,10 +152,12 @@ function expandTilde(path: string): string {
|
|
|
151
152
|
let cachedConfig: Config | null | undefined;
|
|
152
153
|
// Track the resolved config path so saveConfig writes back to the same file
|
|
153
154
|
let cachedConfigPath: string | undefined;
|
|
155
|
+
const loadedProjectConfigPaths = new Map<string, string>();
|
|
154
156
|
|
|
155
157
|
export function resetConfigCache(): void {
|
|
156
158
|
cachedConfig = undefined;
|
|
157
159
|
cachedConfigPath = undefined;
|
|
160
|
+
loadedProjectConfigPaths.clear();
|
|
158
161
|
}
|
|
159
162
|
|
|
160
163
|
export async function loadConfig(
|
|
@@ -600,31 +603,76 @@ export function generateYAMLTemplate(name: string): string {
|
|
|
600
603
|
return lines.join('\n');
|
|
601
604
|
}
|
|
602
605
|
|
|
603
|
-
export const ProjectConfigNotFoundException = StructuredError('ProjectConfigNotFoundException')
|
|
606
|
+
export const ProjectConfigNotFoundException = StructuredError('ProjectConfigNotFoundException')<{
|
|
607
|
+
code?: ErrorCode;
|
|
608
|
+
configPath?: string;
|
|
609
|
+
explicit?: boolean;
|
|
610
|
+
}>();
|
|
604
611
|
|
|
605
612
|
type ProjectConfig = z.infer<typeof ProjectSchema>;
|
|
606
613
|
|
|
607
|
-
export
|
|
608
|
-
|
|
614
|
+
export type ResolvedProjectConfigPaths = {
|
|
615
|
+
projectDir: string;
|
|
616
|
+
configPath: string;
|
|
617
|
+
explicitConfigFile: boolean;
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
export async function resolveProjectConfigPaths(
|
|
621
|
+
path: string,
|
|
609
622
|
config?: Config | null
|
|
610
|
-
): Promise<
|
|
611
|
-
|
|
623
|
+
): Promise<ResolvedProjectConfigPaths> {
|
|
624
|
+
const resolvedPath = resolve(expandTilde(path));
|
|
625
|
+
const pathStats = await stat(resolvedPath).catch(() => null);
|
|
626
|
+
const isExplicitJsonFile =
|
|
627
|
+
(pathStats?.isFile() || pathStats === null) && extname(resolvedPath) === '.json';
|
|
628
|
+
|
|
629
|
+
if (isExplicitJsonFile) {
|
|
630
|
+
return {
|
|
631
|
+
projectDir: dirname(resolvedPath),
|
|
632
|
+
configPath: resolvedPath,
|
|
633
|
+
explicitConfigFile: true,
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const projectDir = resolvedPath;
|
|
638
|
+
let configPath = join(projectDir, 'agentuity.json');
|
|
612
639
|
|
|
613
|
-
// Check for profile-specific override if config is provided
|
|
614
640
|
if (config?.name) {
|
|
615
|
-
const profileConfigPath = join(
|
|
641
|
+
const profileConfigPath = join(projectDir, `agentuity.${config.name}.json`);
|
|
616
642
|
if (await Bun.file(profileConfigPath).exists()) {
|
|
617
643
|
configPath = profileConfigPath;
|
|
618
644
|
}
|
|
619
645
|
}
|
|
620
646
|
|
|
647
|
+
return {
|
|
648
|
+
projectDir,
|
|
649
|
+
configPath,
|
|
650
|
+
explicitConfigFile: false,
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
export async function loadProjectConfig(
|
|
655
|
+
dir: string,
|
|
656
|
+
config?: Config | null
|
|
657
|
+
): Promise<ProjectConfig> {
|
|
658
|
+
const { projectDir, configPath, explicitConfigFile } = await resolveProjectConfigPaths(
|
|
659
|
+
dir,
|
|
660
|
+
config
|
|
661
|
+
);
|
|
662
|
+
|
|
621
663
|
const file = Bun.file(configPath);
|
|
622
664
|
if (!(await file.exists())) {
|
|
623
665
|
// TODO: check to see if a valid project that was created unauthenticated
|
|
624
666
|
// and then if so:
|
|
625
667
|
// 1. if authentication, offer to import the project
|
|
626
668
|
// 2. tell them that they need to login to use the command and import the project
|
|
627
|
-
throw new ProjectConfigNotFoundException({
|
|
669
|
+
throw new ProjectConfigNotFoundException({
|
|
670
|
+
message: explicitConfigFile
|
|
671
|
+
? `Project config not found at ${configPath}`
|
|
672
|
+
: 'project config not found',
|
|
673
|
+
configPath,
|
|
674
|
+
explicit: explicitConfigFile,
|
|
675
|
+
});
|
|
628
676
|
}
|
|
629
677
|
const text = await file.text();
|
|
630
678
|
const parsedConfig = parseJSONC(text);
|
|
@@ -637,6 +685,7 @@ export async function loadProjectConfig(
|
|
|
637
685
|
}
|
|
638
686
|
process.exit(1);
|
|
639
687
|
}
|
|
688
|
+
loadedProjectConfigPaths.set(projectDir, configPath);
|
|
640
689
|
return result.data;
|
|
641
690
|
}
|
|
642
691
|
|
|
@@ -717,18 +766,19 @@ export async function updateProjectConfig(
|
|
|
717
766
|
updates: Partial<z.infer<typeof ProjectSchema>>,
|
|
718
767
|
config?: Config | null
|
|
719
768
|
): Promise<void> {
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
if (await Bun.file(profileConfigPath).exists()) {
|
|
725
|
-
configPath = profileConfigPath;
|
|
726
|
-
}
|
|
727
|
-
}
|
|
769
|
+
const resolved = await resolveProjectConfigPaths(dir, config);
|
|
770
|
+
const configPath = resolved.explicitConfigFile
|
|
771
|
+
? resolved.configPath
|
|
772
|
+
: (loadedProjectConfigPaths.get(resolved.projectDir) ?? resolved.configPath);
|
|
728
773
|
|
|
729
774
|
const file = Bun.file(configPath);
|
|
730
775
|
if (!(await file.exists())) {
|
|
731
|
-
throw new
|
|
776
|
+
throw new ProjectConfigNotFoundException({
|
|
777
|
+
code: ErrorCode.PROJECT_NOT_FOUND,
|
|
778
|
+
message: `Project config not found at ${configPath}`,
|
|
779
|
+
configPath: resolved.configPath,
|
|
780
|
+
explicit: resolved.explicitConfigFile,
|
|
781
|
+
});
|
|
732
782
|
}
|
|
733
783
|
|
|
734
784
|
const text = await file.text();
|