@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.
Files changed (87) hide show
  1. package/dist/agent-detection.d.ts.map +1 -1
  2. package/dist/agent-detection.js +1 -0
  3. package/dist/agent-detection.js.map +1 -1
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +15 -8
  6. package/dist/cli.js.map +1 -1
  7. package/dist/cmd/cloud/sandbox/create.d.ts.map +1 -1
  8. package/dist/cmd/cloud/sandbox/create.js +46 -4
  9. package/dist/cmd/cloud/sandbox/create.js.map +1 -1
  10. package/dist/cmd/cloud/sandbox/exec.d.ts.map +1 -1
  11. package/dist/cmd/cloud/sandbox/exec.js +4 -3
  12. package/dist/cmd/cloud/sandbox/exec.js.map +1 -1
  13. package/dist/cmd/cloud/sandbox/run.d.ts.map +1 -1
  14. package/dist/cmd/cloud/sandbox/run.js +9 -5
  15. package/dist/cmd/cloud/sandbox/run.js.map +1 -1
  16. package/dist/cmd/cloud/sandbox/snapshot/create.js +4 -4
  17. package/dist/cmd/cloud/sandbox/snapshot/create.js.map +1 -1
  18. package/dist/cmd/coder/start.d.ts.map +1 -1
  19. package/dist/cmd/coder/start.js +1 -0
  20. package/dist/cmd/coder/start.js.map +1 -1
  21. package/dist/cmd/coder/workspace/common.d.ts +29 -0
  22. package/dist/cmd/coder/workspace/common.d.ts.map +1 -0
  23. package/dist/cmd/coder/workspace/common.js +83 -0
  24. package/dist/cmd/coder/workspace/common.js.map +1 -0
  25. package/dist/cmd/coder/workspace/create.d.ts.map +1 -1
  26. package/dist/cmd/coder/workspace/create.js +34 -37
  27. package/dist/cmd/coder/workspace/create.js.map +1 -1
  28. package/dist/cmd/coder/workspace/get.d.ts.map +1 -1
  29. package/dist/cmd/coder/workspace/get.js +2 -5
  30. package/dist/cmd/coder/workspace/get.js.map +1 -1
  31. package/dist/cmd/coder/workspace/index.d.ts.map +1 -1
  32. package/dist/cmd/coder/workspace/index.js +10 -0
  33. package/dist/cmd/coder/workspace/index.js.map +1 -1
  34. package/dist/cmd/coder/workspace/list.d.ts.map +1 -1
  35. package/dist/cmd/coder/workspace/list.js +4 -0
  36. package/dist/cmd/coder/workspace/list.js.map +1 -1
  37. package/dist/cmd/coder/workspace/refresh.d.ts +2 -0
  38. package/dist/cmd/coder/workspace/refresh.d.ts.map +1 -0
  39. package/dist/cmd/coder/workspace/refresh.js +59 -0
  40. package/dist/cmd/coder/workspace/refresh.js.map +1 -0
  41. package/dist/cmd/coder/workspace/update.d.ts +2 -0
  42. package/dist/cmd/coder/workspace/update.d.ts.map +1 -0
  43. package/dist/cmd/coder/workspace/update.js +131 -0
  44. package/dist/cmd/coder/workspace/update.js.map +1 -0
  45. package/dist/cmd/coder/workspace/validate-dependencies.d.ts +2 -0
  46. package/dist/cmd/coder/workspace/validate-dependencies.d.ts.map +1 -0
  47. package/dist/cmd/coder/workspace/validate-dependencies.js +70 -0
  48. package/dist/cmd/coder/workspace/validate-dependencies.js.map +1 -0
  49. package/dist/cmd/project/random-name.d.ts +17 -0
  50. package/dist/cmd/project/random-name.d.ts.map +1 -0
  51. package/dist/cmd/project/random-name.js +144 -0
  52. package/dist/cmd/project/random-name.js.map +1 -0
  53. package/dist/cmd/project/template-flow.d.ts.map +1 -1
  54. package/dist/cmd/project/template-flow.js +181 -153
  55. package/dist/cmd/project/template-flow.js.map +1 -1
  56. package/dist/composite-logger.d.ts.map +1 -1
  57. package/dist/composite-logger.js +19 -0
  58. package/dist/composite-logger.js.map +1 -1
  59. package/dist/config.d.ts +18 -16
  60. package/dist/config.d.ts.map +1 -1
  61. package/dist/config.js +46 -16
  62. package/dist/config.js.map +1 -1
  63. package/dist/tui/prompt.d.ts +29 -0
  64. package/dist/tui/prompt.d.ts.map +1 -1
  65. package/dist/tui/prompt.js +180 -8
  66. package/dist/tui/prompt.js.map +1 -1
  67. package/package.json +7 -7
  68. package/src/agent-detection.ts +1 -0
  69. package/src/cli.ts +30 -8
  70. package/src/cmd/cloud/sandbox/create.ts +57 -4
  71. package/src/cmd/cloud/sandbox/exec.ts +4 -3
  72. package/src/cmd/cloud/sandbox/run.ts +9 -5
  73. package/src/cmd/cloud/sandbox/snapshot/create.ts +6 -6
  74. package/src/cmd/coder/start.ts +1 -0
  75. package/src/cmd/coder/workspace/common.ts +103 -0
  76. package/src/cmd/coder/workspace/create.ts +39 -43
  77. package/src/cmd/coder/workspace/get.ts +2 -5
  78. package/src/cmd/coder/workspace/index.ts +10 -0
  79. package/src/cmd/coder/workspace/list.ts +4 -0
  80. package/src/cmd/coder/workspace/refresh.ts +63 -0
  81. package/src/cmd/coder/workspace/update.ts +154 -0
  82. package/src/cmd/coder/workspace/validate-dependencies.ts +75 -0
  83. package/src/cmd/project/random-name.ts +152 -0
  84. package/src/cmd/project/template-flow.ts +199 -161
  85. package/src/composite-logger.ts +20 -0
  86. package/src/config.ts +69 -19
  87. package/src/tui/prompt.ts +214 -8
