@agentuity/cli 0.1.12 → 0.1.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 (140) hide show
  1. package/dist/auth.d.ts +1 -1
  2. package/dist/auth.d.ts.map +1 -1
  3. package/dist/auth.js +6 -2
  4. package/dist/auth.js.map +1 -1
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +44 -91
  7. package/dist/cli.js.map +1 -1
  8. package/dist/cmd/auth/index.d.ts.map +1 -1
  9. package/dist/cmd/auth/index.js +3 -0
  10. package/dist/cmd/auth/index.js.map +1 -1
  11. package/dist/cmd/auth/org/index.d.ts +2 -0
  12. package/dist/cmd/auth/org/index.d.ts.map +1 -0
  13. package/dist/cmd/auth/org/index.js +121 -0
  14. package/dist/cmd/auth/org/index.js.map +1 -0
  15. package/dist/cmd/build/vite/beacon-plugin.d.ts +19 -0
  16. package/dist/cmd/build/vite/beacon-plugin.d.ts.map +1 -0
  17. package/dist/cmd/build/vite/beacon-plugin.js +137 -0
  18. package/dist/cmd/build/vite/beacon-plugin.js.map +1 -0
  19. package/dist/cmd/build/vite/vite-builder.d.ts +2 -0
  20. package/dist/cmd/build/vite/vite-builder.d.ts.map +1 -1
  21. package/dist/cmd/build/vite/vite-builder.js +12 -2
  22. package/dist/cmd/build/vite/vite-builder.js.map +1 -1
  23. package/dist/cmd/build/webanalytics-generator.js +25 -9
  24. package/dist/cmd/build/webanalytics-generator.js.map +1 -1
  25. package/dist/cmd/cloud/db/get.d.ts.map +1 -1
  26. package/dist/cmd/cloud/db/get.js +7 -0
  27. package/dist/cmd/cloud/db/get.js.map +1 -1
  28. package/dist/cmd/cloud/db/list.d.ts.map +1 -1
  29. package/dist/cmd/cloud/db/list.js +19 -6
  30. package/dist/cmd/cloud/db/list.js.map +1 -1
  31. package/dist/cmd/cloud/deploy.d.ts.map +1 -1
  32. package/dist/cmd/cloud/deploy.js +24 -1
  33. package/dist/cmd/cloud/deploy.js.map +1 -1
  34. package/dist/cmd/cloud/deployment/show.d.ts.map +1 -1
  35. package/dist/cmd/cloud/deployment/show.js +5 -0
  36. package/dist/cmd/cloud/deployment/show.js.map +1 -1
  37. package/dist/cmd/cloud/index.d.ts.map +1 -1
  38. package/dist/cmd/cloud/index.js +3 -0
  39. package/dist/cmd/cloud/index.js.map +1 -1
  40. package/dist/cmd/cloud/region/index.d.ts +2 -0
  41. package/dist/cmd/cloud/region/index.d.ts.map +1 -0
  42. package/dist/cmd/cloud/region/index.js +136 -0
  43. package/dist/cmd/cloud/region/index.js.map +1 -0
  44. package/dist/cmd/cloud/sandbox/snapshot/build.d.ts.map +1 -1
  45. package/dist/cmd/cloud/sandbox/snapshot/build.js +35 -5
  46. package/dist/cmd/cloud/sandbox/snapshot/build.js.map +1 -1
  47. package/dist/cmd/cloud/scp/download.d.ts.map +1 -1
  48. package/dist/cmd/cloud/scp/download.js +4 -2
  49. package/dist/cmd/cloud/scp/download.js.map +1 -1
  50. package/dist/cmd/cloud/scp/upload.d.ts.map +1 -1
  51. package/dist/cmd/cloud/scp/upload.js +4 -2
  52. package/dist/cmd/cloud/scp/upload.js.map +1 -1
  53. package/dist/cmd/cloud/ssh.d.ts.map +1 -1
  54. package/dist/cmd/cloud/ssh.js +3 -1
  55. package/dist/cmd/cloud/ssh.js.map +1 -1
  56. package/dist/cmd/cloud/storage/get.d.ts.map +1 -1
  57. package/dist/cmd/cloud/storage/get.js +12 -5
  58. package/dist/cmd/cloud/storage/get.js.map +1 -1
  59. package/dist/cmd/cloud/storage/list.d.ts.map +1 -1
  60. package/dist/cmd/cloud/storage/list.js +10 -0
  61. package/dist/cmd/cloud/storage/list.js.map +1 -1
  62. package/dist/cmd/dev/index.d.ts.map +1 -1
  63. package/dist/cmd/dev/index.js +62 -5
  64. package/dist/cmd/dev/index.js.map +1 -1
  65. package/dist/cmd/help/index.d.ts.map +1 -1
  66. package/dist/cmd/help/index.js +8 -18
  67. package/dist/cmd/help/index.js.map +1 -1
  68. package/dist/cmd/project/create.d.ts.map +1 -1
  69. package/dist/cmd/project/create.js +10 -7
  70. package/dist/cmd/project/create.js.map +1 -1
  71. package/dist/cmd/project/import.d.ts +2 -0
  72. package/dist/cmd/project/import.d.ts.map +1 -0
  73. package/dist/cmd/project/import.js +88 -0
  74. package/dist/cmd/project/import.js.map +1 -0
  75. package/dist/cmd/project/index.d.ts.map +1 -1
  76. package/dist/cmd/project/index.js +3 -0
  77. package/dist/cmd/project/index.js.map +1 -1
  78. package/dist/cmd/project/reconcile.d.ts +67 -0
  79. package/dist/cmd/project/reconcile.d.ts.map +1 -0
  80. package/dist/cmd/project/reconcile.js +458 -0
  81. package/dist/cmd/project/reconcile.js.map +1 -0
  82. package/dist/cmd/project/template-flow.d.ts +11 -1
  83. package/dist/cmd/project/template-flow.d.ts.map +1 -1
  84. package/dist/cmd/project/template-flow.js +25 -7
  85. package/dist/cmd/project/template-flow.js.map +1 -1
  86. package/dist/config.d.ts +8 -3
  87. package/dist/config.d.ts.map +1 -1
  88. package/dist/config.js +50 -21
  89. package/dist/config.js.map +1 -1
  90. package/dist/legacy-check.d.ts.map +1 -1
  91. package/dist/legacy-check.js +8 -0
  92. package/dist/legacy-check.js.map +1 -1
  93. package/dist/program-ref.d.ts +4 -0
  94. package/dist/program-ref.d.ts.map +1 -0
  95. package/dist/program-ref.js +8 -0
  96. package/dist/program-ref.js.map +1 -0
  97. package/dist/regions.d.ts +8 -0
  98. package/dist/regions.d.ts.map +1 -0
  99. package/dist/regions.js +77 -0
  100. package/dist/regions.js.map +1 -0
  101. package/dist/tui.d.ts.map +1 -1
  102. package/dist/tui.js +5 -4
  103. package/dist/tui.js.map +1 -1
  104. package/dist/types.d.ts +1 -0
  105. package/dist/types.d.ts.map +1 -1
  106. package/dist/types.js +1 -0
  107. package/dist/types.js.map +1 -1
  108. package/package.json +6 -6
  109. package/src/auth.ts +8 -8
  110. package/src/cli.ts +52 -108
  111. package/src/cmd/auth/index.ts +3 -0
  112. package/src/cmd/auth/org/index.ts +142 -0
  113. package/src/cmd/build/vite/beacon-plugin.ts +162 -0
  114. package/src/cmd/build/vite/vite-builder.ts +15 -2
  115. package/src/cmd/build/webanalytics-generator.ts +25 -9
  116. package/src/cmd/cloud/db/get.ts +7 -0
  117. package/src/cmd/cloud/db/list.ts +20 -6
  118. package/src/cmd/cloud/deploy.ts +32 -1
  119. package/src/cmd/cloud/deployment/show.ts +5 -0
  120. package/src/cmd/cloud/index.ts +3 -0
  121. package/src/cmd/cloud/region/index.ts +157 -0
  122. package/src/cmd/cloud/sandbox/snapshot/build.ts +42 -5
  123. package/src/cmd/cloud/scp/download.ts +6 -2
  124. package/src/cmd/cloud/scp/upload.ts +6 -2
  125. package/src/cmd/cloud/ssh.ts +5 -1
  126. package/src/cmd/cloud/storage/get.ts +12 -5
  127. package/src/cmd/cloud/storage/list.ts +11 -0
  128. package/src/cmd/dev/index.ts +62 -5
  129. package/src/cmd/help/index.ts +8 -22
  130. package/src/cmd/project/create.ts +10 -7
  131. package/src/cmd/project/import.ts +98 -0
  132. package/src/cmd/project/index.ts +3 -0
  133. package/src/cmd/project/reconcile.ts +606 -0
  134. package/src/cmd/project/template-flow.ts +37 -7
  135. package/src/config.ts +58 -22
  136. package/src/legacy-check.ts +10 -0
  137. package/src/program-ref.ts +11 -0
  138. package/src/regions.ts +95 -0
  139. package/src/tui.ts +6 -4
  140. package/src/types.ts +1 -0
