@agentuity/cli 1.0.32 → 1.0.33

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 (257) hide show
  1. package/dist/catalyst.d.ts +3 -1
  2. package/dist/catalyst.d.ts.map +1 -1
  3. package/dist/catalyst.js +5 -1
  4. package/dist/catalyst.js.map +1 -1
  5. package/dist/cmd/cloud/db/create.d.ts.map +1 -1
  6. package/dist/cmd/cloud/db/create.js +8 -10
  7. package/dist/cmd/cloud/db/create.js.map +1 -1
  8. package/dist/cmd/cloud/db/delete.d.ts.map +1 -1
  9. package/dist/cmd/cloud/db/delete.js +12 -14
  10. package/dist/cmd/cloud/db/delete.js.map +1 -1
  11. package/dist/cmd/cloud/db/get.d.ts.map +1 -1
  12. package/dist/cmd/cloud/db/get.js +7 -7
  13. package/dist/cmd/cloud/db/get.js.map +1 -1
  14. package/dist/cmd/cloud/db/list.d.ts.map +1 -1
  15. package/dist/cmd/cloud/db/list.js +5 -5
  16. package/dist/cmd/cloud/db/list.js.map +1 -1
  17. package/dist/cmd/cloud/db/logs.d.ts.map +1 -1
  18. package/dist/cmd/cloud/db/logs.js +7 -7
  19. package/dist/cmd/cloud/db/logs.js.map +1 -1
  20. package/dist/cmd/cloud/db/sql.js +5 -5
  21. package/dist/cmd/cloud/db/sql.js.map +1 -1
  22. package/dist/cmd/cloud/db/stats.d.ts.map +1 -1
  23. package/dist/cmd/cloud/db/stats.js +5 -5
  24. package/dist/cmd/cloud/db/stats.js.map +1 -1
  25. package/dist/cmd/cloud/deploy.d.ts.map +1 -1
  26. package/dist/cmd/cloud/deploy.js +1 -1
  27. package/dist/cmd/cloud/deploy.js.map +1 -1
  28. package/dist/cmd/cloud/email/stats.d.ts.map +1 -1
  29. package/dist/cmd/cloud/email/stats.js +5 -5
  30. package/dist/cmd/cloud/email/stats.js.map +1 -1
  31. package/dist/cmd/cloud/email/util.d.ts +2 -2
  32. package/dist/cmd/cloud/email/util.d.ts.map +1 -1
  33. package/dist/cmd/cloud/email/util.js +2 -2
  34. package/dist/cmd/cloud/email/util.js.map +1 -1
  35. package/dist/cmd/cloud/keyvalue/util.d.ts.map +1 -1
  36. package/dist/cmd/cloud/keyvalue/util.js +1 -1
  37. package/dist/cmd/cloud/keyvalue/util.js.map +1 -1
  38. package/dist/cmd/cloud/machine/delete.d.ts.map +1 -1
  39. package/dist/cmd/cloud/machine/delete.js +6 -6
  40. package/dist/cmd/cloud/machine/delete.js.map +1 -1
  41. package/dist/cmd/cloud/machine/deployments.d.ts.map +1 -1
  42. package/dist/cmd/cloud/machine/deployments.js +5 -5
  43. package/dist/cmd/cloud/machine/deployments.js.map +1 -1
  44. package/dist/cmd/cloud/machine/get.d.ts.map +1 -1
  45. package/dist/cmd/cloud/machine/get.js +5 -5
  46. package/dist/cmd/cloud/machine/get.js.map +1 -1
  47. package/dist/cmd/cloud/machine/list.d.ts.map +1 -1
  48. package/dist/cmd/cloud/machine/list.js +5 -5
  49. package/dist/cmd/cloud/machine/list.js.map +1 -1
  50. package/dist/cmd/cloud/queue/util.d.ts +1 -1
  51. package/dist/cmd/cloud/queue/util.d.ts.map +1 -1
  52. package/dist/cmd/cloud/queue/util.js +1 -1
  53. package/dist/cmd/cloud/queue/util.js.map +1 -1
  54. package/dist/cmd/cloud/redis/get.js +5 -5
  55. package/dist/cmd/cloud/redis/get.js.map +1 -1
  56. package/dist/cmd/cloud/sandbox/execution/get.d.ts.map +1 -1
  57. package/dist/cmd/cloud/sandbox/execution/get.js +4 -4
  58. package/dist/cmd/cloud/sandbox/execution/get.js.map +1 -1
  59. package/dist/cmd/cloud/sandbox/execution/list.d.ts.map +1 -1
  60. package/dist/cmd/cloud/sandbox/execution/list.js +5 -5
  61. package/dist/cmd/cloud/sandbox/execution/list.js.map +1 -1
  62. package/dist/cmd/cloud/sandbox/runtime/list.d.ts.map +1 -1
  63. package/dist/cmd/cloud/sandbox/runtime/list.js +4 -4
  64. package/dist/cmd/cloud/sandbox/runtime/list.js.map +1 -1
  65. package/dist/cmd/cloud/sandbox/snapshot/build.js +1 -1
  66. package/dist/cmd/cloud/sandbox/snapshot/build.js.map +1 -1
  67. package/dist/cmd/cloud/sandbox/snapshot/create.d.ts.map +1 -1
  68. package/dist/cmd/cloud/sandbox/snapshot/create.js +5 -5
  69. package/dist/cmd/cloud/sandbox/snapshot/create.js.map +1 -1
  70. package/dist/cmd/cloud/sandbox/snapshot/delete.d.ts.map +1 -1
  71. package/dist/cmd/cloud/sandbox/snapshot/delete.js +4 -4
  72. package/dist/cmd/cloud/sandbox/snapshot/delete.js.map +1 -1
  73. package/dist/cmd/cloud/sandbox/snapshot/get.d.ts.map +1 -1
  74. package/dist/cmd/cloud/sandbox/snapshot/get.js +4 -4
  75. package/dist/cmd/cloud/sandbox/snapshot/get.js.map +1 -1
  76. package/dist/cmd/cloud/sandbox/snapshot/list.d.ts.map +1 -1
  77. package/dist/cmd/cloud/sandbox/snapshot/list.js +4 -4
  78. package/dist/cmd/cloud/sandbox/snapshot/list.js.map +1 -1
  79. package/dist/cmd/cloud/sandbox/snapshot/tag.d.ts.map +1 -1
  80. package/dist/cmd/cloud/sandbox/snapshot/tag.js +4 -4
  81. package/dist/cmd/cloud/sandbox/snapshot/tag.js.map +1 -1
  82. package/dist/cmd/cloud/sandbox/stats.d.ts.map +1 -1
  83. package/dist/cmd/cloud/sandbox/stats.js +5 -5
  84. package/dist/cmd/cloud/sandbox/stats.js.map +1 -1
  85. package/dist/cmd/cloud/sandbox/util.d.ts +3 -3
  86. package/dist/cmd/cloud/sandbox/util.d.ts.map +1 -1
  87. package/dist/cmd/cloud/sandbox/util.js +5 -5
  88. package/dist/cmd/cloud/sandbox/util.js.map +1 -1
  89. package/dist/cmd/cloud/schedule/stats.d.ts.map +1 -1
  90. package/dist/cmd/cloud/schedule/stats.js +5 -5
  91. package/dist/cmd/cloud/schedule/stats.js.map +1 -1
  92. package/dist/cmd/cloud/schedule/util.d.ts +1 -1
  93. package/dist/cmd/cloud/schedule/util.d.ts.map +1 -1
  94. package/dist/cmd/cloud/schedule/util.js +1 -1
  95. package/dist/cmd/cloud/schedule/util.js.map +1 -1
  96. package/dist/cmd/cloud/services/stats.d.ts.map +1 -1
  97. package/dist/cmd/cloud/services/stats.js +5 -5
  98. package/dist/cmd/cloud/services/stats.js.map +1 -1
  99. package/dist/cmd/cloud/session/get.d.ts.map +1 -1
  100. package/dist/cmd/cloud/session/get.js +5 -5
  101. package/dist/cmd/cloud/session/get.js.map +1 -1
  102. package/dist/cmd/cloud/session/list.d.ts.map +1 -1
  103. package/dist/cmd/cloud/session/list.js +5 -5
  104. package/dist/cmd/cloud/session/list.js.map +1 -1
  105. package/dist/cmd/cloud/storage/config.d.ts.map +1 -1
  106. package/dist/cmd/cloud/storage/config.js +7 -7
  107. package/dist/cmd/cloud/storage/config.js.map +1 -1
  108. package/dist/cmd/cloud/storage/create.d.ts.map +1 -1
  109. package/dist/cmd/cloud/storage/create.js +8 -10
  110. package/dist/cmd/cloud/storage/create.js.map +1 -1
  111. package/dist/cmd/cloud/storage/delete.d.ts.map +1 -1
  112. package/dist/cmd/cloud/storage/delete.js +12 -14
  113. package/dist/cmd/cloud/storage/delete.js.map +1 -1
  114. package/dist/cmd/cloud/storage/download.d.ts.map +1 -1
  115. package/dist/cmd/cloud/storage/download.js +6 -6
  116. package/dist/cmd/cloud/storage/download.js.map +1 -1
  117. package/dist/cmd/cloud/storage/get.d.ts.map +1 -1
  118. package/dist/cmd/cloud/storage/get.js +6 -6
  119. package/dist/cmd/cloud/storage/get.js.map +1 -1
  120. package/dist/cmd/cloud/storage/list.d.ts.map +1 -1
  121. package/dist/cmd/cloud/storage/list.js +6 -6
  122. package/dist/cmd/cloud/storage/list.js.map +1 -1
  123. package/dist/cmd/cloud/storage/upload.d.ts.map +1 -1
  124. package/dist/cmd/cloud/storage/upload.js +7 -7
  125. package/dist/cmd/cloud/storage/upload.js.map +1 -1
  126. package/dist/cmd/cloud/stream/create.js +1 -1
  127. package/dist/cmd/cloud/stream/create.js.map +1 -1
  128. package/dist/cmd/cloud/stream/stats.d.ts.map +1 -1
  129. package/dist/cmd/cloud/stream/stats.js +5 -5
  130. package/dist/cmd/cloud/stream/stats.js.map +1 -1
  131. package/dist/cmd/cloud/task/stats.d.ts.map +1 -1
  132. package/dist/cmd/cloud/task/stats.js +5 -5
  133. package/dist/cmd/cloud/task/stats.js.map +1 -1
  134. package/dist/cmd/cloud/task/util.d.ts +1 -1
  135. package/dist/cmd/cloud/task/util.d.ts.map +1 -1
  136. package/dist/cmd/cloud/task/util.js +2 -2
  137. package/dist/cmd/cloud/task/util.js.map +1 -1
  138. package/dist/cmd/cloud/thread/delete.d.ts.map +1 -1
  139. package/dist/cmd/cloud/thread/delete.js +5 -5
  140. package/dist/cmd/cloud/thread/delete.js.map +1 -1
  141. package/dist/cmd/cloud/thread/get.d.ts.map +1 -1
  142. package/dist/cmd/cloud/thread/get.js +5 -5
  143. package/dist/cmd/cloud/thread/get.js.map +1 -1
  144. package/dist/cmd/cloud/thread/list.d.ts.map +1 -1
  145. package/dist/cmd/cloud/thread/list.js +5 -5
  146. package/dist/cmd/cloud/thread/list.js.map +1 -1
  147. package/dist/cmd/cloud/vector/util.d.ts.map +1 -1
  148. package/dist/cmd/cloud/vector/util.js +1 -1
  149. package/dist/cmd/cloud/vector/util.js.map +1 -1
  150. package/dist/cmd/cloud/webhook/util.d.ts +1 -1
  151. package/dist/cmd/cloud/webhook/util.d.ts.map +1 -1
  152. package/dist/cmd/cloud/webhook/util.js +1 -1
  153. package/dist/cmd/cloud/webhook/util.js.map +1 -1
  154. package/dist/cmd/git/api.d.ts +38 -0
  155. package/dist/cmd/git/api.d.ts.map +1 -1
  156. package/dist/cmd/git/api.js +55 -0
  157. package/dist/cmd/git/api.js.map +1 -1
  158. package/dist/cmd/project/add/database.js +8 -8
  159. package/dist/cmd/project/add/database.js.map +1 -1
  160. package/dist/cmd/project/add/storage.js +8 -8
  161. package/dist/cmd/project/add/storage.js.map +1 -1
  162. package/dist/cmd/project/auth/init.d.ts.map +1 -1
  163. package/dist/cmd/project/auth/init.js +7 -6
  164. package/dist/cmd/project/auth/init.js.map +1 -1
  165. package/dist/cmd/project/auth/shared.d.ts +2 -3
  166. package/dist/cmd/project/auth/shared.d.ts.map +1 -1
  167. package/dist/cmd/project/auth/shared.js +7 -7
  168. package/dist/cmd/project/auth/shared.js.map +1 -1
  169. package/dist/cmd/project/download.d.ts +12 -1
  170. package/dist/cmd/project/download.d.ts.map +1 -1
  171. package/dist/cmd/project/download.js +37 -14
  172. package/dist/cmd/project/download.js.map +1 -1
  173. package/dist/cmd/project/import.d.ts.map +1 -1
  174. package/dist/cmd/project/import.js +67 -9
  175. package/dist/cmd/project/import.js.map +1 -1
  176. package/dist/cmd/project/remote-import.d.ts +41 -0
  177. package/dist/cmd/project/remote-import.d.ts.map +1 -0
  178. package/dist/cmd/project/remote-import.js +1074 -0
  179. package/dist/cmd/project/remote-import.js.map +1 -0
  180. package/dist/cmd/project/template-flow.d.ts +1 -1
  181. package/dist/cmd/project/template-flow.d.ts.map +1 -1
  182. package/dist/cmd/project/template-flow.js +27 -11
  183. package/dist/cmd/project/template-flow.js.map +1 -1
  184. package/dist/config.d.ts +29 -5
  185. package/dist/config.d.ts.map +1 -1
  186. package/dist/config.js +13 -14
  187. package/dist/config.js.map +1 -1
  188. package/dist/schema-parser.d.ts.map +1 -1
  189. package/dist/schema-parser.js +47 -5
  190. package/dist/schema-parser.js.map +1 -1
  191. package/dist/types.d.ts +28 -3
  192. package/dist/types.d.ts.map +1 -1
  193. package/dist/types.js +42 -0
  194. package/dist/types.js.map +1 -1
  195. package/package.json +6 -6
  196. package/src/catalyst.ts +9 -1
  197. package/src/cmd/cloud/db/create.ts +8 -9
  198. package/src/cmd/cloud/db/delete.ts +18 -13
  199. package/src/cmd/cloud/db/get.ts +13 -7
  200. package/src/cmd/cloud/db/list.ts +11 -5
  201. package/src/cmd/cloud/db/logs.ts +13 -7
  202. package/src/cmd/cloud/db/sql.ts +5 -5
  203. package/src/cmd/cloud/db/stats.ts +11 -5
  204. package/src/cmd/cloud/deploy.ts +7 -1
  205. package/src/cmd/cloud/email/stats.ts +11 -5
  206. package/src/cmd/cloud/email/util.ts +4 -4
  207. package/src/cmd/cloud/keyvalue/util.ts +2 -2
  208. package/src/cmd/cloud/machine/delete.ts +12 -6
  209. package/src/cmd/cloud/machine/deployments.ts +11 -5
  210. package/src/cmd/cloud/machine/get.ts +11 -5
  211. package/src/cmd/cloud/machine/list.ts +11 -5
  212. package/src/cmd/cloud/queue/util.ts +2 -2
  213. package/src/cmd/cloud/redis/get.ts +5 -5
  214. package/src/cmd/cloud/sandbox/execution/get.ts +10 -4
  215. package/src/cmd/cloud/sandbox/execution/list.ts +6 -5
  216. package/src/cmd/cloud/sandbox/runtime/list.ts +10 -4
  217. package/src/cmd/cloud/sandbox/snapshot/build.ts +1 -1
  218. package/src/cmd/cloud/sandbox/snapshot/create.ts +12 -5
  219. package/src/cmd/cloud/sandbox/snapshot/delete.ts +10 -4
  220. package/src/cmd/cloud/sandbox/snapshot/get.ts +12 -6
  221. package/src/cmd/cloud/sandbox/snapshot/list.ts +10 -4
  222. package/src/cmd/cloud/sandbox/snapshot/tag.ts +10 -4
  223. package/src/cmd/cloud/sandbox/stats.ts +11 -5
  224. package/src/cmd/cloud/sandbox/util.ts +14 -7
  225. package/src/cmd/cloud/schedule/stats.ts +11 -5
  226. package/src/cmd/cloud/schedule/util.ts +3 -3
  227. package/src/cmd/cloud/services/stats.ts +13 -7
  228. package/src/cmd/cloud/session/get.ts +14 -8
  229. package/src/cmd/cloud/session/list.ts +11 -5
  230. package/src/cmd/cloud/storage/config.ts +24 -12
  231. package/src/cmd/cloud/storage/create.ts +8 -9
  232. package/src/cmd/cloud/storage/delete.ts +18 -13
  233. package/src/cmd/cloud/storage/download.ts +12 -6
  234. package/src/cmd/cloud/storage/get.ts +12 -6
  235. package/src/cmd/cloud/storage/list.ts +12 -6
  236. package/src/cmd/cloud/storage/upload.ts +13 -7
  237. package/src/cmd/cloud/stream/create.ts +1 -1
  238. package/src/cmd/cloud/stream/stats.ts +11 -5
  239. package/src/cmd/cloud/task/stats.ts +11 -5
  240. package/src/cmd/cloud/task/util.ts +4 -4
  241. package/src/cmd/cloud/thread/delete.ts +11 -5
  242. package/src/cmd/cloud/thread/get.ts +11 -5
  243. package/src/cmd/cloud/thread/list.ts +11 -5
  244. package/src/cmd/cloud/vector/util.ts +2 -2
  245. package/src/cmd/cloud/webhook/util.ts +2 -2
  246. package/src/cmd/git/api.ts +127 -0
  247. package/src/cmd/project/add/database.ts +9 -9
  248. package/src/cmd/project/add/storage.ts +9 -9
  249. package/src/cmd/project/auth/init.ts +11 -10
  250. package/src/cmd/project/auth/shared.ts +15 -10
  251. package/src/cmd/project/download.ts +52 -16
  252. package/src/cmd/project/import.ts +71 -9
  253. package/src/cmd/project/remote-import.ts +1347 -0
  254. package/src/cmd/project/template-flow.ts +38 -22
  255. package/src/config.ts +23 -18
  256. package/src/schema-parser.ts +48 -5
  257. package/src/types.ts +45 -0