@@ -0,0 +1,144 @@
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
+ const BUCKET_MAX = 63;
14
+ const BUCKET_MIN = 3;
15
+ const DB_MAX = 63;
16
+ /** 3 lowercase alphanumeric chars, e.g. "k7p". */
17
+ function shortSuffix() {
18
+ // toString(36) yields [0-9a-z]; slice 3 chars after the "0." prefix.
19
+ const s = Math.random().toString(36).slice(2, 5);
20
+ // Pad in the (extremely unlikely) case the slice is shorter than 3 chars.
21
+ return s.length === 3 ? s : (s + '000').slice(0, 3);
22
+ }
23
+ /**
24
+ * Sanitize a project name into the bucket-name alphabet:
25
+ * - lowercase
26
+ * - spaces / underscores / dots → hyphens
27
+ * - drop anything else
28
+ * - collapse and trim hyphens
29
+ * - strip reserved prefixes (`agentuity*`, `ag-*`, `ago-*`, `xn--`)
30
+ */
31
+ function sanitizeForBucket(name) {
32
+ let out = name
33
+ .toLowerCase()
34
+ .trim()
35
+ .replace(/[\s_.]+/g, '-')
36
+ .replace(/[^a-z0-9-]/g, '')
37
+ .replace(/-+/g, '-')
38
+ .replace(/^-+|-+$/g, '');
39
+ // Strip reserved prefixes (rules from validateBucketName).
40
+ while (out.startsWith('agentuity') ||
41
+ out.startsWith('ag-') ||
42
+ out.startsWith('ago-') ||
43
+ out.startsWith('xn--')) {
44
+ if (out.startsWith('agentuity'))
45
+ out = out.slice('agentuity'.length);
46
+ else if (out.startsWith('ago-'))
47
+ out = out.slice('ago-'.length);
48
+ else if (out.startsWith('ag-'))
49
+ out = out.slice('ag-'.length);
50
+ else if (out.startsWith('xn--'))
51
+ out = out.slice('xn--'.length);
52
+ out = out.replace(/^-+/, '');
53
+ }
54
+ return out;
55
+ }
56
+ /**
57
+ * Sanitize a project name into the database-name alphabet:
58
+ * - lowercase
59
+ * - non `[a-z0-9_]` → `_`
60
+ * - collapse and trim underscores
61
+ * - ensure it starts with a letter or underscore (prepend `p_` otherwise)
62
+ * - strip reserved `pg_` prefix
63
+ */
64
+ function sanitizeForDatabase(name) {
65
+ let out = name
66
+ .toLowerCase()
67
+ .trim()
68
+ .replace(/[^a-z0-9_]+/g, '_')
69
+ .replace(/_+/g, '_')
70
+ .replace(/^_+|_+$/g, '');
71
+ if (!/^[a-z_]/.test(out)) {
72
+ out = out.length > 0 ? `p_${out}` : '';
73
+ }
74
+ while (out.startsWith('pg_')) {
75
+ out = out.slice(3).replace(/^_+/, '');
76
+ if (!/^[a-z_]/.test(out) && out.length > 0)
77
+ out = `p_${out}`;
78
+ }
79
+ return out;
80
+ }
81
+ /**
82
+ * Truncate so the final string (`<base>-<suffixWithDash>`) fits within `max` chars
83
+ * while keeping the suffix intact and not ending on a hyphen.
84
+ */
85
+ function truncateBaseHyphen(base, suffixWithDash, max) {
86
+ const room = max - suffixWithDash.length;
87
+ if (room <= 0)
88
+ return '';
89
+ let out = base.slice(0, room);
90
+ out = out.replace(/-+$/, '');
91
+ return out;
92
+ }
93
+ /** Same as `truncateBaseHyphen` but for underscore-joined names (database). */
94
+ function truncateBaseUnderscore(base, suffixWithUnderscore, max) {
95
+ const room = max - suffixWithUnderscore.length;
96
+ if (room <= 0)
97
+ return '';
98
+ let out = base.slice(0, room);
99
+ out = out.replace(/_+$/, '');
100
+ return out;
101
+ }
102
+ /**
103
+ * Generate a suggested S3 bucket name derived from the project name.
104
+ * Format: `<sanitized-project>-storage-<3char>` (≤ 63 chars).
105
+ *
106
+ * Falls back to `bucket-<3char><3char>` if the project name produces nothing usable.
107
+ * Always returns a value that passes `validateBucketName`.
108
+ */
109
+ export function suggestBucketName(projectName) {
110
+ const sanitized = sanitizeForBucket(projectName);
111
+ // Try a few times in case sanitization + suffix happens to land on something invalid.
112
+ for (let attempt = 0; attempt < 5; attempt++) {
113
+ const suffix = `-storage-${shortSuffix()}`;
114
+ const base = truncateBaseHyphen(sanitized, suffix, BUCKET_MAX);
115
+ const candidate = base.length > 0 ? `${base}${suffix}` : `bucket${suffix}`;
116
+ if (candidate.length >= BUCKET_MIN && validateBucketName(candidate).valid) {
117
+ return candidate;
118
+ }
119
+ }
120
+ // Pure fallback: short, always-valid generic name.
121
+ const fallback = `bucket-${shortSuffix()}${shortSuffix()}`;
122
+ return validateBucketName(fallback).valid ? fallback : `bucket-${shortSuffix()}aaa`;
123
+ }
124
+ /**
125
+ * Generate a suggested PostgreSQL database name derived from the project name.
126
+ * Format: `<sanitized-project>_db_<3char>` (≤ 63 chars).
127
+ *
128
+ * Falls back to `db_<3char><3char>` if the project name produces nothing usable.
129
+ * Always returns a value that passes `validateDatabaseName`.
130
+ */
131
+ export function suggestDatabaseName(projectName) {
132
+ const sanitized = sanitizeForDatabase(projectName);
133
+ for (let attempt = 0; attempt < 5; attempt++) {
134
+ const suffix = `_db_${shortSuffix()}`;
135
+ const base = truncateBaseUnderscore(sanitized, suffix, DB_MAX);
136
+ const candidate = base.length > 0 ? `${base}${suffix}` : `db${suffix}`;
137
+ if (validateDatabaseName(candidate).valid) {
138
+ return candidate;
139
+ }
140
+ }
141
+ const fallback = `db_${shortSuffix()}${shortSuffix()}`;
142
+ return validateDatabaseName(fallback).valid ? fallback : `db_${shortSuffix()}aaa`;
143
+ }
144
+ //# sourceMappingURL=random-name.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"random-name.js","sourceRoot":"","sources":["../../../src/cmd/project/random-name.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"template-flow.d.ts","sourceRoot":"","sources":["../../../src/cmd/project/template-flow.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAY9C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAe3C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAKpD,UAAU,iBAAiB;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,OAAO,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED,wBAAsB,aAAa,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA8mBzF"}
1
+ {"version":3,"file":"template-flow.d.ts","sourceRoot":"","sources":["../../../src/cmd/project/template-flow.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAY9C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAe3C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAUpD,UAAU,iBAAiB;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,OAAO,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED,wBAAsB,aAAa,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAynBzF"}
@@ -12,7 +12,10 @@ import * as tui from '../../tui';
12
12
  import { createPrompt, note } from '../../tui';
13
13
  import { getGithubBotIdentity } from '../git/api';
14
14
  import { downloadTemplate, initGitRepo, setupProject } from './download';
15
+ import { suggestBucketName, suggestDatabaseName } from './random-name';
15
16
  import { fetchTemplates } from './templates';
17
+ // Domain validator shared between the multi-select branch and the standalone prompt.
18
+ const DOMAIN_REGEX = /^(?=.{1,253}$)(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[A-Za-z]{2,63}$/;
16
19
  export async function runCreateFlow(options) {
17
20
  const { projectName: initialProjectName, dir: targetDir, template: initialTemplate, templateDir, templateBranch, skipPrompts, logger, auth, config, orgId: selectedOrgId, region, apiClient, domains, database: databaseOption, storage: storageOption, } = options;
18
21
  const isHeadless = !process.stdin.isTTY || !process.stdout.isTTY;
@@ -187,7 +190,7 @@ export async function runCreateFlow(options) {
187
190
  error: 'Project setup completed with errors',
188
191
  };
189
192
  }
190
- // Add separator bar if we're going to show resource prompts
193
+ // Resource provisioning gates
191
194
  const canProvision = auth && apiClient && catalystClient && orgId && region;
192
195
  // Only count as resource flags if actually requesting provisioning (not explicit skip)
193
196
  const hasResourceFlags = (databaseOption !== undefined && databaseOption.toLowerCase() !== 'skip') ||
@@ -204,11 +207,56 @@ export async function runCreateFlow(options) {
204
207
  'Remove --no-register or omit --database/--storage flags.', ErrorCode.VALIDATION_FAILED);
205
208
  }
206
209
  if (canProvision) {
207
- // Fetch resources for selected org and region using Catalyst API (needed for both interactive and CLI flags)
210
+ // CLI flags pre-resolve their respective per-resource decision; the multi-select
211
+ // is only used for resources where the user didn't pass a flag.
212
+ const dbFlagAction = resolveFlagAction(databaseOption, 'database');
213
+ const storageFlagAction = resolveFlagAction(storageOption, 'storage');
214
+ const domainFlagProvided = (domains?.length ?? 0) > 0;
215
+ // Determine which resources should run through the configuration phase.
216
+ // In interactive mode, ask the user via a single multi-select.
217
+ // In headless / non-interactive mode, only flagged resources are considered.
218
+ let wantDb = dbFlagAction !== undefined && dbFlagAction !== 'Skip';
219
+ let wantStorage = storageFlagAction !== undefined && storageFlagAction !== 'Skip';
220
+ let wantDomain = domainFlagProvided;
221
+ if (isInteractive) {
222
+ // Build multi-select options dynamically: only show resources the user hasn't
223
+ // already decided about via CLI flags. If all three came from flags, skip the prompt.
224
+ const msOptions = [];
225
+ if (dbFlagAction === undefined) {
226
+ msOptions.push({ value: 'database', label: 'SQL Database', hint: 'PostgreSQL' });
227
+ }
228
+ if (storageFlagAction === undefined) {
229
+ msOptions.push({ value: 'storage', label: 'Storage Bucket', hint: 'S3-compatible' });
230
+ }
231
+ if (!domainFlagProvided) {
232
+ msOptions.push({ value: 'domain', label: 'Custom Domain', hint: 'BYO domain' });
233
+ }
234
+ if (msOptions.length > 0) {
235
+ const picked = await prompt.multiselect({
236
+ message: 'What would you like to set up? (all optional)',
237
+ options: msOptions,
238
+ initial: [],
239
+ });
240
+ if (dbFlagAction === undefined)
241
+ wantDb = picked.includes('database');
242
+ if (storageFlagAction === undefined)
243
+ wantStorage = picked.includes('storage');
244
+ if (!domainFlagProvided)
245
+ wantDomain = picked.includes('domain');
246
+ }
247
+ }
248
+ // Fetch existing resources only if we'll actually need them.
249
+ // Need them when:
250
+ // - user wants db/storage in interactive mode (to offer "use existing")
251
+ // - a CLI flag pointed at an existing resource by name
208
252
  let resources;
209
- const needResources = isInteractive ||
210
- (databaseOption && databaseOption !== 'skip' && databaseOption !== 'new') ||
211
- (storageOption && storageOption !== 'skip' && storageOption !== 'new');
253
+ const needResources = (isInteractive && (wantDb || wantStorage)) ||
254
+ (databaseOption !== undefined &&
255
+ dbFlagAction !== 'Create New' &&
256
+ dbFlagAction !== 'Skip') ||
257
+ (storageOption !== undefined &&
258
+ storageFlagAction !== 'Create New' &&
259
+ storageFlagAction !== 'Skip');
212
260
  if (needResources) {
213
261
  resources = await tui.spinner({
214
262
  message: 'Fetching resources',
@@ -221,204 +269,163 @@ export async function runCreateFlow(options) {
221
269
  logger.debug(`Resources for org ${orgId} in region ${region}: ${resources.db.length} databases, ${resources.s3.length} storage buckets`);
222
270
  logger.debug(`Database names: ${resources.db.map((d) => d.name).join(', ') || '(none)'}`);
223
271
  logger.debug(`Storage buckets: ${resources.s3.map((b) => b.bucket_name).join(', ') || '(none)'}`);
224
- }
225
- // Determine database action: CLI flag > interactive prompt > skip (headless)
226
- let db_action;
227
- if (databaseOption !== undefined) {
228
- // CLI flag provided - normalize to expected values
229
- if (databaseOption.toLowerCase() === 'new') {
230
- db_action = 'Create New';
272
+ // Validate flag-supplied resource names against the fetched list.
273
+ if (databaseOption !== undefined &&
274
+ dbFlagAction !== 'Create New' &&
275
+ dbFlagAction !== 'Skip' &&
276
+ !resources.db.find((d) => d.name === dbFlagAction)) {
277
+ logger.fatal(`Database '${databaseOption}' not found. Use 'new' to create a new database or 'skip' to skip.`, ErrorCode.RESOURCE_NOT_FOUND);
231
278
  }
232
- else if (databaseOption.toLowerCase() === 'skip') {
233
- db_action = 'Skip';
279
+ if (storageOption !== undefined &&
280
+ storageFlagAction !== 'Create New' &&
281
+ storageFlagAction !== 'Skip' &&
282
+ !resources.s3.find((b) => b.bucket_name === storageFlagAction)) {
283
+ logger.fatal(`Storage bucket '${storageOption}' not found. Use 'new' to create a new bucket or 'skip' to skip.`, ErrorCode.RESOURCE_NOT_FOUND);
234
284
  }
235
- else {
236
- // Existing database name - validate it exists
237
- const existingDb = resources?.db.find((d) => d.name === databaseOption);
238
- if (!existingDb) {
239
- logger.fatal(`Database '${databaseOption}' not found. Use 'new' to create a new database or 'skip' to skip.`, ErrorCode.RESOURCE_NOT_FOUND);
240
- }
241
- db_action = databaseOption;
242
- }
243
- }
244
- else if (isInteractive) {
245
- db_action = await prompt.select({
246
- message: 'Create SQL Database?',
247
- options: [
248
- { value: 'Skip', label: 'Skip or Setup later' },
249
- { value: 'Create New', label: 'Create a new database' },
250
- ...resources.db.map((db) => ({
251
- value: db.name,
252
- label: `Use database: ${tui.tuiColors.primary(db.name)}`,
253
- })),
254
- ],
255
- });
256
- }
257
- else {
258
- // Headless without flag - skip
259
- db_action = 'Skip';
260
285
  }
261
- // Determine storage action: CLI flag > interactive prompt > skip (headless)
262
- let s3_action;
263
- if (storageOption !== undefined) {
264
- // CLI flag provided - normalize to expected values
265
- if (storageOption.toLowerCase() === 'new') {
266
- s3_action = 'Create New';
267
- }
268
- else if (storageOption.toLowerCase() === 'skip') {
269
- s3_action = 'Skip';
270
- }
271
- else {
272
- // Existing bucket name - validate it exists
273
- const existingBucket = resources?.s3.find((b) => b.bucket_name === storageOption);
274
- if (!existingBucket) {
275
- logger.fatal(`Storage bucket '${storageOption}' not found. Use 'new' to create a new bucket or 'skip' to skip.`, ErrorCode.RESOURCE_NOT_FOUND);
286
+ // === Configure each selected resource: Database Storage Domain ===
287
+ // Database
288
+ if (wantDb) {
289
+ let dbAction = dbFlagAction;
290
+ if (dbAction === undefined && isInteractive) {
291
+ const existing = resources?.db ?? [];
292
+ if (existing.length > 0) {
293
+ dbAction = await prompt.select({
294
+ message: 'SQL Database',
295
+ options: [
296
+ { value: 'Create New', label: 'Create a new database' },
297
+ ...existing.map((db) => ({
298
+ value: db.name,
299
+ label: `Use database: ${tui.tuiColors.primary(db.name)}`,
300
+ })),
301
+ ],
302
+ });
303
+ }
304
+ else {
305
+ // No existing databases — user already opted in via the multi-select, so create new.
306
+ dbAction = 'Create New';
276
307
  }
277
- s3_action = storageOption;
278
- }
279
- }
280
- else if (isInteractive) {
281
- s3_action = await prompt.select({
282
- message: 'Create Storage Bucket?',
283
- options: [
284
- { value: 'Skip', label: 'Skip or Setup later' },
285
- { value: 'Create New', label: 'Create a new bucket' },
286
- ...resources.s3.map((bucket) => ({
287
- value: bucket.bucket_name,
288
- label: `Use bucket: ${tui.tuiColors.primary(bucket.bucket_name)}`,
289
- })),
290
- ],
291
- });
292
- }
293
- else {
294
- // Headless without flag - skip
295
- s3_action = 'Skip';
296
- }
297
- // Custom DNS: only prompt in interactive mode if not already provided
298
- if (!domains?.length && isInteractive) {
299
- const customDns = await prompt.text({
300
- message: 'Setup custom DNS?',
301
- hint: 'Enter a domain name or press Enter to skip',
302
- validate: (val) => val === ''
303
- ? true
304
- : /^(?=.{1,253}$)(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[A-Za-z]{2,63}$/.test(val),
305
- });
306
- if (customDns) {
307
- _domains = [customDns];
308
308
  }
309
- }
310
- // Process storage action
311
- switch (s3_action) {
312
- case 'Create New': {
313
- let bucketName;
314
- let bucketDescription;
315
- // Only prompt for name/description in interactive mode
309
+ if (dbAction === 'Create New') {
310
+ let dbName;
311
+ let dbDescription;
316
312
  if (isInteractive) {
317
- const bucketNameInput = await prompt.text({
318
- message: 'Bucket name',
319
- hint: 'Optional - lowercase letters, digits, hyphens only',
313
+ const suggestion = suggestDatabaseName(projectName);
314
+ const dbNameInput = await prompt.text({
315
+ message: 'Database name',
316
+ hint: 'Optional · lowercase letters, digits, underscores',
317
+ placeholder: suggestion,
320
318
  validate: (value) => {
321
319
  const trimmed = value.trim();
322
320
  if (trimmed === '')
323
321
  return true;
324
- const result = validateBucketName(trimmed);
322
+ const result = validateDatabaseName(trimmed);
325
323
  return result.valid ? true : result.error;
326
324
  },
327
325
  });
328
- bucketName = bucketNameInput.trim() || undefined;
329
- bucketDescription =
326
+ dbName = dbNameInput.trim() || undefined;
327
+ dbDescription =
330
328
  (await prompt.text({
331
- message: 'Bucket description',
332
- hint: 'Optional - press Enter to skip',
329
+ message: 'Database description',
330
+ hint: 'Optional · press Enter to skip',
333
331
  })) || undefined;
334
332
  }
335
333
  const created = await tui.spinner({
336
- message: 'Provisioning New Bucket',
334
+ message: 'Provisioning New SQL Database',
337
335
  clearOnSuccess: true,
338
336
  callback: async () => {
339
337
  return createResources(catalystClient, orgId, region, [
340
- {
341
- type: 's3',
342
- name: bucketName,
343
- description: bucketDescription,
344
- },
338
+ { type: 'db', name: dbName, description: dbDescription },
345
339
  ]);
346
340
  },
347
341
  });
348
- // Collect env vars from newly created resource
349
- if (created[0]?.env) {
342
+ if (created[0]?.env)
350
343
  Object.assign(resourceEnvVars, created[0].env);
351
- }
352
- break;
353
344
  }
354
- case 'Skip': {
355
- break;
345
+ else if (dbAction && dbAction !== 'Skip') {
346
+ // Existing database selected — reuse its env vars.
347
+ const selectedDb = resources?.db.find((d) => d.name === dbAction);
348
+ if (selectedDb?.env)
349
+ Object.assign(resourceEnvVars, selectedDb.env);
356
350
  }
357
- default: {
358
- // User selected an existing bucket - get env vars from the resources list
359
- const selectedBucket = resources?.s3.find((b) => b.bucket_name === s3_action);
360
- if (selectedBucket?.env) {
361
- Object.assign(resourceEnvVars, selectedBucket.env);
351
+ }
352
+ // Storage
353
+ if (wantStorage) {
354
+ let s3Action = storageFlagAction;
355
+ if (s3Action === undefined && isInteractive) {
356
+ const existing = resources?.s3 ?? [];
357
+ if (existing.length > 0) {
358
+ s3Action = await prompt.select({
359
+ message: 'Storage Bucket',
360
+ options: [
361
+ { value: 'Create New', label: 'Create a new bucket' },
362
+ ...existing.map((bucket) => ({
363
+ value: bucket.bucket_name,
364
+ label: `Use bucket: ${tui.tuiColors.primary(bucket.bucket_name)}`,
365
+ })),
366
+ ],
367
+ });
368
+ }
369
+ else {
370
+ s3Action = 'Create New';
362
371
  }
363
- break;
364
372
  }
365
- }
366
- // Process database action
367
- switch (db_action) {
368
- case 'Create New': {
369
- let dbName;
370
- let dbDescription;
371
- // Only prompt for name/description in interactive mode
373
+ if (s3Action === 'Create New') {
374
+ let bucketName;
375
+ let bucketDescription;
372
376
  if (isInteractive) {
373
- const dbNameInput = await prompt.text({
374
- message: 'Database name',
375
- hint: 'Optional - lowercase letters, digits, underscores only',
377
+ const suggestion = suggestBucketName(projectName);
378
+ const bucketNameInput = await prompt.text({
379
+ message: 'Bucket name',
380
+ hint: 'Optional · lowercase letters, digits, hyphens',
381
+ placeholder: suggestion,
376
382
  validate: (value) => {
377
383
  const trimmed = value.trim();
378
384
  if (trimmed === '')
379
385
  return true;
380
- const result = validateDatabaseName(trimmed);
386
+ const result = validateBucketName(trimmed);
381
387
  return result.valid ? true : result.error;
382
388
  },
383
389
  });
384
- dbName = dbNameInput.trim() || undefined;
385
- dbDescription =
390
+ bucketName = bucketNameInput.trim() || undefined;
391
+ bucketDescription =
386
392
  (await prompt.text({
387
- message: 'Database description',
388
- hint: 'Optional - press Enter to skip',
393
+ message: 'Bucket description',
394
+ hint: 'Optional · press Enter to skip',
389
395
  })) || undefined;
390
396
  }
391
397
  const created = await tui.spinner({
392
- message: 'Provisioning New SQL Database',
398
+ message: 'Provisioning New Bucket',
393
399
  clearOnSuccess: true,
394
400
  callback: async () => {
395
401
  return createResources(catalystClient, orgId, region, [
396
- {
397
- type: 'db',
398
- name: dbName,
399
- description: dbDescription,
400
- },
402
+ { type: 's3', name: bucketName, description: bucketDescription },
401
403
  ]);
402
404
  },
403
405
  });
404
- // Collect env vars from newly created resource
405
- if (created[0]?.env) {
406
+ if (created[0]?.env)
406
407
  Object.assign(resourceEnvVars, created[0].env);
407
- }
408
- break;
409
408
  }
410
- case 'Skip': {
411
- break;
412
- }
413
- default: {
414
- // User selected an existing database - get env vars from the resources list
415
- const selectedDb = resources?.db.find((d) => d.name === db_action);
416
- if (selectedDb?.env) {
417
- Object.assign(resourceEnvVars, selectedDb.env);
418
- }
419
- break;
409
+ else if (s3Action && s3Action !== 'Skip') {
410
+ const selectedBucket = resources?.s3.find((b) => b.bucket_name === s3Action);
411
+ if (selectedBucket?.env)
412
+ Object.assign(resourceEnvVars, selectedBucket.env);
420
413
  }
421
414
  }
415
+ // Custom Domain
416
+ if (wantDomain && !domainFlagProvided && isInteractive) {
417
+ const customDns = await prompt.text({
418
+ message: 'Custom domain',
419
+ hint: 'e.g. agents.example.com',
420
+ validate: (val) => val === ''
421
+ ? 'Domain is required (or go back and uncheck Custom Domain)'
422
+ : DOMAIN_REGEX.test(val)
423
+ ? true
424
+ : 'Invalid domain',
425
+ });
426
+ if (customDns)
427
+ _domains = [customDns];
428
+ }
422
429
  }
423
430
  let projectId;
424
431
  if (auth && apiClient && orgId) {
@@ -553,6 +560,27 @@ export async function runCreateFlow(options) {
553
560
  error: setupResult.success ? undefined : 'Project setup completed with errors',
554
561
  };
555
562
  }
563
+ /**
564
+ * Normalize a CLI flag value (`--database` / `--storage`) into the same
565
+ * action vocabulary the interactive flow uses:
566
+ * - 'new' -> 'Create New'
567
+ * - 'skip' -> 'Skip'
568
+ * - any other string -> treated as an existing-resource name (returned as-is)
569
+ * - undefined -> undefined (no flag passed; multi-select decides)
570
+ *
571
+ * The existence check for named resources happens later, after the resource
572
+ * list is fetched.
573
+ */
574
+ function resolveFlagAction(flag, _kind) {
575
+ if (flag === undefined)
576
+ return undefined;
577
+ const lower = flag.toLowerCase();
578
+ if (lower === 'new')
579
+ return 'Create New';
580
+ if (lower === 'skip')
581
+ return 'Skip';
582
+ return flag;
583
+ }
556
584
  /**
557
585
  * Sanitize a project name to create a safe directory/package name
558
586
  * - Converts to lowercase