@@ -0,0 +1,606 @@
1
+ import { join, basename } from 'node:path';
2
+ import { existsSync, statSync } from 'node:fs';
3
+ import type { Logger } from '@agentuity/core';
4
+ import {
5
+ projectGet,
6
+ projectCreate,
7
+ projectEnvUpdate,
8
+ listOrganizations,
9
+ type OrganizationList,
10
+ type RegionList,
11
+ } from '@agentuity/server';
12
+ import type { APIClient } from '../../api';
13
+ import type { AuthData, Config, Project } from '../../types';
14
+ import { loadProjectConfig, createProjectConfig } from '../../config';
15
+ import * as tui from '../../tui';
16
+ import { createPrompt } from '../../tui';
17
+ import { isTTY } from '../../auth';
18
+ import {
19
+ findExistingEnvFile,
20
+ readEnvFile,
21
+ writeEnvFile,
22
+ filterAgentuitySdkKeys,
23
+ splitEnvAndSecrets,
24
+ } from '../../env-util';
25
+ import { fetchRegionsWithCache } from '../../regions';
26
+
27
+ export interface ReconcileResult {
28
+ status: 'valid' | 'imported' | 'skipped' | 'error';
29
+ project?: Project;
30
+ message?: string;
31
+ }
32
+
33
+ export interface ReconcileOptions {
34
+ dir: string;
35
+ auth: AuthData;
36
+ apiClient: APIClient;
37
+ config: Config;
38
+ logger: Logger;
39
+ interactive?: boolean;
40
+ /** If true, skip prompts and just validate */
41
+ validateOnly?: boolean;
42
+ }
43
+
44
+ /**
45
+ * Dependencies that can be injected for testing
46
+ */
47
+ export interface ReconcileDeps {
48
+ projectGet: typeof projectGet;
49
+ projectCreate: typeof projectCreate;
50
+ projectEnvUpdate: typeof projectEnvUpdate;
51
+ listOrganizations: typeof listOrganizations;
52
+ loadProjectConfig: typeof loadProjectConfig;
53
+ createProjectConfig: typeof createProjectConfig;
54
+ isTTY: typeof isTTY;
55
+ confirm: typeof tui.confirm;
56
+ selectOrganization: typeof tui.selectOrganization;
57
+ }
58
+
59
+ const defaultDeps: ReconcileDeps = {
60
+ projectGet,
61
+ projectCreate,
62
+ projectEnvUpdate,
63
+ listOrganizations,
64
+ loadProjectConfig,
65
+ createProjectConfig,
66
+ isTTY,
67
+ confirm: tui.confirm,
68
+ selectOrganization: tui.selectOrganization,
69
+ };
70
+
71
+ /**
72
+ * Try to load project config, returning null if not found or invalid
73
+ * @internal Exported for testing
74
+ */
75
+ export async function tryLoadProjectConfig(
76
+ dir: string,
77
+ config: Config | null,
78
+ deps: Pick<ReconcileDeps, 'loadProjectConfig'> = defaultDeps
79
+ ): Promise<Project | null> {
80
+ try {
81
+ return await deps.loadProjectConfig(dir, config);
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Get the default project name from package.json or directory name
89
+ * @internal Exported for testing
90
+ */
91
+ export async function getDefaultProjectName(dir: string): Promise<string> {
92
+ const pkgPath = join(dir, 'package.json');
93
+ if (await Bun.file(pkgPath).exists()) {
94
+ try {
95
+ const pkg = await Bun.file(pkgPath).json();
96
+ if (pkg.name && typeof pkg.name === 'string' && pkg.name.trim()) {
97
+ // Strip org scope if present (e.g., @myorg/project-name -> project-name)
98
+ return pkg.name.replace(/^@[^/]+\//, '').trim();
99
+ }
100
+ } catch {
101
+ // Fall through to directory name
102
+ }
103
+ }
104
+ return basename(dir);
105
+ }
106
+
107
+ /**
108
+ * Check if a directory contains a valid Agentuity project structure
109
+ * @internal Exported for testing
110
+ */
111
+ export async function isValidProjectStructure(dir: string): Promise<boolean> {
112
+ // Check 1: package.json with @agentuity/runtime and agentuity.config.ts
113
+ const pkgPath = join(dir, 'package.json');
114
+ const configPath = join(dir, 'agentuity.config.ts');
115
+
116
+ if (await Bun.file(pkgPath).exists()) {
117
+ try {
118
+ const pkg = await Bun.file(pkgPath).json();
119
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
120
+ if (deps['@agentuity/runtime'] && (await Bun.file(configPath).exists())) {
121
+ return true;
122
+ }
123
+ } catch {
124
+ // Fall through to check child project
125
+ }
126
+ }
127
+
128
+ // Check 2: ./agentuity/ subdirectory exists with valid structure (parent project with child)
129
+ const agentuityDir = join(dir, 'agentuity');
130
+ if (existsSync(agentuityDir) && statSync(agentuityDir).isDirectory()) {
131
+ const childPkgPath = join(agentuityDir, 'package.json');
132
+ const childConfigPath = join(agentuityDir, 'agentuity.config.ts');
133
+
134
+ if (await Bun.file(childPkgPath).exists()) {
135
+ try {
136
+ const childPkg = await Bun.file(childPkgPath).json();
137
+ const childDeps = { ...childPkg.dependencies, ...childPkg.devDependencies };
138
+ if (childDeps['@agentuity/runtime'] && (await Bun.file(childConfigPath).exists())) {
139
+ return true;
140
+ }
141
+ } catch {
142
+ // Invalid package.json in child - fall through
143
+ }
144
+ }
145
+ }
146
+
147
+ return false;
148
+ }
149
+
150
+ /**
151
+ * Update or create .env file with new SDK key
152
+ */
153
+ async function updateSdkKeyInEnv(dir: string, sdkKey: string): Promise<void> {
154
+ const envPath = join(dir, '.env');
155
+ const envFile = Bun.file(envPath);
156
+
157
+ if (await envFile.exists()) {
158
+ // Update existing .env - read, modify, write
159
+ const existing = await readEnvFile(envPath);
160
+ existing.AGENTUITY_SDK_KEY = sdkKey;
161
+ await writeEnvFile(envPath, existing);
162
+ } else {
163
+ // Create new .env
164
+ const comment =
165
+ '# AGENTUITY_SDK_KEY is a sensitive value and should not be committed to version control.';
166
+ const content = `${comment}\nAGENTUITY_SDK_KEY=${sdkKey}\n`;
167
+ await Bun.write(envPath, content);
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Sync existing environment variables to the new project
173
+ */
174
+ async function syncEnvToProject(
175
+ dir: string,
176
+ projectId: string,
177
+ apiClient: APIClient,
178
+ logger: Logger
179
+ ): Promise<void> {
180
+ try {
181
+ const envFilePath = await findExistingEnvFile(dir);
182
+ const localEnv = await readEnvFile(envFilePath);
183
+ const filteredEnv = filterAgentuitySdkKeys(localEnv);
184
+
185
+ if (Object.keys(filteredEnv).length > 0) {
186
+ const { env, secrets } = splitEnvAndSecrets(filteredEnv);
187
+ await projectEnvUpdate(apiClient, {
188
+ id: projectId,
189
+ env,
190
+ secrets,
191
+ });
192
+ logger.debug(`Synced ${Object.keys(filteredEnv).length} environment variables to cloud`);
193
+ }
194
+ } catch (error) {
195
+ // Non-fatal: just log the error
196
+ logger.debug('Failed to sync environment variables:', error);
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Prompt user to select an organization
202
+ */
203
+ async function selectOrg(
204
+ orgs: OrganizationList,
205
+ config: Config,
206
+ defaultOrgId?: string
207
+ ): Promise<string> {
208
+ return tui.selectOrganization(orgs, defaultOrgId ?? config.preferences?.orgId);
209
+ }
210
+
211
+ /**
212
+ * Prompt user to select a region from the available regions
213
+ */
214
+ async function selectRegion(regions: RegionList, defaultRegion?: string): Promise<string> {
215
+ if (regions.length === 0) {
216
+ throw new Error('No cloud regions available');
217
+ }
218
+
219
+ if (regions.length === 1) {
220
+ return regions[0].region;
221
+ }
222
+
223
+ // Build options from API regions
224
+ const options = regions.map((r) => ({
225
+ value: r.region,
226
+ label: `${r.description} (${r.region})`,
227
+ }));
228
+
229
+ // Move default to top if found
230
+ const defaultValue = defaultRegion ?? regions[0].region;
231
+ const defaultIndex = options.findIndex((r) => r.value === defaultValue);
232
+ if (defaultIndex > 0) {
233
+ const [defaultItem] = options.splice(defaultIndex, 1);
234
+ options.unshift(defaultItem);
235
+ }
236
+
237
+ const prompt = createPrompt();
238
+ return prompt.select({
239
+ message: 'Select a region:',
240
+ options,
241
+ initial: options[0].value,
242
+ });
243
+ }
244
+
245
+ /**
246
+ * Prompt user for text input with validation
247
+ */
248
+ async function textPrompt(options: {
249
+ message: string;
250
+ initial?: string;
251
+ validate?: (value: string) => boolean | string;
252
+ }): Promise<string> {
253
+ const prompt = createPrompt();
254
+ return prompt.text({
255
+ message: options.message,
256
+ initial: options.initial,
257
+ hint: options.initial ? `(default: ${options.initial})` : undefined,
258
+ validate: options.validate,
259
+ });
260
+ }
261
+
262
+ /**
263
+ * Import an existing project (with invalid/inaccessible agentuity.json) to user's org
264
+ */
265
+ async function importExistingProject(
266
+ opts: ReconcileOptions,
267
+ existingConfig: Project,
268
+ orgs: OrganizationList
269
+ ): Promise<ReconcileResult> {
270
+ const { dir, apiClient, config, logger } = opts;
271
+
272
+ tui.warning(
273
+ "You don't have access to this project. It may have been deleted or transferred to another organization."
274
+ );
275
+ tui.newline();
276
+
277
+ const shouldImport = await tui.confirm(
278
+ 'Would you like to import this project to your organization?',
279
+ true
280
+ );
281
+
282
+ if (!shouldImport) {
283
+ return { status: 'skipped', message: 'Project import cancelled.' };
284
+ }
285
+
286
+ tui.newline();
287
+
288
+ // Select org
289
+ const orgId = await selectOrg(orgs, config, existingConfig.orgId);
290
+
291
+ // Fetch regions and select
292
+ const regions = await tui.spinner({
293
+ message: 'Fetching regions',
294
+ clearOnSuccess: true,
295
+ callback: () => fetchRegionsWithCache(config.name, apiClient, logger),
296
+ });
297
+ const region = await selectRegion(regions, existingConfig.region);
298
+
299
+ // Get project name
300
+ const defaultName = await getDefaultProjectName(dir);
301
+ const projectName = await textPrompt({
302
+ message: 'Project name:',
303
+ initial: defaultName,
304
+ validate: (value: string) => {
305
+ if (!value || value.trim().length === 0) {
306
+ return 'Project name is required';
307
+ }
308
+ return true;
309
+ },
310
+ });
311
+
312
+ // Create the project
313
+ const newProject = await tui.spinner({
314
+ message: 'Registering project',
315
+ clearOnSuccess: true,
316
+ callback: async () => {
317
+ return projectCreate(apiClient, {
318
+ name: projectName,
319
+ orgId,
320
+ cloudRegion: region,
321
+ });
322
+ },
323
+ });
324
+
325
+ // Update .env with new SDK key
326
+ await updateSdkKeyInEnv(dir, newProject.sdkKey);
327
+ tui.success('Updated AGENTUITY_SDK_KEY in .env');
328
+
329
+ // Create new agentuity.json
330
+ await createProjectConfig(dir, {
331
+ projectId: newProject.id,
332
+ orgId,
333
+ sdkKey: newProject.sdkKey,
334
+ region,
335
+ });
336
+ tui.success('Updated agentuity.json');
337
+
338
+ // Sync env vars
339
+ await tui.spinner({
340
+ message: 'Syncing environment variables',
341
+ clearOnSuccess: true,
342
+ callback: () => syncEnvToProject(dir, newProject.id, apiClient, logger),
343
+ });
344
+
345
+ const project: Project = {
346
+ projectId: newProject.id,
347
+ orgId,
348
+ region,
349
+ };
350
+
351
+ tui.success('Project imported successfully!');
352
+
353
+ return { status: 'imported', project };
354
+ }
355
+
356
+ /**
357
+ * Create a new project from an unregistered local project
358
+ */
359
+ async function createNewProject(opts: ReconcileOptions): Promise<ReconcileResult> {
360
+ const { dir, apiClient, config, logger } = opts;
361
+
362
+ tui.warning('This project is not registered with Agentuity Cloud.');
363
+ tui.newline();
364
+
365
+ const shouldCreate = await tui.confirm('Would you like to register it now?', true);
366
+
367
+ if (!shouldCreate) {
368
+ return { status: 'skipped', message: 'Project registration cancelled.' };
369
+ }
370
+
371
+ tui.newline();
372
+
373
+ // Fetch user's orgs
374
+ const orgs = await tui.spinner({
375
+ message: 'Fetching organizations',
376
+ clearOnSuccess: true,
377
+ callback: () => listOrganizations(apiClient),
378
+ });
379
+
380
+ if (orgs.length === 0) {
381
+ return { status: 'error', message: 'No organizations found for your account.' };
382
+ }
383
+
384
+ // Select org
385
+ const orgId = await selectOrg(orgs, config);
386
+
387
+ // Fetch regions and select
388
+ const regions = await tui.spinner({
389
+ message: 'Fetching regions',
390
+ clearOnSuccess: true,
391
+ callback: () => fetchRegionsWithCache(config.name, apiClient, logger),
392
+ });
393
+ const region = await selectRegion(regions);
394
+
395
+ // Get project name from package.json or prompt
396
+ const defaultName = await getDefaultProjectName(dir);
397
+ const projectName = await textPrompt({
398
+ message: 'Project name:',
399
+ initial: defaultName,
400
+ validate: (value: string) => {
401
+ if (!value || value.trim().length === 0) {
402
+ return 'Project name is required';
403
+ }
404
+ return true;
405
+ },
406
+ });
407
+
408
+ // Create the project
409
+ const newProject = await tui.spinner({
410
+ message: 'Registering project',
411
+ clearOnSuccess: true,
412
+ callback: async () => {
413
+ return projectCreate(apiClient, {
414
+ name: projectName,
415
+ orgId,
416
+ cloudRegion: region,
417
+ });
418
+ },
419
+ });
420
+
421
+ // Update/create .env with SDK key
422
+ await updateSdkKeyInEnv(dir, newProject.sdkKey);
423
+ tui.success('Updated AGENTUITY_SDK_KEY in .env');
424
+
425
+ // Create agentuity.json
426
+ await createProjectConfig(dir, {
427
+ projectId: newProject.id,
428
+ orgId,
429
+ sdkKey: newProject.sdkKey,
430
+ region,
431
+ });
432
+ tui.success('Created agentuity.json');
433
+
434
+ // Sync env vars
435
+ await tui.spinner({
436
+ message: 'Syncing environment variables',
437
+ clearOnSuccess: true,
438
+ callback: () => syncEnvToProject(dir, newProject.id, apiClient, logger),
439
+ });
440
+
441
+ const project: Project = {
442
+ projectId: newProject.id,
443
+ orgId,
444
+ region,
445
+ };
446
+
447
+ tui.success('Project registered successfully!');
448
+
449
+ return { status: 'imported', project };
450
+ }
451
+
452
+ /**
453
+ * Reconcile a project - validate access or import if needed
454
+ *
455
+ * This function checks if the current directory has a valid agentuity.json
456
+ * and if the user has access to the project. If not, it offers to import
457
+ * the project to the user's organization.
458
+ *
459
+ * For directories without agentuity.json, it validates the project structure
460
+ * and offers to register the project with Agentuity Cloud.
461
+ */
462
+ export async function reconcileProject(opts: ReconcileOptions): Promise<ReconcileResult> {
463
+ const { dir, apiClient, config, logger, interactive = isTTY(), validateOnly } = opts;
464
+
465
+ // 1. Check if agentuity.json exists
466
+ const projectConfig = await tryLoadProjectConfig(dir, config);
467
+
468
+ if (projectConfig) {
469
+ // 2. Validate access to existing project
470
+ try {
471
+ const project = await projectGet(apiClient, { id: projectConfig.projectId, keys: false });
472
+
473
+ // 3. Check if orgId matches user's orgs
474
+ const userOrgs = await listOrganizations(apiClient);
475
+ const hasAccess = userOrgs.some((org) => org.id === project.orgId);
476
+
477
+ if (hasAccess) {
478
+ return { status: 'valid', project: projectConfig };
479
+ }
480
+
481
+ // User doesn't have access - offer to import
482
+ if (!interactive || validateOnly) {
483
+ return {
484
+ status: 'error',
485
+ message:
486
+ "You don't have access to this project. Run interactively to import it to your organization.",
487
+ };
488
+ }
489
+
490
+ return await importExistingProject(opts, projectConfig, userOrgs);
491
+ } catch (err) {
492
+ // Project not found or access denied
493
+ logger.debug('Failed to get project:', err);
494
+
495
+ if (!interactive || validateOnly) {
496
+ return {
497
+ status: 'error',
498
+ message:
499
+ 'Project not found or access denied. Run interactively to import it to your organization.',
500
+ };
501
+ }
502
+
503
+ const userOrgs = await listOrganizations(apiClient);
504
+ return await importExistingProject(opts, projectConfig, userOrgs);
505
+ }
506
+ }
507
+
508
+ // 4. No agentuity.json - validate project structure
509
+ const isValid = await isValidProjectStructure(dir);
510
+
511
+ if (!isValid) {
512
+ return {
513
+ status: 'error',
514
+ message:
515
+ 'This directory does not appear to be a valid Agentuity project. ' +
516
+ 'Expected agentuity.config.ts and @agentuity/runtime dependency, ' +
517
+ 'or an agentuity/ subdirectory.',
518
+ };
519
+ }
520
+
521
+ if (!interactive || validateOnly) {
522
+ return {
523
+ status: 'error',
524
+ message:
525
+ 'Project must be registered with Agentuity Cloud. ' +
526
+ 'Run interactively or use "agentuity project import".',
527
+ };
528
+ }
529
+
530
+ // 5. Prompt to create new project
531
+ return await createNewProject(opts);
532
+ }
533
+
534
+ /**
535
+ * Run project import directly (for the import command)
536
+ */
537
+ export async function runProjectImport(opts: ReconcileOptions): Promise<ReconcileResult> {
538
+ const { dir, apiClient, config, interactive = true, validateOnly = false } = opts;
539
+
540
+ // Check if agentuity.json already exists and is valid
541
+ const projectConfig = await tryLoadProjectConfig(dir, config);
542
+
543
+ if (projectConfig) {
544
+ try {
545
+ const project = await projectGet(apiClient, { id: projectConfig.projectId, keys: false });
546
+ const userOrgs = await listOrganizations(apiClient);
547
+ const hasAccess = userOrgs.some((org) => org.id === project.orgId);
548
+
549
+ if (hasAccess) {
550
+ tui.info('This project is already registered and you have access to it.');
551
+ return { status: 'valid', project: projectConfig };
552
+ }
553
+
554
+ // Has agentuity.json but no access - offer to import
555
+ if (!interactive) {
556
+ return {
557
+ status: 'error',
558
+ message:
559
+ "You don't have access to this project. Run interactively to import it to your organization.",
560
+ };
561
+ }
562
+
563
+ return await importExistingProject(opts, projectConfig, userOrgs);
564
+ } catch {
565
+ // Project doesn't exist - offer to import
566
+ if (!interactive) {
567
+ return {
568
+ status: 'error',
569
+ message: 'Project not found. Run interactively to import it to your organization.',
570
+ };
571
+ }
572
+
573
+ const userOrgs = await listOrganizations(apiClient);
574
+ return await importExistingProject(opts, projectConfig, userOrgs);
575
+ }
576
+ }
577
+
578
+ // No agentuity.json - validate structure and create new project
579
+ const isValid = await isValidProjectStructure(dir);
580
+
581
+ if (!isValid) {
582
+ return {
583
+ status: 'error',
584
+ message:
585
+ 'This directory does not appear to be a valid Agentuity project. ' +
586
+ 'Expected agentuity.config.ts and @agentuity/runtime dependency, ' +
587
+ 'or an agentuity/ subdirectory.',
588
+ };
589
+ }
590
+
591
+ if (validateOnly) {
592
+ return {
593
+ status: 'valid',
594
+ message: 'Project structure is valid and ready to import.',
595
+ };
596
+ }
597
+
598
+ if (!interactive) {
599
+ return {
600
+ status: 'error',
601
+ message: 'Project import requires interactive mode.',
602
+ };
603
+ }
604
+
605
+ return await createNewProject(opts);
606
+ }
@@ -10,6 +10,7 @@ import {
10
10
  getServiceUrls,
11
11
  APIClient as ServerAPIClient,
12
12
  createResources,
13
+ validateDatabaseName,
13
14
  } from '@agentuity/server';
14
15
  import type { Logger } from '@agentuity/core';
15
16
  import * as tui from '../../tui';
@@ -56,7 +57,18 @@ interface CreateFlowOptions {
56
57
  apiClient?: APIClient;
57
58
  }
58
59
 
59
- export async function runCreateFlow(options: CreateFlowOptions): Promise<void> {
60
+ export interface CreateFlowResult {
61
+ projectId?: string;
62
+ orgId?: string;
63
+ name: string;
64
+ path: string;
65
+ template: string;
66
+ installed: boolean;
67
+ built: boolean;
68
+ domains?: string[];
69
+ }
70
+
71
+ export async function runCreateFlow(options: CreateFlowOptions): Promise<CreateFlowResult> {
60
72
  const {
61
73
  projectName: initialProjectName,
62
74
  dir: targetDir,
@@ -168,7 +180,7 @@ export async function runCreateFlow(options: CreateFlowOptions): Promise<void> {
168
180
  const home = homedir();
169
181
  if (dest === '/' || dest === home) {
170
182
  logger.fatal(`Refusing to delete protected path: ${dest}`, ErrorCode.VALIDATION_FAILED);
171
- return;
183
+ return undefined as never;
172
184
  }
173
185
  rmSync(dest, { recursive: true, force: true });
174
186
  tui.success(`Deleted ${dest}`);
@@ -193,7 +205,7 @@ export async function runCreateFlow(options: CreateFlowOptions): Promise<void> {
193
205
  `Template "${initialTemplate}" not found\n\nAvailable templates:\n${availableTemplates}`,
194
206
  ErrorCode.RESOURCE_NOT_FOUND
195
207
  );
196
- return;
208
+ return undefined as never;
197
209
  }
198
210
  selectedTemplate = found;
199
211
  } else if (skipPrompts || templates.length === 1) {
@@ -222,7 +234,7 @@ export async function runCreateFlow(options: CreateFlowOptions): Promise<void> {
222
234
  const found = templates.find((t) => t.id === templateId);
223
235
  if (!found) {
224
236
  logger.fatal('Template selection failed', ErrorCode.USER_CANCELLED);
225
- return;
237
+ return undefined as never;
226
238
  }
227
239
  selectedTemplate = found;
228
240
  }
@@ -342,10 +354,17 @@ export async function runCreateFlow(options: CreateFlowOptions): Promise<void> {
342
354
  }
343
355
  switch (choices.db_action) {
344
356
  case 'Create New': {
345
- const dbName = await prompt.text({
357
+ const dbNameInput = await prompt.text({
346
358
  message: 'Database name',
347
- hint: 'Optional - press Enter to auto-generate',
359
+ hint: 'Optional - lowercase letters, digits, underscores only',
360
+ validate: (value: string) => {
361
+ const trimmed = value.trim();
362
+ if (trimmed === '') return true;
363
+ const result = validateDatabaseName(trimmed);
364
+ return result.valid ? true : result.error!;
365
+ },
348
366
  });
367
+ const dbName = dbNameInput.trim() || undefined;
349
368
  const dbDescription = await prompt.text({
350
369
  message: 'Database description',
351
370
  hint: 'Optional - press Enter to skip',
@@ -357,7 +376,7 @@ export async function runCreateFlow(options: CreateFlowOptions): Promise<void> {
357
376
  return createResources(catalystClient, orgId, region!, [
358
377
  {
359
378
  type: 'db',
360
- name: dbName || undefined,
379
+ name: dbName,
361
380
  description: dbDescription || undefined,
362
381
  },
363
382
  ]);
@@ -616,6 +635,17 @@ export async function runCreateFlow(options: CreateFlowOptions): Promise<void> {
616
635
  if (authEnabled && !templateHasAuth) {
617
636
  printIntegrationExamples();
618
637
  }
638
+
639
+ return {
640
+ projectId,
641
+ orgId,
642
+ name: projectName,
643
+ path: dest,
644
+ template: selectedTemplate.id,
645
+ installed: !options.noInstall,
646
+ built: !options.noBuild,
647
+ domains: _domains,
648
+ };
619
649
  }
620
650
 
621
651
  /**