@@ -0,0 +1,1074 @@
1
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { parseEnvExample, StructuredError } from '@agentuity/core';
5
+ import { createQueue, createResources, listOrganizations, listQueues, listResources, projectCreate, validateDatabaseName, } from '@agentuity/server';
6
+ import { isTTY } from '../../auth';
7
+ import { createProjectConfig, getCatalystAPIClient, getGlobalCatalystAPIClient, } from '../../config';
8
+ import { addResourceEnvVars } from '../../env-util';
9
+ import { getDefaultBranch, isGitAvailable } from '../../git-helper';
10
+ import { fetchRegionsWithCache } from '../../regions';
11
+ import * as tui from '../../tui';
12
+ import { createPrompt } from '../../tui';
13
+ import { checkGithubRepo, createGithubRepo, getGithubBotIdentity, getGithubToken, linkProjectToRepo, } from '../git/api';
14
+ import { initGitRepo } from './download';
15
+ // ─── Structured Errors ───
16
+ const RemoteImportInvalidURLError = StructuredError('RemoteImportInvalidURLError');
17
+ const RemoteImportUnsupportedHostError = StructuredError('RemoteImportUnsupportedHostError');
18
+ const RemoteImportDownloadError = StructuredError('RemoteImportDownloadError');
19
+ const RemoteImportExtractError = StructuredError('RemoteImportExtractError');
20
+ const RemoteImportNoOrganizationError = StructuredError('RemoteImportNoOrganizationError', 'No organizations found for your account');
21
+ const RemoteImportNoRegionError = StructuredError('RemoteImportNoRegionError', 'No cloud regions available');
22
+ const RemoteImportInvalidRepoError = StructuredError('RemoteImportInvalidRepoError');
23
+ const RemoteImportGitError = StructuredError('RemoteImportGitError');
24
+ const RemoteImportDeployError = StructuredError('RemoteImportDeployError');
25
+ const RemoteImportDirectoryNotFoundError = StructuredError('RemoteImportDirectoryNotFoundError');
26
+ const RemoteImportConfigError = StructuredError('RemoteImportConfigError');
27
+ /**
28
+ * Sanitize a string by removing any embedded GitHub tokens from URLs.
29
+ * Prevents token leakage in error messages and logs.
30
+ */
31
+ function sanitizeTokens(msg) {
32
+ return msg.replace(/x-access-token:[^@]+@/g, 'x-access-token:***@');
33
+ }
34
+ /**
35
+ * Build GitHub API request headers, using the Agentuity-managed GitHub token
36
+ * when available, falling back to the GITHUB_TOKEN env var.
37
+ */
38
+ async function githubHeaders(apiClient) {
39
+ const headers = {
40
+ Accept: 'application/vnd.github+json',
41
+ 'User-Agent': 'Agentuity-CLI',
42
+ };
43
+ try {
44
+ const { token } = await getGithubToken(apiClient);
45
+ headers.Authorization = `Bearer ${token}`;
46
+ }
47
+ catch {
48
+ // Fallback to GITHUB_TOKEN env var
49
+ const githubToken = process.env.GITHUB_TOKEN;
50
+ if (githubToken) {
51
+ headers.Authorization = `Bearer ${githubToken}`;
52
+ }
53
+ }
54
+ return headers;
55
+ }
56
+ /**
57
+ * Fetch the default branch of a GitHub repository via the API.
58
+ * Falls back to 'main' on any error.
59
+ */
60
+ async function fetchDefaultBranch(owner, repo, apiClient) {
61
+ try {
62
+ const headers = await githubHeaders(apiClient);
63
+ const resp = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
64
+ headers,
65
+ });
66
+ if (!resp.ok)
67
+ return 'main';
68
+ const data = (await resp.json());
69
+ return data.default_branch ?? 'main';
70
+ }
71
+ catch {
72
+ return 'main';
73
+ }
74
+ }
75
+ /**
76
+ * Parse a GitHub URL into its components.
77
+ *
78
+ * Supported formats:
79
+ * https://github.com/owner/repo
80
+ * https://github.com/owner/repo/tree/branch
81
+ * https://github.com/owner/repo/tree/branch/path/to/dir
82
+ *
83
+ * When the URL does not include a branch (no `/tree/…` segment), the GitHub
84
+ * API is queried to discover the repository's default branch.
85
+ */
86
+ export async function parseGitHubUrl(url, apiClient) {
87
+ let parsed;
88
+ try {
89
+ parsed = new URL(url);
90
+ }
91
+ catch {
92
+ throw new RemoteImportInvalidURLError({ message: `Invalid URL: ${url}` });
93
+ }
94
+ if (parsed.hostname !== 'github.com') {
95
+ throw new RemoteImportUnsupportedHostError({
96
+ message: `Only GitHub URLs are supported. Got: ${parsed.hostname}`,
97
+ });
98
+ }
99
+ // pathname is like /owner/repo or /owner/repo/tree/branch/path
100
+ const parts = parsed.pathname.replace(/^\//, '').replace(/\/$/, '').split('/');
101
+ if (parts.length < 2) {
102
+ throw new RemoteImportInvalidURLError({
103
+ message: `Invalid GitHub URL: expected at least owner/repo in path. Got: ${parsed.pathname}`,
104
+ });
105
+ }
106
+ const owner = parts[0];
107
+ // Strip .git suffix from repo name if present
108
+ const repo = parts[1].replace(/\.git$/, '');
109
+ let branch;
110
+ let directory;
111
+ // /owner/repo/tree/branch[/path/to/dir]
112
+ if (parts.length >= 4 && parts[2] === 'tree') {
113
+ branch = parts[3];
114
+ if (parts.length > 4) {
115
+ directory = parts.slice(4).join('/');
116
+ }
117
+ }
118
+ else {
119
+ // No branch in URL — ask GitHub for the repo's default branch
120
+ branch = await fetchDefaultBranch(owner, repo, apiClient);
121
+ }
122
+ return { owner, repo, branch, directory };
123
+ }
124
+ /**
125
+ * Download and extract a GitHub repository zipball to a temp directory.
126
+ * Returns the path to the extracted content root.
127
+ */
128
+ async function downloadAndExtract(parsed, apiClient, logger) {
129
+ const { owner, repo, branch } = parsed;
130
+ const zipUrl = `https://api.github.com/repos/${owner}/${repo}/zipball/${branch}`;
131
+ logger.debug('[remote-import] Downloading zipball from: %s', zipUrl);
132
+ const tempDir = mkdtempSync(join(tmpdir(), 'agentuity-remote-'));
133
+ try {
134
+ const zipPath = join(tempDir, 'download.zip');
135
+ // Download the zipball
136
+ const headers = await githubHeaders(apiClient);
137
+ await tui.spinner({
138
+ message: `Downloading ${owner}/${repo}...`,
139
+ clearOnSuccess: true,
140
+ callback: async () => {
141
+ const resp = await fetch(zipUrl, {
142
+ headers,
143
+ redirect: 'follow',
144
+ });
145
+ if (!resp.ok) {
146
+ throw new RemoteImportDownloadError({
147
+ message: `Failed to download from GitHub: ${resp.status} ${resp.statusText}`,
148
+ });
149
+ }
150
+ const buffer = Buffer.from(await resp.arrayBuffer());
151
+ await Bun.write(zipPath, buffer);
152
+ logger.debug('[remote-import] Downloaded %d bytes to %s', buffer.length, zipPath);
153
+ return resp;
154
+ },
155
+ });
156
+ // Extract the zip
157
+ const extractDir = join(tempDir, 'extracted');
158
+ mkdirSync(extractDir, { recursive: true });
159
+ await tui.spinner({
160
+ message: 'Extracting template...',
161
+ clearOnSuccess: true,
162
+ callback: async () => {
163
+ // Use Bun's built-in unzip via subprocess
164
+ const proc = Bun.spawnSync(['unzip', '-q', '-o', zipPath, '-d', extractDir], {
165
+ stdout: 'pipe',
166
+ stderr: 'pipe',
167
+ });
168
+ if (proc.exitCode !== 0) {
169
+ const stderr = proc.stderr.toString();
170
+ throw new RemoteImportExtractError({
171
+ message: `Failed to extract zip: ${stderr}`,
172
+ });
173
+ }
174
+ logger.debug('[remote-import] Extracted to %s', extractDir);
175
+ },
176
+ });
177
+ // GitHub zipball creates a top-level directory like "owner-repo-sha/"
178
+ // We need to find it and return its path
179
+ const entries = readdirSync(extractDir);
180
+ if (entries.length === 1 && entries[0]) {
181
+ const innerDir = join(extractDir, entries[0]);
182
+ return { extractDir: innerDir, tempDir };
183
+ }
184
+ return { extractDir, tempDir };
185
+ }
186
+ catch (err) {
187
+ // Clean up temp directory on failure to prevent leaks
188
+ try {
189
+ rmSync(tempDir, { recursive: true, force: true });
190
+ }
191
+ catch {
192
+ // Ignore cleanup errors
193
+ }
194
+ throw err;
195
+ }
196
+ }
197
+ /**
198
+ * Look for agentuity.yaml in the extracted content and parse it.
199
+ * Returns the parsed content or null if not found.
200
+ */
201
+ async function findAgentuityYaml(dir, logger) {
202
+ const yamlPath = join(dir, 'agentuity.yaml');
203
+ const file = Bun.file(yamlPath);
204
+ if (!(await file.exists())) {
205
+ logger.debug('[remote-import] No agentuity.yaml found at %s', yamlPath);
206
+ return null;
207
+ }
208
+ try {
209
+ const { YAML } = await import('bun');
210
+ const content = await file.text();
211
+ const parsed = YAML.parse(content);
212
+ logger.debug('[remote-import] Parsed agentuity.yaml: %o', parsed);
213
+ return parsed;
214
+ }
215
+ catch (err) {
216
+ logger.debug('[remote-import] Failed to parse agentuity.yaml: %o', err);
217
+ return null;
218
+ }
219
+ }
220
+ /**
221
+ * Create a project via the API in non-interactive mode using the provided name.
222
+ */
223
+ async function createProjectNonInteractive(apiClient, config, logger, name, region, orgOverride) {
224
+ // Fetch orgs — use the first one in non-interactive mode
225
+ const orgs = await listOrganizations(apiClient);
226
+ if (orgs.length === 0) {
227
+ throw new RemoteImportNoOrganizationError();
228
+ }
229
+ const firstOrg = orgs[0];
230
+ if (!firstOrg) {
231
+ throw new RemoteImportNoOrganizationError();
232
+ }
233
+ const orgId = orgOverride ?? config.preferences?.orgId ?? firstOrg.id;
234
+ // Determine region
235
+ let selectedRegion = region;
236
+ if (!selectedRegion) {
237
+ selectedRegion = process.env.AGENTUITY_REGION ?? config.preferences?.region;
238
+ }
239
+ if (!selectedRegion) {
240
+ const regions = await fetchRegionsWithCache(config.name, apiClient, logger);
241
+ const firstRegion = regions[0];
242
+ if (!firstRegion) {
243
+ throw new RemoteImportNoRegionError();
244
+ }
245
+ selectedRegion = firstRegion.region;
246
+ }
247
+ const newProject = await tui.spinner({
248
+ message: 'Creating project...',
249
+ clearOnSuccess: true,
250
+ callback: async () => {
251
+ return projectCreate(apiClient, {
252
+ name,
253
+ orgId,
254
+ cloudRegion: selectedRegion,
255
+ });
256
+ },
257
+ });
258
+ return {
259
+ id: newProject.id,
260
+ sdkKey: newProject.sdkKey,
261
+ orgId,
262
+ region: selectedRegion,
263
+ };
264
+ }
265
+ /**
266
+ * Create a project interactively — select org, region, name via TUI prompts.
267
+ */
268
+ async function createProjectInteractive(apiClient, config, logger, defaultName) {
269
+ // Fetch orgs
270
+ const orgs = await tui.spinner({
271
+ message: 'Fetching organizations...',
272
+ clearOnSuccess: true,
273
+ callback: () => listOrganizations(apiClient),
274
+ });
275
+ if (orgs.length === 0) {
276
+ throw new RemoteImportNoOrganizationError();
277
+ }
278
+ // Select org
279
+ const orgId = await tui.selectOrganization(orgs, config.preferences?.orgId);
280
+ // Fetch and select region
281
+ const regions = await tui.spinner({
282
+ message: 'Fetching regions...',
283
+ clearOnSuccess: true,
284
+ callback: () => fetchRegionsWithCache(config.name, apiClient, logger),
285
+ });
286
+ let selectedRegion;
287
+ if (regions.length === 1 && regions[0]) {
288
+ selectedRegion = regions[0].region;
289
+ }
290
+ else {
291
+ const prompt = tui.createPrompt();
292
+ const options = regions.map((r) => ({
293
+ value: r.region,
294
+ label: `${r.description} (${r.region})`,
295
+ }));
296
+ const firstOption = options[0];
297
+ selectedRegion = await prompt.select({
298
+ message: 'Select a region:',
299
+ options,
300
+ initial: firstOption?.value ?? '',
301
+ });
302
+ }
303
+ // Get project name
304
+ const prompt = tui.createPrompt();
305
+ const projectName = await prompt.text({
306
+ message: 'Project name:',
307
+ initial: defaultName,
308
+ validate: (value) => {
309
+ if (!value || value.trim().length === 0) {
310
+ return 'Project name is required';
311
+ }
312
+ return true;
313
+ },
314
+ });
315
+ // Create the project
316
+ const newProject = await tui.spinner({
317
+ message: 'Registering project...',
318
+ clearOnSuccess: true,
319
+ callback: async () => {
320
+ return projectCreate(apiClient, {
321
+ name: projectName,
322
+ orgId,
323
+ cloudRegion: selectedRegion,
324
+ });
325
+ },
326
+ });
327
+ return {
328
+ id: newProject.id,
329
+ sdkKey: newProject.sdkKey,
330
+ orgId,
331
+ region: selectedRegion,
332
+ };
333
+ }
334
+ /**
335
+ * Parse a repo URL or "owner/name" string into owner and name components.
336
+ * Supports:
337
+ * - https://github.com/owner/repo
338
+ * - https://github.com/owner/repo.git
339
+ * - owner/repo
340
+ */
341
+ function parseRepoTarget(repo) {
342
+ // Try as a URL first
343
+ try {
344
+ const parsed = new URL(repo);
345
+ if (parsed.hostname === 'github.com') {
346
+ const parts = parsed.pathname.replace(/^\//, '').replace(/\/$/, '').split('/');
347
+ if (parts.length >= 2 && parts[0] && parts[1]) {
348
+ return { owner: parts[0], name: parts[1].replace(/\.git$/, '') };
349
+ }
350
+ }
351
+ }
352
+ catch {
353
+ // Not a URL — try "owner/name" format
354
+ }
355
+ const parts = repo.split('/');
356
+ if (parts.length === 2 && parts[0] && parts[1]) {
357
+ return { owner: parts[0], name: parts[1].replace(/\.git$/, '') };
358
+ }
359
+ throw new RemoteImportInvalidRepoError({
360
+ message: `Invalid repo target: "${repo}". Expected a GitHub URL (https://github.com/owner/repo) or owner/repo format.`,
361
+ });
362
+ }
363
+ /**
364
+ * Create a GitHub repo via the API. Preflight check already verified it doesn't exist.
365
+ * Returns the HTTPS URL of the repo.
366
+ */
367
+ async function createGithubRepoForImport(apiClient, repo, _logger) {
368
+ const { owner, name } = parseRepoTarget(repo);
369
+ const createResult = await tui.spinner({
370
+ message: `Creating repository ${owner}/${name}...`,
371
+ clearOnSuccess: true,
372
+ callback: () => createGithubRepo(apiClient, {
373
+ name,
374
+ owner,
375
+ private: true,
376
+ }),
377
+ });
378
+ tui.success(`Created repository ${createResult.fullName}`);
379
+ return createResult.url;
380
+ }
381
+ /**
382
+ * Push the working directory to a remote git repository.
383
+ */
384
+ async function pushToRepo(dest, repoUrl, apiClient, logger) {
385
+ const gitAvailable = await isGitAvailable();
386
+ if (!gitAvailable) {
387
+ tui.warning('Git is not available — skipping git push.');
388
+ return;
389
+ }
390
+ const defaultBranch = (await getDefaultBranch()) || 'main';
391
+ // Get GitHub token from Agentuity API (uses stored OAuth token)
392
+ let remoteUrl = repoUrl;
393
+ try {
394
+ const { token } = await getGithubToken(apiClient);
395
+ const parsed = new URL(repoUrl);
396
+ if (parsed.hostname === 'github.com') {
397
+ remoteUrl = `https://x-access-token:${token}@github.com${parsed.pathname}`;
398
+ if (!remoteUrl.endsWith('.git')) {
399
+ remoteUrl += '.git';
400
+ }
401
+ }
402
+ }
403
+ catch (err) {
404
+ logger.debug('[remote-import] Could not get GitHub token from API, trying without auth: %o', err);
405
+ // Fall through — push will likely fail for private repos but may work for public
406
+ }
407
+ await tui.spinner({
408
+ message: 'Pushing to remote repository...',
409
+ clearOnSuccess: true,
410
+ callback: async () => {
411
+ // Add remote origin
412
+ const addRemote = Bun.spawnSync(['git', 'remote', 'add', 'origin', remoteUrl], {
413
+ cwd: dest,
414
+ stdout: 'pipe',
415
+ stderr: 'pipe',
416
+ });
417
+ if (addRemote.exitCode !== 0) {
418
+ // Remote might already exist, try set-url instead
419
+ const setUrl = Bun.spawnSync(['git', 'remote', 'set-url', 'origin', remoteUrl], {
420
+ cwd: dest,
421
+ stdout: 'pipe',
422
+ stderr: 'pipe',
423
+ });
424
+ if (setUrl.exitCode !== 0) {
425
+ throw new RemoteImportGitError({
426
+ message: `Failed to set git remote: ${sanitizeTokens(setUrl.stderr.toString())}`,
427
+ });
428
+ }
429
+ }
430
+ // Push to remote
431
+ const push = Bun.spawnSync(['git', 'push', '-u', 'origin', defaultBranch], {
432
+ cwd: dest,
433
+ stdout: 'pipe',
434
+ stderr: 'pipe',
435
+ });
436
+ if (push.exitCode !== 0) {
437
+ throw new RemoteImportGitError({
438
+ message: `Failed to push to remote: ${sanitizeTokens(push.stderr.toString())}`,
439
+ });
440
+ }
441
+ logger.debug('[remote-import] Pushed to %s on branch %s', repoUrl, defaultBranch);
442
+ },
443
+ });
444
+ tui.success(`Pushed to ${repoUrl}`);
445
+ }
446
+ /**
447
+ * Run the deploy command as a subprocess.
448
+ *
449
+ * Uses `bunx agentuity deploy …` to match the fork-wrapper pattern used
450
+ * elsewhere in the CLI (see deploy-fork.ts). This avoids relying on
451
+ * `agentuity` being independently available on PATH.
452
+ */
453
+ async function runDeploy(dest, logger) {
454
+ tui.info('Deploying project...');
455
+ const args = ['bunx', 'agentuity', 'deploy', '--trigger', 'cli', '--event', 'manual'];
456
+ logger.debug('[remote-import] Running deploy: %s', args.join(' '));
457
+ const proc = Bun.spawn(args, {
458
+ cwd: dest,
459
+ stdout: 'inherit',
460
+ stderr: 'inherit',
461
+ env: {
462
+ ...process.env,
463
+ },
464
+ });
465
+ const exitCode = await proc.exited;
466
+ if (exitCode !== 0) {
467
+ throw new RemoteImportDeployError({
468
+ message: `Deploy failed with exit code ${exitCode}`,
469
+ });
470
+ }
471
+ }
472
+ /**
473
+ * Run the remote import flow: download from GitHub, set up project, optionally push and deploy.
474
+ */
475
+ export async function runRemoteImport(options) {
476
+ const { url, deploy, projectId, repo, name, env, org, region: optRegion, apiClient, auth, config, logger, } = options;
477
+ // Safety check: refuse to run inside an existing git repo
478
+ try {
479
+ const result = Bun.spawnSync(['git', 'rev-parse', '--is-inside-work-tree'], {
480
+ cwd: process.cwd(),
481
+ stdout: 'pipe',
482
+ stderr: 'pipe',
483
+ });
484
+ if (result.exitCode === 0 && result.stdout.toString().trim() === 'true') {
485
+ tui.fatal('Cannot run remote import inside an existing git repository. Please run from an empty directory.');
486
+ }
487
+ }
488
+ catch {
489
+ // git not found or command failed — not inside a repo, which is fine
490
+ }
491
+ // 1. Parse GitHub URL (async — may query GitHub API for default branch)
492
+ const parsed = await parseGitHubUrl(url, apiClient);
493
+ logger.debug('[remote-import] Parsed URL: owner=%s repo=%s branch=%s dir=%s', parsed.owner, parsed.repo, parsed.branch, parsed.directory ?? '(root)');
494
+ // ── Preflight checks (all before any mutations) ──
495
+ // Check: target directory doesn't already exist
496
+ const projectDirName = name ?? parsed.repo;
497
+ const dest = join(process.cwd(), projectDirName);
498
+ if (existsSync(dest)) {
499
+ tui.fatal(`Directory "${projectDirName}" already exists. Choose a different name with --name.`);
500
+ }
501
+ // Check: target GitHub repo doesn't already exist
502
+ if (repo) {
503
+ const { owner: repoOwner, name: repoName } = parseRepoTarget(repo);
504
+ const checkResult = await tui.spinner({
505
+ message: `Checking repository ${repoOwner}/${repoName}...`,
506
+ clearOnSuccess: true,
507
+ callback: () => checkGithubRepo(apiClient, { owner: repoOwner, name: repoName }),
508
+ });
509
+ if (checkResult.exists) {
510
+ tui.fatal(`Repository ${repoOwner}/${repoName} already exists. Use a different name or delete the existing repo first.`);
511
+ }
512
+ }
513
+ // ── All checks passed — start doing work ──
514
+ // 2. Download and extract template source
515
+ let tempDir;
516
+ let sourceDir;
517
+ try {
518
+ const result = await downloadAndExtract(parsed, apiClient, logger);
519
+ tempDir = result.tempDir;
520
+ sourceDir = result.extractDir;
521
+ // If a specific directory was specified in the URL, navigate into it
522
+ if (parsed.directory) {
523
+ const subDir = join(sourceDir, parsed.directory);
524
+ if (!existsSync(subDir)) {
525
+ throw new RemoteImportDirectoryNotFoundError({
526
+ message: `Directory "${parsed.directory}" not found in the repository.`,
527
+ });
528
+ }
529
+ sourceDir = subDir;
530
+ }
531
+ // 3. Find and parse agentuity.yaml (informational, for future use)
532
+ const yamlConfig = await findAgentuityYaml(sourceDir, logger);
533
+ if (yamlConfig) {
534
+ tui.info('Found agentuity.yaml in template.');
535
+ }
536
+ // 4. Project setup
537
+ let projectInfo;
538
+ if (projectId) {
539
+ // --project-id was provided: skip creation, just write config
540
+ const sdkKey = process.env.AGENTUITY_SDK_KEY;
541
+ if (!sdkKey) {
542
+ throw new RemoteImportConfigError({
543
+ message: 'AGENTUITY_SDK_KEY environment variable is required when using --project-id',
544
+ });
545
+ }
546
+ const orgId = org ?? config.preferences?.orgId;
547
+ if (!orgId) {
548
+ throw new RemoteImportConfigError({
549
+ message: 'Organization ID not found. Use --org flag, set orgId in config preferences, or use interactive mode.',
550
+ });
551
+ }
552
+ const region = process.env.AGENTUITY_REGION ?? config.preferences?.region ?? 'usc';
553
+ projectInfo = { id: projectId, sdkKey, orgId, region };
554
+ tui.info(`Using pre-created project: ${projectId}`);
555
+ }
556
+ else if (name) {
557
+ // --name provided: create non-interactively (headless-friendly)
558
+ projectInfo = await createProjectNonInteractive(apiClient, config, logger, name, optRegion, org);
559
+ }
560
+ else if (isTTY()) {
561
+ // Interactive mode: prompt for org/region/name
562
+ projectInfo = await createProjectInteractive(apiClient, config, logger, parsed.repo);
563
+ }
564
+ else {
565
+ // Non-interactive without --name: use repo name
566
+ projectInfo = await createProjectNonInteractive(apiClient, config, logger, parsed.repo, optRegion, org);
567
+ }
568
+ // Parse .env.example to detect required env vars and resources
569
+ // Platform-managed vars that should not appear in requirements
570
+ const platformManagedVars = new Set([
571
+ 'AGENTUITY_SDK_KEY',
572
+ 'AGENTUITY_URL',
573
+ 'AGENTUITY_TRANSPORT_URL',
574
+ 'AGENTUITY_BEARER_TOKEN',
575
+ 'NODE_ENV',
576
+ ]);
577
+ let template;
578
+ const envExamplePath = join(sourceDir, '.env.example');
579
+ if (await Bun.file(envExamplePath).exists()) {
580
+ try {
581
+ const envContent = await Bun.file(envExamplePath).text();
582
+ const envFields = parseEnvExample(envContent).filter((f) => !platformManagedVars.has(f.key));
583
+ const resources = envFields
584
+ .filter((f) => f.resource)
585
+ .map((f) => ({
586
+ type: f.resource,
587
+ envVar: f.key,
588
+ description: f.comment,
589
+ }));
590
+ const envVars = envFields
591
+ .filter((f) => !f.resource)
592
+ .map((f) => ({
593
+ key: f.key,
594
+ required: f.required ?? false,
595
+ description: f.comment,
596
+ }));
597
+ // Merge curated metadata from the source template's existing agentuity.json
598
+ const existingConfigPath = join(sourceDir, 'agentuity.json');
599
+ if (await Bun.file(existingConfigPath).exists()) {
600
+ try {
601
+ const existingConfig = JSON.parse(await Bun.file(existingConfigPath).text());
602
+ const existingResources = existingConfig?.template?.requirements?.resources;
603
+ if (Array.isArray(existingResources)) {
604
+ // Merge extra fields (like defaultName) from curated config
605
+ for (const resource of resources) {
606
+ const curated = existingResources.find((r) => r.envVar === resource.envVar);
607
+ if (curated?.defaultName) {
608
+ resource.defaultName = curated.defaultName;
609
+ }
610
+ if (curated?.queueType) {
611
+ resource.queueType = curated.queueType;
612
+ }
613
+ }
614
+ // Preserve curated resources NOT detected by parser
615
+ for (const curated of existingResources) {
616
+ if (!resources.some((r) => r.envVar === curated.envVar)) {
617
+ resources.push(curated);
618
+ }
619
+ }
620
+ }
621
+ // Merge curated env vars not detected by parser
622
+ const existingEnv = existingConfig?.template?.requirements?.env;
623
+ if (Array.isArray(existingEnv)) {
624
+ for (const curated of existingEnv) {
625
+ if (!envVars.some((e) => e.key === curated.key)) {
626
+ envVars.push(curated);
627
+ }
628
+ }
629
+ }
630
+ }
631
+ catch {
632
+ // Ignore parse errors — source config may be malformed
633
+ }
634
+ }
635
+ if (resources.length > 0 || envVars.length > 0) {
636
+ template = {
637
+ source: `github.com/${parsed.owner}/${parsed.repo}`,
638
+ requirements: {
639
+ resources: resources.length > 0 ? resources : undefined,
640
+ env: envVars.length > 0 ? envVars : undefined,
641
+ },
642
+ };
643
+ for (const r of resources) {
644
+ tui.info(`Requires ${r.type}: ${r.envVar}${r.description ? ` (${r.description})` : ''}`);
645
+ }
646
+ const requiredEnv = envVars.filter((f) => f.required);
647
+ for (const f of requiredEnv) {
648
+ tui.info(`Required env var: ${f.key}${f.description ? ` (${f.description})` : ''}`);
649
+ }
650
+ }
651
+ }
652
+ catch (err) {
653
+ logger.debug('[remote-import] Could not parse .env.example: %o', err);
654
+ }
655
+ }
656
+ // If no .env.example but we know the source, still track it
657
+ if (!template && parsed.owner && parsed.repo) {
658
+ template = { source: `github.com/${parsed.owner}/${parsed.repo}` };
659
+ }
660
+ // ─── Resource Provisioning ───
661
+ // Parse --env flags into a map
662
+ const envOverrides = new Map();
663
+ for (const e of env ?? []) {
664
+ const colonIdx = e.indexOf(':');
665
+ if (colonIdx > 0) {
666
+ envOverrides.set(e.slice(0, colonIdx), e.slice(colonIdx + 1));
667
+ }
668
+ }
669
+ const resourceEnvVars = {};
670
+ const interactive = isTTY();
671
+ const templateResources = template?.requirements?.resources ?? [];
672
+ const templateEnvVars = template?.requirements?.env ?? [];
673
+ // Check if we can provision (need auth + org + region)
674
+ const orgId = projectInfo.orgId;
675
+ const region = projectInfo.region;
676
+ const canProvision = !!orgId && !!region;
677
+ if (canProvision && (templateResources.length > 0 || templateEnvVars.length > 0)) {
678
+ // ── Preflight validation: check all required items before creating anything ──
679
+ if (!interactive) {
680
+ const missing = [];
681
+ for (const r of templateResources) {
682
+ if (!envOverrides.has(r.envVar)) {
683
+ missing.push(`--env ${r.envVar}:<${r.type}-name>`);
684
+ }
685
+ }
686
+ for (const e of templateEnvVars) {
687
+ if (e.required && !envOverrides.has(e.key)) {
688
+ missing.push(`--env ${e.key}:<value>`);
689
+ }
690
+ }
691
+ if (missing.length > 0) {
692
+ for (const m of missing) {
693
+ tui.error(`Missing: ${m}`);
694
+ }
695
+ tui.fatal('Provide all required --env flags for non-interactive mode.');
696
+ }
697
+ // Validate database names upfront
698
+ for (const r of templateResources.filter((res) => res.type === 'database')) {
699
+ const name = envOverrides.get(r.envVar);
700
+ const validation = validateDatabaseName(name);
701
+ if (!validation.valid) {
702
+ tui.fatal(`Invalid database name "${name}" for ${r.envVar}: ${validation.error}`);
703
+ }
704
+ }
705
+ }
706
+ const catalystClient = getCatalystAPIClient(logger, auth, region, undefined, config);
707
+ // ── Database Resources ──
708
+ for (const r of templateResources.filter((resource) => resource.type === 'database')) {
709
+ const overrideName = envOverrides.get(r.envVar);
710
+ if (overrideName) {
711
+ // Non-interactive path: create DB with the given name
712
+ try {
713
+ const validation = validateDatabaseName(overrideName);
714
+ if (!validation.valid) {
715
+ throw new RemoteImportConfigError({
716
+ message: `Invalid database name "${overrideName}": ${validation.error}`,
717
+ });
718
+ }
719
+ const created = await tui.spinner({
720
+ message: `Creating database "${overrideName}"`,
721
+ clearOnSuccess: true,
722
+ callback: () => createResources(catalystClient, orgId, region, [
723
+ { type: 'db', name: overrideName, description: r.description },
724
+ ]),
725
+ });
726
+ if (created[0]?.env) {
727
+ // Map using the template-defined envVar name
728
+ const connStr = created[0].env.DATABASE_URL ?? Object.values(created[0].env)[0];
729
+ if (connStr)
730
+ resourceEnvVars[r.envVar] = connStr;
731
+ }
732
+ tui.success(`Created database: ${overrideName}`);
733
+ }
734
+ catch (err) {
735
+ const msg = err instanceof Error ? err.message : String(err);
736
+ if (!interactive) {
737
+ throw new RemoteImportConfigError({
738
+ message: `Failed to create database "${overrideName}": ${msg}`,
739
+ });
740
+ }
741
+ tui.error(`Failed to create database "${overrideName}": ${msg}`);
742
+ // Fall through to interactive prompt below
743
+ }
744
+ }
745
+ // Interactive fallback (no --env or --env failed)
746
+ if (!resourceEnvVars[r.envVar] && interactive) {
747
+ const prompt = createPrompt();
748
+ let existingDbs;
749
+ try {
750
+ existingDbs = await tui.spinner({
751
+ message: 'Fetching existing databases',
752
+ clearOnSuccess: true,
753
+ callback: () => listResources(catalystClient, orgId, region),
754
+ });
755
+ }
756
+ catch {
757
+ // Ignore — just won't show existing options
758
+ }
759
+ let dbCreated = false;
760
+ while (!dbCreated) {
761
+ const action = await prompt.select({
762
+ message: `${r.description || r.envVar} requires a database`,
763
+ options: [
764
+ { value: 'skip', label: 'Skip — set up later' },
765
+ { value: 'create', label: 'Create a new database' },
766
+ ...(existingDbs?.db ?? []).map((db) => ({
767
+ value: `existing:${db.name}`,
768
+ label: `Use existing: ${tui.tuiColors.primary(db.name)}`,
769
+ })),
770
+ ],
771
+ });
772
+ if (action === 'skip')
773
+ break;
774
+ if (action === 'create') {
775
+ const dbName = await prompt.text({
776
+ message: 'Database name',
777
+ hint: 'Lowercase letters, digits, underscores only',
778
+ initial: r.defaultName,
779
+ validate: (value) => {
780
+ const trimmed = value.trim();
781
+ if (trimmed === '')
782
+ return 'Name is required';
783
+ const result = validateDatabaseName(trimmed);
784
+ return result.valid ? true : result.error;
785
+ },
786
+ });
787
+ try {
788
+ const created = await tui.spinner({
789
+ message: `Creating database "${dbName}"`,
790
+ clearOnSuccess: true,
791
+ callback: () => createResources(catalystClient, orgId, region, [
792
+ { type: 'db', name: dbName.trim(), description: r.description },
793
+ ]),
794
+ });
795
+ if (created[0]?.env) {
796
+ const connStr = created[0].env.DATABASE_URL ?? Object.values(created[0].env)[0];
797
+ if (connStr)
798
+ resourceEnvVars[r.envVar] = connStr;
799
+ }
800
+ tui.success(`Created database: ${dbName}`);
801
+ dbCreated = true;
802
+ }
803
+ catch (err) {
804
+ tui.error(`Failed to create database: ${err instanceof Error ? err.message : String(err)}`);
805
+ // Loop back to prompt
806
+ }
807
+ }
808
+ else if (action.startsWith('existing:')) {
809
+ const selectedName = action.slice('existing:'.length);
810
+ const selectedDb = existingDbs?.db.find((d) => d.name === selectedName);
811
+ if (selectedDb?.env) {
812
+ const connStr = selectedDb.env.DATABASE_URL ?? Object.values(selectedDb.env)[0];
813
+ if (connStr)
814
+ resourceEnvVars[r.envVar] = connStr;
815
+ }
816
+ dbCreated = true;
817
+ }
818
+ }
819
+ }
820
+ if (!resourceEnvVars[r.envVar] && !interactive) {
821
+ throw new RemoteImportConfigError({
822
+ message: `Missing required database for ${r.envVar}. Pass --env ${r.envVar}:<db-name> to provision.`,
823
+ });
824
+ }
825
+ }
826
+ // ── Queue Resources ──
827
+ const queueClient = await getGlobalCatalystAPIClient(logger, auth, config?.name, undefined, config);
828
+ const queueOrgOpts = orgId ? { orgId } : undefined;
829
+ for (const r of templateResources.filter((resource) => resource.type === 'queue')) {
830
+ if (!r.queueType) {
831
+ logger.debug('[remote-import] Queue resource %s missing queueType, skipping', r.envVar);
832
+ continue;
833
+ }
834
+ const overrideName = envOverrides.get(r.envVar);
835
+ if (overrideName) {
836
+ // Non-interactive path: create queue with given name
837
+ try {
838
+ const queue = await tui.spinner({
839
+ message: `Creating ${r.queueType} queue "${overrideName}"`,
840
+ clearOnSuccess: true,
841
+ callback: () => createQueue(queueClient, {
842
+ name: overrideName,
843
+ queue_type: r.queueType,
844
+ description: r.description,
845
+ }, queueOrgOpts),
846
+ });
847
+ resourceEnvVars[r.envVar] = queue.name;
848
+ tui.success(`Created queue: ${queue.name}`);
849
+ }
850
+ catch (err) {
851
+ const msg = err instanceof Error ? err.message : String(err);
852
+ if (!interactive) {
853
+ throw new RemoteImportConfigError({
854
+ message: `Failed to create queue "${overrideName}": ${msg}`,
855
+ });
856
+ }
857
+ tui.error(`Failed to create queue "${overrideName}": ${msg}`);
858
+ }
859
+ }
860
+ // Interactive fallback
861
+ if (!resourceEnvVars[r.envVar] && interactive) {
862
+ const prompt = createPrompt();
863
+ let existingQueues;
864
+ try {
865
+ existingQueues = await tui.spinner({
866
+ message: 'Fetching existing queues',
867
+ clearOnSuccess: true,
868
+ callback: () => listQueues(queueClient, {}, queueOrgOpts),
869
+ });
870
+ }
871
+ catch {
872
+ // Ignore
873
+ }
874
+ let queueCreated = false;
875
+ while (!queueCreated) {
876
+ const action = await prompt.select({
877
+ message: `${r.description || r.envVar} requires a ${r.queueType} queue`,
878
+ options: [
879
+ { value: 'skip', label: 'Skip — set up later' },
880
+ { value: 'create', label: `Create a new ${r.queueType} queue` },
881
+ ...(existingQueues?.queues ?? []).map((q) => ({
882
+ value: `existing:${q.name}`,
883
+ label: `Use existing: ${tui.tuiColors.primary(q.name)}`,
884
+ })),
885
+ ],
886
+ });
887
+ if (action === 'skip')
888
+ break;
889
+ if (action === 'create') {
890
+ const queueName = await prompt.text({
891
+ message: 'Queue name',
892
+ hint: 'Optional — auto-generated if empty',
893
+ initial: r.defaultName,
894
+ });
895
+ try {
896
+ const queue = await tui.spinner({
897
+ message: `Creating ${r.queueType} queue "${queueName || '(auto)'}"`,
898
+ clearOnSuccess: true,
899
+ callback: () => createQueue(queueClient, {
900
+ name: queueName.trim() || undefined,
901
+ queue_type: r.queueType,
902
+ description: r.description,
903
+ }, queueOrgOpts),
904
+ });
905
+ resourceEnvVars[r.envVar] = queue.name;
906
+ tui.success(`Created queue: ${queue.name}`);
907
+ queueCreated = true;
908
+ }
909
+ catch (err) {
910
+ tui.error(`Failed to create queue: ${err instanceof Error ? err.message : String(err)}`);
911
+ }
912
+ }
913
+ else if (action.startsWith('existing:')) {
914
+ const selectedName = action.slice('existing:'.length);
915
+ resourceEnvVars[r.envVar] = selectedName;
916
+ queueCreated = true;
917
+ }
918
+ }
919
+ }
920
+ if (!resourceEnvVars[r.envVar] && !interactive) {
921
+ throw new RemoteImportConfigError({
922
+ message: `Missing required queue for ${r.envVar}. Pass --env ${r.envVar}:<queue-name> to provision.`,
923
+ });
924
+ }
925
+ }
926
+ // ── Plain Env Vars ──
927
+ for (const e of templateEnvVars) {
928
+ if (envOverrides.has(e.key)) {
929
+ resourceEnvVars[e.key] = envOverrides.get(e.key);
930
+ }
931
+ else if (e.required) {
932
+ if (interactive) {
933
+ const prompt = createPrompt();
934
+ const val = await prompt.text({
935
+ message: `Enter value for ${e.key}${e.description ? ` (${e.description})` : ''}`,
936
+ });
937
+ if (val.trim()) {
938
+ resourceEnvVars[e.key] = val.trim();
939
+ }
940
+ }
941
+ else {
942
+ throw new RemoteImportConfigError({
943
+ message: `Missing required env var ${e.key}. Pass --env ${e.key}:<value> to set it.`,
944
+ });
945
+ }
946
+ }
947
+ }
948
+ // Write all collected env vars to .env
949
+ if (Object.keys(resourceEnvVars).length > 0) {
950
+ await addResourceEnvVars(sourceDir, resourceEnvVars);
951
+ tui.success(`Configured ${Object.keys(resourceEnvVars).length} environment variable(s)`);
952
+ }
953
+ }
954
+ // Write agentuity.json and .env to sourceDir so git commit includes them
955
+ await createProjectConfig(sourceDir, {
956
+ projectId: projectInfo.id,
957
+ orgId: projectInfo.orgId,
958
+ sdkKey: projectInfo.sdkKey,
959
+ region: projectInfo.region,
960
+ template,
961
+ });
962
+ tui.success('Created agentuity.json');
963
+ // Ensure .env is gitignored before any git operations (prevents secret leak)
964
+ const gitignorePath = join(sourceDir, '.gitignore');
965
+ const gitignoreFile = Bun.file(gitignorePath);
966
+ if (await gitignoreFile.exists()) {
967
+ const gitignoreContent = await gitignoreFile.text();
968
+ const lines = gitignoreContent.split('\n').map((l) => l.trim());
969
+ if (!lines.includes('.env')) {
970
+ await Bun.write(gitignorePath, `${gitignoreContent.trimEnd()}\n.env\n`);
971
+ }
972
+ }
973
+ else {
974
+ await Bun.write(gitignorePath, '.env\n.env.*\n');
975
+ }
976
+ // Update package.json name to match the project name
977
+ const pkgJsonPath = join(sourceDir, 'package.json');
978
+ if (await Bun.file(pkgJsonPath).exists()) {
979
+ try {
980
+ const pkgRaw = await Bun.file(pkgJsonPath).text();
981
+ const pkg = JSON.parse(pkgRaw);
982
+ pkg.name = projectDirName;
983
+ await Bun.write(pkgJsonPath, JSON.stringify(pkg, null, 2) + '\n');
984
+ logger.debug('[remote-import] Updated package.json name to %s', projectDirName);
985
+ }
986
+ catch (err) {
987
+ logger.debug('[remote-import] Could not update package.json name: %o', err);
988
+ }
989
+ }
990
+ // Fetch GitHub App bot identity for commit authorship
991
+ let botAuthor;
992
+ try {
993
+ botAuthor = await getGithubBotIdentity(apiClient);
994
+ }
995
+ catch {
996
+ logger.debug('[remote-import] Could not fetch bot identity, using fallback');
997
+ }
998
+ // 5. Git init + push (if --repo flag provided) — in sourceDir, not CWD
999
+ if (repo) {
1000
+ // Create the repo (we already verified it doesn't exist in preflight)
1001
+ const repoUrl = await createGithubRepoForImport(apiClient, repo, logger);
1002
+ // Initialize git repo in sourceDir (handles init + first commit)
1003
+ await initGitRepo(sourceDir, {
1004
+ projectName: projectDirName,
1005
+ source: `github.com/${parsed.owner}/${parsed.repo}`,
1006
+ author: botAuthor,
1007
+ });
1008
+ // Push to remote from sourceDir
1009
+ await pushToRepo(sourceDir, repoUrl, apiClient, logger);
1010
+ tui.success(`GitHub repo: ${repoUrl}`);
1011
+ // Link the repo to the Agentuity project (auto-deploy + preview disabled until first deploy completes)
1012
+ try {
1013
+ const { owner: linkOwner, name: linkName } = parseRepoTarget(repo);
1014
+ const pushedBranch = (await getDefaultBranch()) || 'main';
1015
+ await linkProjectToRepo(apiClient, {
1016
+ projectId: projectInfo.id,
1017
+ repoFullName: `${linkOwner}/${linkName}`,
1018
+ branch: pushedBranch,
1019
+ autoDeploy: false,
1020
+ previewDeploy: false,
1021
+ directory: parsed.directory,
1022
+ });
1023
+ tui.success('Linked repo to project');
1024
+ }
1025
+ catch (err) {
1026
+ logger.debug('[remote-import] Failed to link repo to project: %o', err);
1027
+ tui.warning('Could not link repo to project — you can link manually with `agentuity link`');
1028
+ }
1029
+ }
1030
+ // 6. Copy extracted content into project folder (already validated in preflight)
1031
+ await tui.spinner({
1032
+ message: 'Copying project files...',
1033
+ clearOnSuccess: true,
1034
+ callback: async () => {
1035
+ mkdirSync(dest, { recursive: true });
1036
+ const entries = readdirSync(sourceDir);
1037
+ for (const entry of entries) {
1038
+ cpSync(join(sourceDir, entry), join(dest, entry), {
1039
+ recursive: true,
1040
+ });
1041
+ }
1042
+ },
1043
+ });
1044
+ // Reset git remote to clean URL (pushToRepo may have embedded a token)
1045
+ if (repo) {
1046
+ const { owner, name: repoName } = parseRepoTarget(repo);
1047
+ const cleanUrl = `https://github.com/${owner}/${repoName}.git`;
1048
+ Bun.spawnSync(['git', 'remote', 'set-url', 'origin', cleanUrl], {
1049
+ cwd: dest,
1050
+ stdout: 'pipe',
1051
+ stderr: 'pipe',
1052
+ });
1053
+ }
1054
+ tui.success(`Project created in ./${projectDirName}`);
1055
+ // 7. Deploy (if --deploy flag)
1056
+ if (deploy) {
1057
+ await runDeploy(dest, logger);
1058
+ }
1059
+ tui.success('Remote import completed successfully!');
1060
+ }
1061
+ finally {
1062
+ // Clean up temp directory
1063
+ if (tempDir) {
1064
+ try {
1065
+ rmSync(tempDir, { recursive: true, force: true });
1066
+ logger.debug('[remote-import] Cleaned up temp dir: %s', tempDir);
1067
+ }
1068
+ catch {
1069
+ // Ignore cleanup errors
1070
+ }
1071
+ }
1072
+ }
1073
+ }
1074
+ //# sourceMappingURL=remote-import.js.map