@agentuity/cli 0.1.32 → 0.1.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/cmd/cloud/deploy.d.ts.map +1 -1
- package/dist/cmd/cloud/deploy.js +52 -2
- package/dist/cmd/cloud/deploy.js.map +1 -1
- package/dist/cmd/cloud/env/delete.d.ts.map +1 -1
- package/dist/cmd/cloud/env/delete.js +3 -4
- package/dist/cmd/cloud/env/delete.js.map +1 -1
- package/dist/cmd/cloud/env/import.d.ts.map +1 -1
- package/dist/cmd/cloud/env/import.js +4 -6
- package/dist/cmd/cloud/env/import.js.map +1 -1
- package/dist/cmd/cloud/env/pull.d.ts.map +1 -1
- package/dist/cmd/cloud/env/pull.js +17 -25
- package/dist/cmd/cloud/env/pull.js.map +1 -1
- package/dist/cmd/cloud/env/set.d.ts.map +1 -1
- package/dist/cmd/cloud/env/set.js +3 -6
- package/dist/cmd/cloud/env/set.js.map +1 -1
- package/dist/cmd/cloud/region-lookup.d.ts +2 -2
- package/dist/cmd/cloud/region-lookup.d.ts.map +1 -1
- package/dist/cmd/cloud/region-lookup.js +7 -3
- package/dist/cmd/cloud/region-lookup.js.map +1 -1
- package/dist/cmd/cloud/sandbox/exec.d.ts.map +1 -1
- package/dist/cmd/cloud/sandbox/exec.js +10 -35
- package/dist/cmd/cloud/sandbox/exec.js.map +1 -1
- package/dist/cmd/cloud/scp/download.d.ts.map +1 -1
- package/dist/cmd/cloud/scp/download.js +1 -1
- package/dist/cmd/cloud/scp/download.js.map +1 -1
- package/dist/cmd/cloud/scp/upload.d.ts.map +1 -1
- package/dist/cmd/cloud/scp/upload.js +1 -1
- package/dist/cmd/cloud/scp/upload.js.map +1 -1
- package/dist/cmd/cloud/ssh.d.ts.map +1 -1
- package/dist/cmd/cloud/ssh.js +1 -1
- package/dist/cmd/cloud/ssh.js.map +1 -1
- package/dist/cmd/cloud/storage/create.d.ts.map +1 -1
- package/dist/cmd/cloud/storage/create.js +7 -2
- package/dist/cmd/cloud/storage/create.js.map +1 -1
- package/dist/cmd/cloud/storage/get.d.ts.map +1 -1
- package/dist/cmd/cloud/storage/get.js +6 -0
- 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 -0
- package/dist/cmd/cloud/storage/list.js.map +1 -1
- package/dist/cmd/project/auth/init.d.ts.map +1 -1
- package/dist/cmd/project/auth/init.js +10 -21
- package/dist/cmd/project/auth/init.js.map +1 -1
- package/dist/config.d.ts +2 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +7 -2
- package/dist/config.js.map +1 -1
- package/dist/env-util.d.ts +8 -1
- package/dist/env-util.d.ts.map +1 -1
- package/dist/env-util.js +12 -3
- package/dist/env-util.js.map +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/installation-type.d.ts.map +1 -1
- package/dist/utils/installation-type.js +54 -16
- package/dist/utils/installation-type.js.map +1 -1
- package/package.json +6 -6
- package/src/cmd/cloud/deploy.ts +71 -1
- package/src/cmd/cloud/env/delete.ts +2 -4
- package/src/cmd/cloud/env/import.ts +3 -8
- package/src/cmd/cloud/env/pull.ts +17 -26
- package/src/cmd/cloud/env/set.ts +2 -8
- package/src/cmd/cloud/region-lookup.ts +19 -4
- package/src/cmd/cloud/sandbox/exec.ts +10 -41
- package/src/cmd/cloud/scp/download.ts +2 -1
- package/src/cmd/cloud/scp/upload.ts +2 -1
- package/src/cmd/cloud/ssh.ts +2 -1
- package/src/cmd/cloud/storage/create.ts +7 -2
- package/src/cmd/cloud/storage/get.ts +6 -0
- package/src/cmd/cloud/storage/list.ts +6 -0
- package/src/cmd/project/auth/init.ts +10 -22
- package/src/config.ts +10 -2
- package/src/env-util.ts +20 -3
- package/src/types.ts +4 -1
- package/src/utils/installation-type.ts +55 -16
|
@@ -7,10 +7,8 @@ import {
|
|
|
7
7
|
readEnvFile,
|
|
8
8
|
writeEnvFile,
|
|
9
9
|
filterAgentuitySdkKeys,
|
|
10
|
-
mergeEnvVars,
|
|
11
10
|
splitEnvAndSecrets,
|
|
12
11
|
validateNoPublicSecrets,
|
|
13
|
-
isReservedAgentuityKey,
|
|
14
12
|
} from '../../../env-util';
|
|
15
13
|
import { getCommand } from '../../../command-prefix';
|
|
16
14
|
import { resolveOrgId, isOrgScope } from './org-util';
|
|
@@ -164,12 +162,9 @@ export const importSubcommand = createSubcommand({
|
|
|
164
162
|
let localEnvPath: string | undefined;
|
|
165
163
|
if (projectDir) {
|
|
166
164
|
localEnvPath = await findExistingEnvFile(projectDir);
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
await writeEnvFile(localEnvPath, mergedEnv, {
|
|
171
|
-
skipKeys: Object.keys(mergedEnv).filter(isReservedAgentuityKey),
|
|
172
|
-
});
|
|
165
|
+
// writeEnvFile preserves existing keys by default, so just write the filtered vars
|
|
166
|
+
// This will merge with existing .env content, preserving AGENTUITY_SDK_KEY and other keys
|
|
167
|
+
await writeEnvFile(localEnvPath, filteredVars);
|
|
173
168
|
}
|
|
174
169
|
|
|
175
170
|
tui.success(
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
2
|
import { createSubcommand } from '../../../types';
|
|
4
3
|
import * as tui from '../../../tui';
|
|
5
4
|
import { projectGet, orgEnvGet } from '@agentuity/server';
|
|
@@ -8,7 +7,6 @@ import {
|
|
|
8
7
|
readEnvFile,
|
|
9
8
|
writeEnvFile,
|
|
10
9
|
mergeEnvVars,
|
|
11
|
-
isReservedAgentuityKey,
|
|
12
10
|
} from '../../../env-util';
|
|
13
11
|
import { getCommand } from '../../../command-prefix';
|
|
14
12
|
import { resolveOrgId, isOrgScope } from './org-util';
|
|
@@ -93,7 +91,7 @@ export const pullSubcommand = createSubcommand({
|
|
|
93
91
|
const targetEnvPath = await findExistingEnvFile(projectDir);
|
|
94
92
|
const localEnv = await readEnvFile(targetEnvPath);
|
|
95
93
|
|
|
96
|
-
// Preserve local AGENTUITY_SDK_KEY
|
|
94
|
+
// Preserve local AGENTUITY_SDK_KEY
|
|
97
95
|
const localSdkKey = localEnv.AGENTUITY_SDK_KEY;
|
|
98
96
|
|
|
99
97
|
// Merge: cloud values override local if force=true, otherwise keep local
|
|
@@ -106,32 +104,25 @@ export const pullSubcommand = createSubcommand({
|
|
|
106
104
|
mergedEnv = mergeEnvVars(cloudEnv, localEnv);
|
|
107
105
|
}
|
|
108
106
|
|
|
109
|
-
//
|
|
107
|
+
// Determine the SDK key to use: cloud api_key is source of truth, fallback to local
|
|
108
|
+
const sdkKeyToWrite = cloudApiKey || localSdkKey;
|
|
109
|
+
if (sdkKeyToWrite) {
|
|
110
|
+
mergedEnv.AGENTUITY_SDK_KEY = sdkKeyToWrite;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Write to .env in a single operation, preserveExisting: false since we have the full merged state
|
|
110
114
|
await writeEnvFile(targetEnvPath, mergedEnv, {
|
|
111
|
-
|
|
115
|
+
preserveExisting: false,
|
|
116
|
+
addComment: (key) => {
|
|
117
|
+
if (key === 'AGENTUITY_SDK_KEY') {
|
|
118
|
+
return 'AGENTUITY_SDK_KEY is a sensitive value and should not be committed to version control.';
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
},
|
|
112
122
|
});
|
|
113
123
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const dotEnvPath = join(projectDir, '.env');
|
|
117
|
-
const dotEnv = await readEnvFile(dotEnvPath);
|
|
118
|
-
|
|
119
|
-
// Cloud is source of truth: use cloud api_key if available, otherwise fallback to local
|
|
120
|
-
// For org scope, only restore if local key exists (orgs don't have api_key)
|
|
121
|
-
const sdkKeyToWrite = cloudApiKey || localSdkKey;
|
|
122
|
-
if (sdkKeyToWrite) {
|
|
123
|
-
dotEnv.AGENTUITY_SDK_KEY = sdkKeyToWrite;
|
|
124
|
-
await writeEnvFile(dotEnvPath, dotEnv, {
|
|
125
|
-
addComment: (key) => {
|
|
126
|
-
if (key === 'AGENTUITY_SDK_KEY') {
|
|
127
|
-
return 'AGENTUITY_SDK_KEY is a sensitive value and should not be committed to version control.';
|
|
128
|
-
}
|
|
129
|
-
return null;
|
|
130
|
-
},
|
|
131
|
-
});
|
|
132
|
-
if (cloudApiKey && cloudApiKey !== localSdkKey) {
|
|
133
|
-
tui.info(`Wrote AGENTUITY_SDK_KEY to ${dotEnvPath}`);
|
|
134
|
-
}
|
|
124
|
+
if (cloudApiKey && cloudApiKey !== localSdkKey) {
|
|
125
|
+
tui.info(`Wrote AGENTUITY_SDK_KEY to ${targetEnvPath}`);
|
|
135
126
|
}
|
|
136
127
|
|
|
137
128
|
const count = Object.keys(cloudEnv).length;
|
package/src/cmd/cloud/env/set.ts
CHANGED
|
@@ -4,9 +4,7 @@ import * as tui from '../../../tui';
|
|
|
4
4
|
import { projectEnvUpdate, orgEnvUpdate } from '@agentuity/server';
|
|
5
5
|
import {
|
|
6
6
|
findExistingEnvFile,
|
|
7
|
-
readEnvFile,
|
|
8
7
|
writeEnvFile,
|
|
9
|
-
filterAgentuitySdkKeys,
|
|
10
8
|
looksLikeSecret,
|
|
11
9
|
isReservedAgentuityKey,
|
|
12
10
|
isPublicVarKey,
|
|
@@ -145,12 +143,8 @@ export const setSubcommand = createSubcommand({
|
|
|
145
143
|
let envFilePath: string | undefined;
|
|
146
144
|
if (projectDir) {
|
|
147
145
|
envFilePath = await findExistingEnvFile(projectDir);
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
// Filter out AGENTUITY_ keys before writing
|
|
152
|
-
const filteredEnv = filterAgentuitySdkKeys(currentEnv);
|
|
153
|
-
await writeEnvFile(envFilePath, filteredEnv);
|
|
146
|
+
// Write only the new key - writeEnvFile preserves existing keys by default
|
|
147
|
+
await writeEnvFile(envFilePath, { [args.key]: args.value });
|
|
154
148
|
}
|
|
155
149
|
|
|
156
150
|
const successMsg = envFilePath
|
|
@@ -2,7 +2,7 @@ import type { Logger } from '@agentuity/core';
|
|
|
2
2
|
import { projectGet, sandboxGet, deploymentGet, type APIClient } from '@agentuity/server';
|
|
3
3
|
import { getResourceRegion, setResourceRegion } from '../../cache';
|
|
4
4
|
import { getGlobalCatalystAPIClient } from '../../config';
|
|
5
|
-
import type { AuthData } from '../../types';
|
|
5
|
+
import type { AuthData, Config } from '../../types';
|
|
6
6
|
import * as tui from '../../tui';
|
|
7
7
|
import { ErrorCode } from '../../errors';
|
|
8
8
|
|
|
@@ -35,7 +35,8 @@ export async function getIdentifierRegion(
|
|
|
35
35
|
apiClient: APIClient,
|
|
36
36
|
profileName = 'production',
|
|
37
37
|
identifier: string,
|
|
38
|
-
orgId?: string
|
|
38
|
+
orgId?: string,
|
|
39
|
+
config?: Config | null
|
|
39
40
|
): Promise<string> {
|
|
40
41
|
const identifierType = getIdentifierType(identifier);
|
|
41
42
|
|
|
@@ -57,8 +58,14 @@ export async function getIdentifierRegion(
|
|
|
57
58
|
const deployment = await deploymentGet(apiClient, identifier);
|
|
58
59
|
region = deployment.cloudRegion ?? null;
|
|
59
60
|
} else {
|
|
60
|
-
// sandbox
|
|
61
|
-
const globalClient = await getGlobalCatalystAPIClient(
|
|
61
|
+
// sandbox - pass config to getGlobalCatalystAPIClient for proper region resolution
|
|
62
|
+
const globalClient = await getGlobalCatalystAPIClient(
|
|
63
|
+
logger,
|
|
64
|
+
auth,
|
|
65
|
+
profileName,
|
|
66
|
+
orgId,
|
|
67
|
+
config
|
|
68
|
+
);
|
|
62
69
|
const sandbox = await sandboxGet(globalClient, { sandboxId: identifier, orgId });
|
|
63
70
|
region = sandbox.region ?? null;
|
|
64
71
|
}
|
|
@@ -70,6 +77,14 @@ export async function getIdentifierRegion(
|
|
|
70
77
|
);
|
|
71
78
|
}
|
|
72
79
|
|
|
80
|
+
// Validate region is a non-empty string
|
|
81
|
+
if (typeof region !== 'string' || region.trim() === '') {
|
|
82
|
+
tui.fatal(
|
|
83
|
+
`Invalid region returned for ${identifierType} '${identifier}': '${region}'. Use --region flag to specify a valid region.`,
|
|
84
|
+
ErrorCode.RESOURCE_NOT_FOUND
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
73
88
|
// Cache the result
|
|
74
89
|
await setResourceRegion(identifierType, profileName, identifier, region);
|
|
75
90
|
logger.trace(`[region-lookup] Cached region for ${identifier}: ${region}`);
|
|
@@ -7,8 +7,8 @@ import { getCommand } from '../../../command-prefix';
|
|
|
7
7
|
import { sandboxExecute, executionGet, writeAndDrain } from '@agentuity/server';
|
|
8
8
|
import type { Logger } from '@agentuity/core';
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
const
|
|
10
|
+
// Server-side long-poll wait duration (max 5 minutes supported by server)
|
|
11
|
+
const EXECUTION_WAIT_DURATION = '5m';
|
|
12
12
|
|
|
13
13
|
const SandboxExecResponseSchema = z.object({
|
|
14
14
|
executionId: z.string().describe('Unique execution identifier'),
|
|
@@ -115,41 +115,14 @@ export const execSubcommand = createCommand({
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
await sleep(POLL_INTERVAL_MS);
|
|
127
|
-
attempts++;
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
const execInfo = await executionGet(client, {
|
|
131
|
-
executionId: execution.executionId,
|
|
132
|
-
orgId,
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
if (
|
|
136
|
-
execInfo.status === 'completed' ||
|
|
137
|
-
execInfo.status === 'failed' ||
|
|
138
|
-
execInfo.status === 'timeout' ||
|
|
139
|
-
execInfo.status === 'cancelled'
|
|
140
|
-
) {
|
|
141
|
-
finalExecution = {
|
|
142
|
-
executionId: execInfo.executionId,
|
|
143
|
-
status: execInfo.status,
|
|
144
|
-
exitCode: execInfo.exitCode,
|
|
145
|
-
durationMs: execInfo.durationMs,
|
|
146
|
-
};
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
149
|
-
} catch {
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
118
|
+
// Use server-side long-polling to wait for execution completion
|
|
119
|
+
// This is more efficient than client-side polling and provides immediate
|
|
120
|
+
// error detection if the sandbox is terminated
|
|
121
|
+
const finalExecution = await executionGet(client, {
|
|
122
|
+
executionId: execution.executionId,
|
|
123
|
+
orgId,
|
|
124
|
+
wait: EXECUTION_WAIT_DURATION,
|
|
125
|
+
});
|
|
153
126
|
|
|
154
127
|
// Wait for all streams to reach EOF (Pulse blocks until true EOF)
|
|
155
128
|
await Promise.all(streamPromises);
|
|
@@ -248,8 +221,4 @@ function createCaptureStream(onChunk: (chunk: string) => void): NodeJS.WritableS
|
|
|
248
221
|
});
|
|
249
222
|
}
|
|
250
223
|
|
|
251
|
-
function sleep(ms: number): Promise<void> {
|
|
252
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
253
|
-
}
|
|
254
|
-
|
|
255
224
|
export default execSubcommand;
|
package/src/cmd/cloud/ssh.ts
CHANGED
|
@@ -29,6 +29,9 @@ export const createSubcommand = defineSubcommand({
|
|
|
29
29
|
},
|
|
30
30
|
],
|
|
31
31
|
schema: {
|
|
32
|
+
options: z.object({
|
|
33
|
+
description: z.string().optional().describe('Optional description for the bucket'),
|
|
34
|
+
}),
|
|
32
35
|
response: z.object({
|
|
33
36
|
success: z.boolean().describe('Whether creation succeeded'),
|
|
34
37
|
name: z.string().describe('Created storage bucket name'),
|
|
@@ -36,7 +39,7 @@ export const createSubcommand = defineSubcommand({
|
|
|
36
39
|
},
|
|
37
40
|
|
|
38
41
|
async handler(ctx) {
|
|
39
|
-
const { logger, orgId, region, auth, options } = ctx;
|
|
42
|
+
const { logger, orgId, region, auth, options, opts } = ctx;
|
|
40
43
|
|
|
41
44
|
// Handle dry-run mode
|
|
42
45
|
if (isDryRunMode(options)) {
|
|
@@ -58,7 +61,9 @@ export const createSubcommand = defineSubcommand({
|
|
|
58
61
|
message: `Creating storage in ${region}`,
|
|
59
62
|
clearOnSuccess: true,
|
|
60
63
|
callback: async () => {
|
|
61
|
-
return createResources(catalystClient, orgId, region!, [
|
|
64
|
+
return createResources(catalystClient, orgId, region!, [
|
|
65
|
+
{ type: 's3', description: opts.description },
|
|
66
|
+
]);
|
|
62
67
|
},
|
|
63
68
|
});
|
|
64
69
|
|
|
@@ -15,6 +15,9 @@ const StorageGetResponseSchema = z.object({
|
|
|
15
15
|
endpoint: z.string().optional().describe('S3 endpoint URL'),
|
|
16
16
|
org_id: z.string().optional().describe('Organization ID that owns this bucket'),
|
|
17
17
|
org_name: z.string().optional().describe('Organization name that owns this bucket'),
|
|
18
|
+
bucket_type: z.string().optional().describe('Bucket type (user or snapshots)'),
|
|
19
|
+
internal: z.boolean().optional().describe('Whether this is a system-managed bucket'),
|
|
20
|
+
description: z.string().optional().describe('Optional description of the bucket'),
|
|
18
21
|
});
|
|
19
22
|
|
|
20
23
|
export const getSubcommand = createSubcommand({
|
|
@@ -134,6 +137,9 @@ export const getSubcommand = createSubcommand({
|
|
|
134
137
|
endpoint: bucket.endpoint ?? undefined,
|
|
135
138
|
org_id: bucket.org_id,
|
|
136
139
|
org_name: bucket.org_name,
|
|
140
|
+
bucket_type: bucket.bucket_type,
|
|
141
|
+
internal: bucket.internal,
|
|
142
|
+
description: bucket.description ?? undefined,
|
|
137
143
|
};
|
|
138
144
|
},
|
|
139
145
|
});
|
|
@@ -20,6 +20,9 @@ const StorageListResponseSchema = z.object({
|
|
|
20
20
|
cloud_region: z.string().optional().describe('Cloud region where bucket is hosted'),
|
|
21
21
|
org_id: z.string().optional().describe('Organization ID that owns this bucket'),
|
|
22
22
|
org_name: z.string().optional().describe('Organization name that owns this bucket'),
|
|
23
|
+
bucket_type: z.string().optional().describe('Bucket type (user or snapshots)'),
|
|
24
|
+
internal: z.boolean().optional().describe('Whether this is a system-managed bucket'),
|
|
25
|
+
description: z.string().optional().describe('Optional description of the bucket'),
|
|
23
26
|
})
|
|
24
27
|
)
|
|
25
28
|
.optional()
|
|
@@ -241,6 +244,9 @@ export const listSubcommand = createSubcommand({
|
|
|
241
244
|
cloud_region: s3.cloud_region,
|
|
242
245
|
org_id: s3.org_id,
|
|
243
246
|
org_name: s3.org_name,
|
|
247
|
+
bucket_type: s3.bucket_type,
|
|
248
|
+
internal: s3.internal,
|
|
249
|
+
description: s3.description ?? undefined,
|
|
244
250
|
})),
|
|
245
251
|
};
|
|
246
252
|
},
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
generateAuthSchemaSql,
|
|
13
13
|
getGeneratedSqlDir,
|
|
14
14
|
} from './shared';
|
|
15
|
+
import { readEnvFile, writeEnvFile } from '../../../env-util';
|
|
15
16
|
import enquirer from 'enquirer';
|
|
16
17
|
import * as fs from 'fs';
|
|
17
18
|
import * as path from 'path';
|
|
@@ -96,32 +97,23 @@ export const initSubcommand = createSubcommand({
|
|
|
96
97
|
|
|
97
98
|
const databaseName = dbInfo.name;
|
|
98
99
|
|
|
99
|
-
// Update .env with database URL
|
|
100
|
+
// Update .env with database URL using proper parsing
|
|
100
101
|
const envPath = path.join(projectDir, '.env');
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (fs.existsSync(envPath)) {
|
|
104
|
-
envContent = fs.readFileSync(envPath, 'utf-8');
|
|
105
|
-
if (!envContent.endsWith('\n') && envContent.length > 0) {
|
|
106
|
-
envContent += '\n';
|
|
107
|
-
}
|
|
108
|
-
}
|
|
102
|
+
const existingEnv = await readEnvFile(envPath);
|
|
109
103
|
|
|
110
104
|
// Check if DATABASE_URL already exists
|
|
111
|
-
const hasDatabaseUrl =
|
|
105
|
+
const hasDatabaseUrl = 'DATABASE_URL' in existingEnv;
|
|
112
106
|
|
|
113
107
|
if (dbInfo.url !== databaseUrl || !hasDatabaseUrl) {
|
|
114
108
|
if (hasDatabaseUrl) {
|
|
115
109
|
// DATABASE_URL exists, use AUTH_DATABASE_URL instead
|
|
116
|
-
|
|
117
|
-
fs.writeFileSync(envPath, envContent);
|
|
110
|
+
await writeEnvFile(envPath, { AUTH_DATABASE_URL: dbInfo.url });
|
|
118
111
|
tui.success('AUTH_DATABASE_URL added to .env');
|
|
119
112
|
tui.warning(
|
|
120
113
|
`DATABASE_URL already exists. Update your ${tui.bold('src/auth.ts')} to use AUTH_DATABASE_URL.`
|
|
121
114
|
);
|
|
122
115
|
} else {
|
|
123
|
-
|
|
124
|
-
fs.writeFileSync(envPath, envContent);
|
|
116
|
+
await writeEnvFile(envPath, { DATABASE_URL: dbInfo.url });
|
|
125
117
|
tui.success('DATABASE_URL added to .env');
|
|
126
118
|
}
|
|
127
119
|
} else {
|
|
@@ -129,18 +121,14 @@ export const initSubcommand = createSubcommand({
|
|
|
129
121
|
}
|
|
130
122
|
|
|
131
123
|
// Add AGENTUITY_AUTH_SECRET if not present
|
|
132
|
-
// Re-read
|
|
133
|
-
|
|
134
|
-
if (!envContent.endsWith('\n') && envContent.length > 0) {
|
|
135
|
-
envContent += '\n';
|
|
136
|
-
}
|
|
124
|
+
// Re-read env to get latest state
|
|
125
|
+
const currentEnv = await readEnvFile(envPath);
|
|
137
126
|
|
|
138
127
|
const hasAuthSecret =
|
|
139
|
-
|
|
128
|
+
'AGENTUITY_AUTH_SECRET' in currentEnv || 'BETTER_AUTH_SECRET' in currentEnv;
|
|
140
129
|
if (!hasAuthSecret) {
|
|
141
130
|
const devSecret = `dev-${crypto.randomUUID()}-CHANGE-ME`;
|
|
142
|
-
|
|
143
|
-
fs.writeFileSync(envPath, envContent);
|
|
131
|
+
await writeEnvFile(envPath, { AGENTUITY_AUTH_SECRET: devSecret });
|
|
144
132
|
tui.success('AGENTUITY_AUTH_SECRET added to .env (development default)');
|
|
145
133
|
tui.warning(
|
|
146
134
|
`Replace ${tui.bold('AGENTUITY_AUTH_SECRET')} with a secure value before deploying.`
|
package/src/config.ts
CHANGED
|
@@ -809,14 +809,16 @@ export async function getDefaultRegion(
|
|
|
809
809
|
* @param auth - Authentication data
|
|
810
810
|
* @param profileName - Profile name (default: 'production')
|
|
811
811
|
* @param orgId - Optional organization ID for CLI key authentication
|
|
812
|
+
* @param config - Optional config for region preference lookup
|
|
812
813
|
*/
|
|
813
814
|
export async function getGlobalCatalystAPIClient(
|
|
814
815
|
logger: Logger,
|
|
815
816
|
auth: AuthData,
|
|
816
817
|
profileName = 'production',
|
|
817
|
-
orgId?: string
|
|
818
|
+
orgId?: string,
|
|
819
|
+
config?: Config | null
|
|
818
820
|
) {
|
|
819
|
-
const region = await getDefaultRegion(profileName);
|
|
821
|
+
const region = await getDefaultRegion(profileName, config);
|
|
820
822
|
return getCatalystAPIClient(logger, auth, region, orgId);
|
|
821
823
|
}
|
|
822
824
|
|
|
@@ -828,6 +830,12 @@ export function getIONHost(config: Config | null, region: string) {
|
|
|
828
830
|
if (config?.name === 'local' || region === 'local') {
|
|
829
831
|
return 'ion.agentuity.io';
|
|
830
832
|
}
|
|
833
|
+
// Validate region is a non-empty string to prevent malformed hostnames
|
|
834
|
+
if (!region || typeof region !== 'string' || region.trim() === '') {
|
|
835
|
+
throw new Error(
|
|
836
|
+
`Invalid region: '${region}'. Region must be a non-empty string. Use --region flag to specify a valid region.`
|
|
837
|
+
);
|
|
838
|
+
}
|
|
831
839
|
return `ion-${region}.agentuity.cloud`;
|
|
832
840
|
}
|
|
833
841
|
|
package/src/env-util.ts
CHANGED
|
@@ -149,7 +149,8 @@ export async function readEnvFile(path: string): Promise<EnvVars> {
|
|
|
149
149
|
|
|
150
150
|
/**
|
|
151
151
|
* Write environment variables to an .env file
|
|
152
|
-
*
|
|
152
|
+
* By default, preserves existing keys that are not in the new vars.
|
|
153
|
+
* Use preserveExisting: false to completely overwrite the file.
|
|
153
154
|
*/
|
|
154
155
|
export async function writeEnvFile(
|
|
155
156
|
path: string,
|
|
@@ -157,20 +158,36 @@ export async function writeEnvFile(
|
|
|
157
158
|
options?: {
|
|
158
159
|
skipKeys?: string[];
|
|
159
160
|
addComment?: (key: string) => string | null;
|
|
161
|
+
/**
|
|
162
|
+
* When true (default), reads existing file first and merges with new vars.
|
|
163
|
+
* New vars take priority for matching keys, but all existing keys are preserved.
|
|
164
|
+
* When false, completely overwrites the file with only the provided vars.
|
|
165
|
+
*/
|
|
166
|
+
preserveExisting?: boolean;
|
|
160
167
|
}
|
|
161
168
|
): Promise<void> {
|
|
162
169
|
const skipKeys = options?.skipKeys || [];
|
|
170
|
+
const preserveExisting = options?.preserveExisting ?? true;
|
|
171
|
+
|
|
172
|
+
// If preserveExisting is true, read existing file and merge
|
|
173
|
+
let finalVars = vars;
|
|
174
|
+
if (preserveExisting) {
|
|
175
|
+
const existing = await readEnvFile(path);
|
|
176
|
+
// Merge: existing as base, new vars override
|
|
177
|
+
finalVars = { ...existing, ...vars };
|
|
178
|
+
}
|
|
179
|
+
|
|
163
180
|
const lines: string[] = [];
|
|
164
181
|
|
|
165
182
|
// Sort keys for consistent output
|
|
166
|
-
const sortedKeys = Object.keys(
|
|
183
|
+
const sortedKeys = Object.keys(finalVars).sort();
|
|
167
184
|
|
|
168
185
|
for (const key of sortedKeys) {
|
|
169
186
|
if (skipKeys.includes(key)) {
|
|
170
187
|
continue;
|
|
171
188
|
}
|
|
172
189
|
|
|
173
|
-
const value =
|
|
190
|
+
const value = finalVars[key];
|
|
174
191
|
|
|
175
192
|
// Add comment if provided
|
|
176
193
|
if (options?.addComment) {
|
package/src/types.ts
CHANGED
|
@@ -24,7 +24,10 @@ export const ConfigSchema = zod.object({
|
|
|
24
24
|
devmode: zod
|
|
25
25
|
.object({
|
|
26
26
|
hostname: zod.string().optional().describe('Development mode hostname'),
|
|
27
|
-
privateKey: zod
|
|
27
|
+
privateKey: zod
|
|
28
|
+
.string()
|
|
29
|
+
.optional()
|
|
30
|
+
.describe('Development mode private key (base64-encoded PEM)'),
|
|
28
31
|
})
|
|
29
32
|
.optional()
|
|
30
33
|
.describe('Development mode configuration'),
|
|
@@ -2,10 +2,26 @@
|
|
|
2
2
|
* Detects how the CLI was installed and is being run
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import fs from 'node:fs';
|
|
5
6
|
import os from 'node:os';
|
|
6
7
|
|
|
7
8
|
export type InstallationType = 'global' | 'local' | 'source';
|
|
8
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Resolve a path to its real path (following symlinks) and normalize to POSIX separators.
|
|
12
|
+
* Returns the original path if resolution fails.
|
|
13
|
+
*/
|
|
14
|
+
function resolveRealPath(path: string): string {
|
|
15
|
+
if (!path) return '';
|
|
16
|
+
try {
|
|
17
|
+
// fs.realpathSync resolves symlinks (e.g., /tmp -> /private/tmp on macOS)
|
|
18
|
+
return fs.realpathSync(path).replace(/\\/g, '/');
|
|
19
|
+
} catch {
|
|
20
|
+
// If the path doesn't exist or can't be resolved, return normalized original
|
|
21
|
+
return path.replace(/\\/g, '/');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
9
25
|
/**
|
|
10
26
|
* Determines the installation type based on how the CLI is being executed
|
|
11
27
|
*
|
|
@@ -14,34 +30,57 @@ export type InstallationType = 'global' | 'local' | 'source';
|
|
|
14
30
|
* @returns 'source' - Running from source code (development)
|
|
15
31
|
*/
|
|
16
32
|
export function getInstallationType(): InstallationType {
|
|
17
|
-
//
|
|
33
|
+
// Bun.main already returns the resolved real path, just normalize separators
|
|
18
34
|
const mainPath = Bun.main.replace(/\\/g, '/');
|
|
19
|
-
// Bun.argv[1] contains the original invocation path (before symlink resolution)
|
|
20
|
-
const invokedPath = (Bun.argv[1] ?? '').replace(/\\/g, '/');
|
|
21
35
|
|
|
22
|
-
// Get
|
|
23
|
-
//
|
|
24
|
-
const home = os.homedir() ?? process.env.HOME ?? process.env.USERPROFILE ?? '';
|
|
25
|
-
const bunInstall = (process.env.BUN_INSTALL ?? (home ? `${home}/.bun` : '')).replace(/\\/g, '/');
|
|
26
|
-
const globalBinDir = bunInstall ? `${bunInstall}/bin/` : '';
|
|
36
|
+
// Get home directory reliably and resolve symlinks
|
|
37
|
+
// On macOS, os.homedir() returns /Users/xxx which is already real
|
|
38
|
+
const home = resolveRealPath(os.homedir() ?? process.env.HOME ?? process.env.USERPROFILE ?? '');
|
|
27
39
|
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
40
|
+
// Get bun install directory from BUN_INSTALL or default to ~/.bun
|
|
41
|
+
// Resolve symlinks to handle cases like BUN_INSTALL=/tmp/... on macOS where /tmp -> /private/tmp
|
|
42
|
+
const bunInstallRaw = process.env.BUN_INSTALL ?? (home ? `${home}/.bun` : '');
|
|
43
|
+
const bunInstall = resolveRealPath(bunInstallRaw);
|
|
44
|
+
|
|
45
|
+
// GLOBAL DETECTION: Check if running from bun's global install location
|
|
46
|
+
// When installed via `bun add -g`, the CLI lives at ~/.bun/node_modules/@agentuity/cli/
|
|
47
|
+
// or ~/.bun/install/global/node_modules/@agentuity/cli/
|
|
48
|
+
if (bunInstall) {
|
|
49
|
+
// Check for ~/.bun/node_modules/@agentuity/cli/ (common bun global layout)
|
|
50
|
+
if (mainPath.startsWith(`${bunInstall}/node_modules/@agentuity/cli/`)) {
|
|
51
|
+
return 'global';
|
|
52
|
+
}
|
|
53
|
+
// Check for ~/.bun/install/global/node_modules/@agentuity/cli/ (alternative layout)
|
|
54
|
+
if (mainPath.startsWith(`${bunInstall}/install/global/`)) {
|
|
55
|
+
return 'global';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// GLOBAL DETECTION: Check for legacy ~/.agentuity/ installation
|
|
60
|
+
// The install.sh script may install to ~/.agentuity/node_modules/@agentuity/cli/
|
|
61
|
+
// or create a shim at ~/.agentuity/bin/agentuity
|
|
62
|
+
if (home) {
|
|
63
|
+
const agentuityDir = resolveRealPath(`${home}/.agentuity`);
|
|
64
|
+
if (mainPath.startsWith(`${agentuityDir}/`)) {
|
|
65
|
+
return 'global';
|
|
66
|
+
}
|
|
32
67
|
}
|
|
33
68
|
|
|
34
|
-
//
|
|
35
|
-
|
|
69
|
+
// GLOBAL DETECTION: Fallback check for any path containing /.bun/ before node_modules
|
|
70
|
+
// This catches edge cases where BUN_INSTALL might not match the actual path
|
|
71
|
+
if (mainPath.includes('/.bun/') && mainPath.includes('/node_modules/@agentuity/cli/')) {
|
|
36
72
|
return 'global';
|
|
37
73
|
}
|
|
38
74
|
|
|
39
|
-
//
|
|
75
|
+
// LOCAL DETECTION: Running from a project's node_modules
|
|
76
|
+
// This is when someone runs `bunx agentuity` or has it as a project dependency
|
|
77
|
+
// At this point, we've ruled out global installs, so any node_modules path is local
|
|
40
78
|
if (mainPath.includes('/node_modules/@agentuity/cli/')) {
|
|
41
79
|
return 'local';
|
|
42
80
|
}
|
|
43
81
|
|
|
44
|
-
//
|
|
82
|
+
// SOURCE DETECTION: Running from source code (development)
|
|
83
|
+
// This is when running directly from the monorepo: packages/cli/bin/cli.ts
|
|
45
84
|
return 'source';
|
|
46
85
|
}
|
|
47
86
|
|