@agentuity/cli 1.0.32 → 1.0.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/catalyst.d.ts +3 -1
- package/dist/catalyst.d.ts.map +1 -1
- package/dist/catalyst.js +5 -1
- package/dist/catalyst.js.map +1 -1
- package/dist/cmd/cloud/db/create.d.ts.map +1 -1
- package/dist/cmd/cloud/db/create.js +8 -10
- package/dist/cmd/cloud/db/create.js.map +1 -1
- package/dist/cmd/cloud/db/delete.d.ts.map +1 -1
- package/dist/cmd/cloud/db/delete.js +12 -14
- package/dist/cmd/cloud/db/delete.js.map +1 -1
- package/dist/cmd/cloud/db/get.d.ts.map +1 -1
- package/dist/cmd/cloud/db/get.js +7 -7
- package/dist/cmd/cloud/db/get.js.map +1 -1
- package/dist/cmd/cloud/db/list.d.ts.map +1 -1
- package/dist/cmd/cloud/db/list.js +5 -5
- package/dist/cmd/cloud/db/list.js.map +1 -1
- package/dist/cmd/cloud/db/logs.d.ts.map +1 -1
- package/dist/cmd/cloud/db/logs.js +7 -7
- package/dist/cmd/cloud/db/logs.js.map +1 -1
- package/dist/cmd/cloud/db/sql.js +5 -5
- package/dist/cmd/cloud/db/sql.js.map +1 -1
- package/dist/cmd/cloud/db/stats.d.ts.map +1 -1
- package/dist/cmd/cloud/db/stats.js +5 -5
- package/dist/cmd/cloud/db/stats.js.map +1 -1
- package/dist/cmd/cloud/deploy.d.ts.map +1 -1
- package/dist/cmd/cloud/deploy.js +1 -1
- package/dist/cmd/cloud/deploy.js.map +1 -1
- package/dist/cmd/cloud/email/stats.d.ts.map +1 -1
- package/dist/cmd/cloud/email/stats.js +5 -5
- package/dist/cmd/cloud/email/stats.js.map +1 -1
- package/dist/cmd/cloud/email/util.d.ts +2 -2
- package/dist/cmd/cloud/email/util.d.ts.map +1 -1
- package/dist/cmd/cloud/email/util.js +2 -2
- package/dist/cmd/cloud/email/util.js.map +1 -1
- package/dist/cmd/cloud/keyvalue/util.d.ts.map +1 -1
- package/dist/cmd/cloud/keyvalue/util.js +1 -1
- package/dist/cmd/cloud/keyvalue/util.js.map +1 -1
- package/dist/cmd/cloud/machine/delete.d.ts.map +1 -1
- package/dist/cmd/cloud/machine/delete.js +6 -6
- package/dist/cmd/cloud/machine/delete.js.map +1 -1
- package/dist/cmd/cloud/machine/deployments.d.ts.map +1 -1
- package/dist/cmd/cloud/machine/deployments.js +5 -5
- package/dist/cmd/cloud/machine/deployments.js.map +1 -1
- package/dist/cmd/cloud/machine/get.d.ts.map +1 -1
- package/dist/cmd/cloud/machine/get.js +5 -5
- package/dist/cmd/cloud/machine/get.js.map +1 -1
- package/dist/cmd/cloud/machine/list.d.ts.map +1 -1
- package/dist/cmd/cloud/machine/list.js +5 -5
- package/dist/cmd/cloud/machine/list.js.map +1 -1
- package/dist/cmd/cloud/queue/consumers.d.ts +3 -0
- package/dist/cmd/cloud/queue/consumers.d.ts.map +1 -0
- package/dist/cmd/cloud/queue/consumers.js +90 -0
- package/dist/cmd/cloud/queue/consumers.js.map +1 -0
- package/dist/cmd/cloud/queue/destinations.d.ts.map +1 -1
- package/dist/cmd/cloud/queue/destinations.js +20 -3
- package/dist/cmd/cloud/queue/destinations.js.map +1 -1
- package/dist/cmd/cloud/queue/index.d.ts.map +1 -1
- package/dist/cmd/cloud/queue/index.js +2 -0
- package/dist/cmd/cloud/queue/index.js.map +1 -1
- package/dist/cmd/cloud/queue/util.d.ts +1 -1
- package/dist/cmd/cloud/queue/util.d.ts.map +1 -1
- package/dist/cmd/cloud/queue/util.js +1 -1
- package/dist/cmd/cloud/queue/util.js.map +1 -1
- package/dist/cmd/cloud/redis/get.js +5 -5
- package/dist/cmd/cloud/redis/get.js.map +1 -1
- package/dist/cmd/cloud/sandbox/execution/get.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/execution/get.js +4 -4
- package/dist/cmd/cloud/sandbox/execution/get.js.map +1 -1
- package/dist/cmd/cloud/sandbox/execution/list.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/execution/list.js +5 -5
- package/dist/cmd/cloud/sandbox/execution/list.js.map +1 -1
- package/dist/cmd/cloud/sandbox/runtime/list.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/runtime/list.js +4 -4
- package/dist/cmd/cloud/sandbox/runtime/list.js.map +1 -1
- package/dist/cmd/cloud/sandbox/snapshot/build.js +1 -1
- package/dist/cmd/cloud/sandbox/snapshot/build.js.map +1 -1
- package/dist/cmd/cloud/sandbox/snapshot/create.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/snapshot/create.js +5 -5
- package/dist/cmd/cloud/sandbox/snapshot/create.js.map +1 -1
- package/dist/cmd/cloud/sandbox/snapshot/delete.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/snapshot/delete.js +4 -4
- package/dist/cmd/cloud/sandbox/snapshot/delete.js.map +1 -1
- package/dist/cmd/cloud/sandbox/snapshot/get.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/snapshot/get.js +4 -4
- package/dist/cmd/cloud/sandbox/snapshot/get.js.map +1 -1
- package/dist/cmd/cloud/sandbox/snapshot/list.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/snapshot/list.js +4 -4
- package/dist/cmd/cloud/sandbox/snapshot/list.js.map +1 -1
- package/dist/cmd/cloud/sandbox/snapshot/tag.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/snapshot/tag.js +4 -4
- package/dist/cmd/cloud/sandbox/snapshot/tag.js.map +1 -1
- package/dist/cmd/cloud/sandbox/stats.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/stats.js +5 -5
- package/dist/cmd/cloud/sandbox/stats.js.map +1 -1
- package/dist/cmd/cloud/sandbox/util.d.ts +3 -3
- package/dist/cmd/cloud/sandbox/util.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/util.js +5 -5
- package/dist/cmd/cloud/sandbox/util.js.map +1 -1
- package/dist/cmd/cloud/schedule/stats.d.ts.map +1 -1
- package/dist/cmd/cloud/schedule/stats.js +5 -5
- package/dist/cmd/cloud/schedule/stats.js.map +1 -1
- package/dist/cmd/cloud/schedule/util.d.ts +1 -1
- package/dist/cmd/cloud/schedule/util.d.ts.map +1 -1
- package/dist/cmd/cloud/schedule/util.js +1 -1
- package/dist/cmd/cloud/schedule/util.js.map +1 -1
- package/dist/cmd/cloud/services/stats.d.ts.map +1 -1
- package/dist/cmd/cloud/services/stats.js +5 -5
- package/dist/cmd/cloud/services/stats.js.map +1 -1
- package/dist/cmd/cloud/session/get.d.ts.map +1 -1
- package/dist/cmd/cloud/session/get.js +5 -5
- package/dist/cmd/cloud/session/get.js.map +1 -1
- package/dist/cmd/cloud/session/list.d.ts.map +1 -1
- package/dist/cmd/cloud/session/list.js +5 -5
- package/dist/cmd/cloud/session/list.js.map +1 -1
- package/dist/cmd/cloud/storage/config.d.ts.map +1 -1
- package/dist/cmd/cloud/storage/config.js +7 -7
- package/dist/cmd/cloud/storage/config.js.map +1 -1
- package/dist/cmd/cloud/storage/create.d.ts.map +1 -1
- package/dist/cmd/cloud/storage/create.js +8 -10
- package/dist/cmd/cloud/storage/create.js.map +1 -1
- package/dist/cmd/cloud/storage/delete.d.ts.map +1 -1
- package/dist/cmd/cloud/storage/delete.js +12 -14
- package/dist/cmd/cloud/storage/delete.js.map +1 -1
- package/dist/cmd/cloud/storage/download.d.ts.map +1 -1
- package/dist/cmd/cloud/storage/download.js +6 -6
- package/dist/cmd/cloud/storage/download.js.map +1 -1
- package/dist/cmd/cloud/storage/get.d.ts.map +1 -1
- package/dist/cmd/cloud/storage/get.js +6 -6
- package/dist/cmd/cloud/storage/get.js.map +1 -1
- package/dist/cmd/cloud/storage/list.d.ts.map +1 -1
- package/dist/cmd/cloud/storage/list.js +6 -6
- package/dist/cmd/cloud/storage/list.js.map +1 -1
- package/dist/cmd/cloud/storage/upload.d.ts.map +1 -1
- package/dist/cmd/cloud/storage/upload.js +7 -7
- package/dist/cmd/cloud/storage/upload.js.map +1 -1
- package/dist/cmd/cloud/stream/create.js +1 -1
- package/dist/cmd/cloud/stream/create.js.map +1 -1
- package/dist/cmd/cloud/stream/stats.d.ts.map +1 -1
- package/dist/cmd/cloud/stream/stats.js +5 -5
- package/dist/cmd/cloud/stream/stats.js.map +1 -1
- package/dist/cmd/cloud/task/stats.d.ts.map +1 -1
- package/dist/cmd/cloud/task/stats.js +5 -5
- package/dist/cmd/cloud/task/stats.js.map +1 -1
- package/dist/cmd/cloud/task/util.d.ts +1 -1
- package/dist/cmd/cloud/task/util.d.ts.map +1 -1
- package/dist/cmd/cloud/task/util.js +2 -2
- package/dist/cmd/cloud/task/util.js.map +1 -1
- package/dist/cmd/cloud/thread/delete.d.ts.map +1 -1
- package/dist/cmd/cloud/thread/delete.js +5 -5
- package/dist/cmd/cloud/thread/delete.js.map +1 -1
- package/dist/cmd/cloud/thread/get.d.ts.map +1 -1
- package/dist/cmd/cloud/thread/get.js +5 -5
- package/dist/cmd/cloud/thread/get.js.map +1 -1
- package/dist/cmd/cloud/thread/list.d.ts.map +1 -1
- package/dist/cmd/cloud/thread/list.js +5 -5
- package/dist/cmd/cloud/thread/list.js.map +1 -1
- package/dist/cmd/cloud/vector/util.d.ts.map +1 -1
- package/dist/cmd/cloud/vector/util.js +1 -1
- package/dist/cmd/cloud/vector/util.js.map +1 -1
- package/dist/cmd/cloud/webhook/util.d.ts +1 -1
- package/dist/cmd/cloud/webhook/util.d.ts.map +1 -1
- package/dist/cmd/cloud/webhook/util.js +1 -1
- package/dist/cmd/cloud/webhook/util.js.map +1 -1
- package/dist/cmd/git/api.d.ts +38 -0
- package/dist/cmd/git/api.d.ts.map +1 -1
- package/dist/cmd/git/api.js +55 -0
- package/dist/cmd/git/api.js.map +1 -1
- package/dist/cmd/project/add/database.js +8 -8
- package/dist/cmd/project/add/database.js.map +1 -1
- package/dist/cmd/project/add/storage.js +8 -8
- package/dist/cmd/project/add/storage.js.map +1 -1
- package/dist/cmd/project/auth/init.d.ts.map +1 -1
- package/dist/cmd/project/auth/init.js +7 -6
- package/dist/cmd/project/auth/init.js.map +1 -1
- package/dist/cmd/project/auth/shared.d.ts +2 -3
- package/dist/cmd/project/auth/shared.d.ts.map +1 -1
- package/dist/cmd/project/auth/shared.js +7 -7
- package/dist/cmd/project/auth/shared.js.map +1 -1
- package/dist/cmd/project/download.d.ts +12 -1
- package/dist/cmd/project/download.d.ts.map +1 -1
- package/dist/cmd/project/download.js +37 -14
- package/dist/cmd/project/download.js.map +1 -1
- package/dist/cmd/project/import.d.ts.map +1 -1
- package/dist/cmd/project/import.js +67 -9
- package/dist/cmd/project/import.js.map +1 -1
- package/dist/cmd/project/remote-import.d.ts +41 -0
- package/dist/cmd/project/remote-import.d.ts.map +1 -0
- package/dist/cmd/project/remote-import.js +1074 -0
- package/dist/cmd/project/remote-import.js.map +1 -0
- package/dist/cmd/project/template-flow.d.ts +1 -1
- package/dist/cmd/project/template-flow.d.ts.map +1 -1
- package/dist/cmd/project/template-flow.js +27 -11
- package/dist/cmd/project/template-flow.js.map +1 -1
- package/dist/config.d.ts +29 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +13 -14
- package/dist/config.js.map +1 -1
- package/dist/schema-parser.d.ts.map +1 -1
- package/dist/schema-parser.js +47 -5
- package/dist/schema-parser.js.map +1 -1
- package/dist/types.d.ts +27 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +42 -0
- package/dist/types.js.map +1 -1
- package/package.json +6 -6
- package/src/catalyst.ts +9 -1
- package/src/cmd/cloud/db/create.ts +8 -9
- package/src/cmd/cloud/db/delete.ts +18 -13
- package/src/cmd/cloud/db/get.ts +13 -7
- package/src/cmd/cloud/db/list.ts +11 -5
- package/src/cmd/cloud/db/logs.ts +13 -7
- package/src/cmd/cloud/db/sql.ts +5 -5
- package/src/cmd/cloud/db/stats.ts +11 -5
- package/src/cmd/cloud/deploy.ts +7 -1
- package/src/cmd/cloud/email/stats.ts +11 -5
- package/src/cmd/cloud/email/util.ts +4 -4
- package/src/cmd/cloud/keyvalue/util.ts +2 -2
- package/src/cmd/cloud/machine/delete.ts +12 -6
- package/src/cmd/cloud/machine/deployments.ts +11 -5
- package/src/cmd/cloud/machine/get.ts +11 -5
- package/src/cmd/cloud/machine/list.ts +11 -5
- package/src/cmd/cloud/queue/consumers.ts +97 -0
- package/src/cmd/cloud/queue/destinations.ts +21 -3
- package/src/cmd/cloud/queue/index.ts +2 -0
- package/src/cmd/cloud/queue/util.ts +2 -2
- package/src/cmd/cloud/redis/get.ts +5 -5
- package/src/cmd/cloud/sandbox/execution/get.ts +10 -4
- package/src/cmd/cloud/sandbox/execution/list.ts +6 -5
- package/src/cmd/cloud/sandbox/runtime/list.ts +10 -4
- package/src/cmd/cloud/sandbox/snapshot/build.ts +1 -1
- package/src/cmd/cloud/sandbox/snapshot/create.ts +12 -5
- package/src/cmd/cloud/sandbox/snapshot/delete.ts +10 -4
- package/src/cmd/cloud/sandbox/snapshot/get.ts +12 -6
- package/src/cmd/cloud/sandbox/snapshot/list.ts +10 -4
- package/src/cmd/cloud/sandbox/snapshot/tag.ts +10 -4
- package/src/cmd/cloud/sandbox/stats.ts +11 -5
- package/src/cmd/cloud/sandbox/util.ts +14 -7
- package/src/cmd/cloud/schedule/stats.ts +11 -5
- package/src/cmd/cloud/schedule/util.ts +3 -3
- package/src/cmd/cloud/services/stats.ts +13 -7
- package/src/cmd/cloud/session/get.ts +14 -8
- package/src/cmd/cloud/session/list.ts +11 -5
- package/src/cmd/cloud/storage/config.ts +24 -12
- package/src/cmd/cloud/storage/create.ts +8 -9
- package/src/cmd/cloud/storage/delete.ts +18 -13
- package/src/cmd/cloud/storage/download.ts +12 -6
- package/src/cmd/cloud/storage/get.ts +12 -6
- package/src/cmd/cloud/storage/list.ts +12 -6
- package/src/cmd/cloud/storage/upload.ts +13 -7
- package/src/cmd/cloud/stream/create.ts +1 -1
- package/src/cmd/cloud/stream/stats.ts +11 -5
- package/src/cmd/cloud/task/stats.ts +11 -5
- package/src/cmd/cloud/task/util.ts +4 -4
- package/src/cmd/cloud/thread/delete.ts +11 -5
- package/src/cmd/cloud/thread/get.ts +11 -5
- package/src/cmd/cloud/thread/list.ts +11 -5
- package/src/cmd/cloud/vector/util.ts +2 -2
- package/src/cmd/cloud/webhook/util.ts +2 -2
- package/src/cmd/git/api.ts +127 -0
- package/src/cmd/project/add/database.ts +9 -9
- package/src/cmd/project/add/storage.ts +9 -9
- package/src/cmd/project/auth/init.ts +11 -10
- package/src/cmd/project/auth/shared.ts +15 -10
- package/src/cmd/project/download.ts +52 -16
- package/src/cmd/project/import.ts +71 -9
- package/src/cmd/project/remote-import.ts +1347 -0
- package/src/cmd/project/template-flow.ts +38 -22
- package/src/config.ts +23 -18
- package/src/schema-parser.ts +48 -5
- 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
|