@agentuity/cli 2.0.12 → 2.0.14
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/agent-detection.d.ts.map +1 -1
- package/dist/agent-detection.js +1 -0
- package/dist/agent-detection.js.map +1 -1
- 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/create.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/create.js +46 -4
- package/dist/cmd/cloud/sandbox/create.js.map +1 -1
- package/dist/cmd/cloud/sandbox/exec.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/exec.js +4 -3
- package/dist/cmd/cloud/sandbox/exec.js.map +1 -1
- package/dist/cmd/cloud/sandbox/run.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/run.js +9 -5
- package/dist/cmd/cloud/sandbox/run.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/start.d.ts.map +1 -1
- package/dist/cmd/coder/start.js +1 -0
- package/dist/cmd/coder/start.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/agent-detection.ts +1 -0
- package/src/cli.ts +30 -8
- package/src/cmd/cloud/sandbox/create.ts +57 -4
- package/src/cmd/cloud/sandbox/exec.ts +4 -3
- package/src/cmd/cloud/sandbox/run.ts +9 -5
- package/src/cmd/cloud/sandbox/snapshot/create.ts +6 -6
- package/src/cmd/coder/start.ts +1 -0
- 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
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates project-derived suggestions for resource names (DB / S3 bucket).
|
|
3
|
+
*
|
|
4
|
+
* The CLI shows these as a dim "press Enter to use ..." default in the create flow.
|
|
5
|
+
* If the user presses Enter, the suggestion is sent to the server. Otherwise the
|
|
6
|
+
* server is responsible for assigning a name when none is provided.
|
|
7
|
+
*
|
|
8
|
+
* Both suggestions are validated against the same rules the server enforces
|
|
9
|
+
* (`validateBucketName` / `validateDatabaseName` from `@agentuity/server`) so the
|
|
10
|
+
* happy path can never produce an invalid suggestion.
|
|
11
|
+
*/
|
|
12
|
+
import { validateBucketName, validateDatabaseName } from '@agentuity/server';
|
|
13
|
+
|
|
14
|
+
const BUCKET_MAX = 63;
|
|
15
|
+
const BUCKET_MIN = 3;
|
|
16
|
+
const DB_MAX = 63;
|
|
17
|
+
|
|
18
|
+
/** 3 lowercase alphanumeric chars, e.g. "k7p". */
|
|
19
|
+
function shortSuffix(): string {
|
|
20
|
+
// toString(36) yields [0-9a-z]; slice 3 chars after the "0." prefix.
|
|
21
|
+
const s = Math.random().toString(36).slice(2, 5);
|
|
22
|
+
// Pad in the (extremely unlikely) case the slice is shorter than 3 chars.
|
|
23
|
+
return s.length === 3 ? s : (s + '000').slice(0, 3);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Sanitize a project name into the bucket-name alphabet:
|
|
28
|
+
* - lowercase
|
|
29
|
+
* - spaces / underscores / dots → hyphens
|
|
30
|
+
* - drop anything else
|
|
31
|
+
* - collapse and trim hyphens
|
|
32
|
+
* - strip reserved prefixes (`agentuity*`, `ag-*`, `ago-*`, `xn--`)
|
|
33
|
+
*/
|
|
34
|
+
function sanitizeForBucket(name: string): string {
|
|
35
|
+
let out = name
|
|
36
|
+
.toLowerCase()
|
|
37
|
+
.trim()
|
|
38
|
+
.replace(/[\s_.]+/g, '-')
|
|
39
|
+
.replace(/[^a-z0-9-]/g, '')
|
|
40
|
+
.replace(/-+/g, '-')
|
|
41
|
+
.replace(/^-+|-+$/g, '');
|
|
42
|
+
|
|
43
|
+
// Strip reserved prefixes (rules from validateBucketName).
|
|
44
|
+
while (
|
|
45
|
+
out.startsWith('agentuity') ||
|
|
46
|
+
out.startsWith('ag-') ||
|
|
47
|
+
out.startsWith('ago-') ||
|
|
48
|
+
out.startsWith('xn--')
|
|
49
|
+
) {
|
|
50
|
+
if (out.startsWith('agentuity')) out = out.slice('agentuity'.length);
|
|
51
|
+
else if (out.startsWith('ago-')) out = out.slice('ago-'.length);
|
|
52
|
+
else if (out.startsWith('ag-')) out = out.slice('ag-'.length);
|
|
53
|
+
else if (out.startsWith('xn--')) out = out.slice('xn--'.length);
|
|
54
|
+
out = out.replace(/^-+/, '');
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Sanitize a project name into the database-name alphabet:
|
|
61
|
+
* - lowercase
|
|
62
|
+
* - non `[a-z0-9_]` → `_`
|
|
63
|
+
* - collapse and trim underscores
|
|
64
|
+
* - ensure it starts with a letter or underscore (prepend `p_` otherwise)
|
|
65
|
+
* - strip reserved `pg_` prefix
|
|
66
|
+
*/
|
|
67
|
+
function sanitizeForDatabase(name: string): string {
|
|
68
|
+
let out = name
|
|
69
|
+
.toLowerCase()
|
|
70
|
+
.trim()
|
|
71
|
+
.replace(/[^a-z0-9_]+/g, '_')
|
|
72
|
+
.replace(/_+/g, '_')
|
|
73
|
+
.replace(/^_+|_+$/g, '');
|
|
74
|
+
|
|
75
|
+
if (!/^[a-z_]/.test(out)) {
|
|
76
|
+
out = out.length > 0 ? `p_${out}` : '';
|
|
77
|
+
}
|
|
78
|
+
while (out.startsWith('pg_')) {
|
|
79
|
+
out = out.slice(3).replace(/^_+/, '');
|
|
80
|
+
if (!/^[a-z_]/.test(out) && out.length > 0) out = `p_${out}`;
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Truncate so the final string (`<base>-<suffixWithDash>`) fits within `max` chars
|
|
87
|
+
* while keeping the suffix intact and not ending on a hyphen.
|
|
88
|
+
*/
|
|
89
|
+
function truncateBaseHyphen(base: string, suffixWithDash: string, max: number): string {
|
|
90
|
+
const room = max - suffixWithDash.length;
|
|
91
|
+
if (room <= 0) return '';
|
|
92
|
+
let out = base.slice(0, room);
|
|
93
|
+
out = out.replace(/-+$/, '');
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Same as `truncateBaseHyphen` but for underscore-joined names (database). */
|
|
98
|
+
function truncateBaseUnderscore(base: string, suffixWithUnderscore: string, max: number): string {
|
|
99
|
+
const room = max - suffixWithUnderscore.length;
|
|
100
|
+
if (room <= 0) return '';
|
|
101
|
+
let out = base.slice(0, room);
|
|
102
|
+
out = out.replace(/_+$/, '');
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Generate a suggested S3 bucket name derived from the project name.
|
|
108
|
+
* Format: `<sanitized-project>-storage-<3char>` (≤ 63 chars).
|
|
109
|
+
*
|
|
110
|
+
* Falls back to `bucket-<3char><3char>` if the project name produces nothing usable.
|
|
111
|
+
* Always returns a value that passes `validateBucketName`.
|
|
112
|
+
*/
|
|
113
|
+
export function suggestBucketName(projectName: string): string {
|
|
114
|
+
const sanitized = sanitizeForBucket(projectName);
|
|
115
|
+
|
|
116
|
+
// Try a few times in case sanitization + suffix happens to land on something invalid.
|
|
117
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
118
|
+
const suffix = `-storage-${shortSuffix()}`;
|
|
119
|
+
const base = truncateBaseHyphen(sanitized, suffix, BUCKET_MAX);
|
|
120
|
+
const candidate = base.length > 0 ? `${base}${suffix}` : `bucket${suffix}`;
|
|
121
|
+
if (candidate.length >= BUCKET_MIN && validateBucketName(candidate).valid) {
|
|
122
|
+
return candidate;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Pure fallback: short, always-valid generic name.
|
|
127
|
+
const fallback = `bucket-${shortSuffix()}${shortSuffix()}`;
|
|
128
|
+
return validateBucketName(fallback).valid ? fallback : `bucket-${shortSuffix()}aaa`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Generate a suggested PostgreSQL database name derived from the project name.
|
|
133
|
+
* Format: `<sanitized-project>_db_<3char>` (≤ 63 chars).
|
|
134
|
+
*
|
|
135
|
+
* Falls back to `db_<3char><3char>` if the project name produces nothing usable.
|
|
136
|
+
* Always returns a value that passes `validateDatabaseName`.
|
|
137
|
+
*/
|
|
138
|
+
export function suggestDatabaseName(projectName: string): string {
|
|
139
|
+
const sanitized = sanitizeForDatabase(projectName);
|
|
140
|
+
|
|
141
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
142
|
+
const suffix = `_db_${shortSuffix()}`;
|
|
143
|
+
const base = truncateBaseUnderscore(sanitized, suffix, DB_MAX);
|
|
144
|
+
const candidate = base.length > 0 ? `${base}${suffix}` : `db${suffix}`;
|
|
145
|
+
if (validateDatabaseName(candidate).valid) {
|
|
146
|
+
return candidate;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const fallback = `db_${shortSuffix()}${shortSuffix()}`;
|
|
151
|
+
return validateDatabaseName(fallback).valid ? fallback : `db_${shortSuffix()}aaa`;
|
|
152
|
+
}
|
|
@@ -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 {
|