@agentuity/cli 1.0.5 → 1.0.7
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/build/entry-generator.js +29 -29
- package/dist/cmd/build/entry-generator.js.map +1 -1
- package/dist/cmd/cloud/env/env-diff.d.ts +35 -0
- package/dist/cmd/cloud/env/env-diff.d.ts.map +1 -0
- package/dist/cmd/cloud/env/env-diff.js +145 -0
- package/dist/cmd/cloud/env/env-diff.js.map +1 -0
- package/dist/cmd/cloud/env/pull.d.ts.map +1 -1
- package/dist/cmd/cloud/env/pull.js +51 -7
- package/dist/cmd/cloud/env/pull.js.map +1 -1
- package/dist/cmd/cloud/env/push.d.ts.map +1 -1
- package/dist/cmd/cloud/env/push.js +73 -4
- package/dist/cmd/cloud/env/push.js.map +1 -1
- package/dist/cmd/cloud/env/set.d.ts.map +1 -1
- package/dist/cmd/cloud/env/set.js +141 -45
- package/dist/cmd/cloud/env/set.js.map +1 -1
- package/dist/domain.d.ts.map +1 -1
- package/dist/domain.js +22 -0
- package/dist/domain.js.map +1 -1
- package/package.json +6 -6
- package/src/cmd/build/entry-generator.ts +29 -29
- package/src/cmd/cloud/env/env-diff.ts +180 -0
- package/src/cmd/cloud/env/pull.ts +69 -9
- package/src/cmd/cloud/env/push.ts +93 -4
- package/src/cmd/cloud/env/set.ts +178 -51
- package/src/domain.ts +22 -0
package/src/cmd/cloud/env/set.ts
CHANGED
|
@@ -13,20 +13,80 @@ import {
|
|
|
13
13
|
import { getCommand } from '../../../command-prefix';
|
|
14
14
|
import { resolveOrgId, isOrgScope } from './org-util';
|
|
15
15
|
|
|
16
|
+
interface ParsedEnvPair {
|
|
17
|
+
key: string;
|
|
18
|
+
value: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parse env set arguments into key-value pairs.
|
|
23
|
+
* Supports two formats:
|
|
24
|
+
* - Legacy: KEY VALUE (exactly 2 args)
|
|
25
|
+
* - KEY=VALUE format: KEY1=VALUE1 [KEY2=VALUE2 ...]
|
|
26
|
+
*/
|
|
27
|
+
function parseEnvArgs(rawArgs: string[]): ParsedEnvPair[] {
|
|
28
|
+
if (rawArgs.length === 0) {
|
|
29
|
+
tui.fatal(
|
|
30
|
+
'No arguments provided. Usage: env set KEY VALUE or env set KEY=VALUE [KEY2=VALUE2 ...]'
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check if first arg contains '=' — if so, treat ALL args as KEY=VALUE format
|
|
35
|
+
const firstArg = rawArgs[0]!;
|
|
36
|
+
if (firstArg.includes('=')) {
|
|
37
|
+
const pairs: ParsedEnvPair[] = [];
|
|
38
|
+
for (const arg of rawArgs) {
|
|
39
|
+
const eqIndex = arg.indexOf('=');
|
|
40
|
+
if (eqIndex === -1 || eqIndex === 0) {
|
|
41
|
+
tui.fatal(`Invalid format: '${arg}'. Expected KEY=VALUE format.`);
|
|
42
|
+
}
|
|
43
|
+
const key = arg.substring(0, eqIndex);
|
|
44
|
+
const value = arg.substring(eqIndex + 1);
|
|
45
|
+
if (!key) {
|
|
46
|
+
tui.fatal(`Invalid format: '${arg}'. Key cannot be empty.`);
|
|
47
|
+
}
|
|
48
|
+
pairs.push({ key, value });
|
|
49
|
+
}
|
|
50
|
+
return pairs;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Legacy format: exactly 2 args = KEY VALUE
|
|
54
|
+
if (rawArgs.length === 2) {
|
|
55
|
+
const key = rawArgs[0]!.trim();
|
|
56
|
+
if (!key) {
|
|
57
|
+
tui.fatal(
|
|
58
|
+
'Invalid format: key cannot be empty. Usage: env set KEY VALUE or env set KEY=VALUE'
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return [{ key, value: rawArgs[1]! }];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Ambiguous: 1 arg without '=' or 3+ args without '='
|
|
65
|
+
if (rawArgs.length === 1) {
|
|
66
|
+
tui.fatal(`Missing value for '${rawArgs[0]}'. Usage: env set KEY VALUE or env set KEY=VALUE`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 3+ args without '=' in first arg
|
|
70
|
+
tui.fatal(
|
|
71
|
+
'Multiple variables must use KEY=VALUE format. Usage: env set KEY1=VALUE1 KEY2=VALUE2 ...'
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
16
75
|
const EnvSetResponseSchema = z.object({
|
|
17
76
|
success: z.boolean().describe('Whether the operation succeeded'),
|
|
18
|
-
|
|
77
|
+
keys: z.array(z.string()).describe('Environment variable keys that were set'),
|
|
19
78
|
path: z
|
|
20
79
|
.string()
|
|
21
80
|
.optional()
|
|
22
|
-
.describe('Local file path where env
|
|
23
|
-
|
|
24
|
-
|
|
81
|
+
.describe('Local file path where env vars were saved (project scope only)'),
|
|
82
|
+
secretKeys: z.array(z.string()).describe('Keys that were stored as secrets'),
|
|
83
|
+
envKeys: z.array(z.string()).describe('Keys that were stored as env vars'),
|
|
84
|
+
scope: z.enum(['project', 'org']).describe('The scope where the variables were set'),
|
|
25
85
|
});
|
|
26
86
|
|
|
27
87
|
export const setSubcommand = createSubcommand({
|
|
28
88
|
name: 'set',
|
|
29
|
-
description: 'Set
|
|
89
|
+
description: 'Set one or more environment variables or secrets',
|
|
30
90
|
tags: ['mutating', 'updates-resource', 'slow', 'requires-auth'],
|
|
31
91
|
idempotent: true,
|
|
32
92
|
requires: { auth: true, apiClient: true },
|
|
@@ -36,7 +96,15 @@ export const setSubcommand = createSubcommand({
|
|
|
36
96
|
command: getCommand('env set NODE_ENV production'),
|
|
37
97
|
description: 'Set environment variable',
|
|
38
98
|
},
|
|
99
|
+
{
|
|
100
|
+
command: getCommand('env set NODE_ENV=production'),
|
|
101
|
+
description: 'Set using KEY=VALUE format',
|
|
102
|
+
},
|
|
39
103
|
{ command: getCommand('env set PORT 3000'), description: 'Set port number' },
|
|
104
|
+
{
|
|
105
|
+
command: getCommand('env set NODE_ENV=production LOG_LEVEL=info PORT=3000'),
|
|
106
|
+
description: 'Set multiple variables at once',
|
|
107
|
+
},
|
|
40
108
|
{
|
|
41
109
|
command: getCommand('env set API_KEY "sk_..." --secret'),
|
|
42
110
|
description: 'Set a secret value',
|
|
@@ -48,8 +116,7 @@ export const setSubcommand = createSubcommand({
|
|
|
48
116
|
],
|
|
49
117
|
schema: {
|
|
50
118
|
args: z.object({
|
|
51
|
-
|
|
52
|
-
value: z.string().describe('the environment variable value'),
|
|
119
|
+
args: z.array(z.string()).describe('KEY VALUE or KEY=VALUE [KEY2=VALUE2 ...]'),
|
|
53
120
|
}),
|
|
54
121
|
options: z.object({
|
|
55
122
|
secret: z
|
|
@@ -67,8 +134,9 @@ export const setSubcommand = createSubcommand({
|
|
|
67
134
|
},
|
|
68
135
|
|
|
69
136
|
async handler(ctx) {
|
|
70
|
-
const { args, opts, apiClient, project, projectDir, config } = ctx;
|
|
137
|
+
const { args: cmdArgs, opts, apiClient, project, projectDir, config } = ctx;
|
|
71
138
|
const useOrgScope = isOrgScope(opts?.org);
|
|
139
|
+
const forceSecret = opts?.secret ?? false;
|
|
72
140
|
|
|
73
141
|
// Require project context if not using org scope
|
|
74
142
|
if (!useOrgScope && !project) {
|
|
@@ -77,86 +145,145 @@ export const setSubcommand = createSubcommand({
|
|
|
77
145
|
);
|
|
78
146
|
}
|
|
79
147
|
|
|
80
|
-
|
|
81
|
-
const isPublic = isPublicVarKey(args.key);
|
|
148
|
+
const pairs = parseEnvArgs(cmdArgs.args);
|
|
82
149
|
|
|
83
|
-
// Validate
|
|
84
|
-
|
|
85
|
-
|
|
150
|
+
// Validate all keys first
|
|
151
|
+
for (const pair of pairs) {
|
|
152
|
+
if (isReservedAgentuityKey(pair.key)) {
|
|
153
|
+
tui.fatal(
|
|
154
|
+
`Cannot set AGENTUITY_ prefixed variables: '${pair.key}'. These are reserved for system use.`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
86
157
|
}
|
|
87
158
|
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
159
|
+
// Reject duplicate keys
|
|
160
|
+
const seenKeys = new Set<string>();
|
|
161
|
+
for (const pair of pairs) {
|
|
162
|
+
if (seenKeys.has(pair.key)) {
|
|
163
|
+
tui.fatal(`Duplicate key '${pair.key}'. Each variable may only be specified once.`);
|
|
164
|
+
}
|
|
165
|
+
seenKeys.add(pair.key);
|
|
93
166
|
}
|
|
94
167
|
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
168
|
+
// Classify each pair as env or secret
|
|
169
|
+
const envPairs: Record<string, string> = {};
|
|
170
|
+
const secretPairs: Record<string, string> = {};
|
|
171
|
+
const secretKeysList: string[] = [];
|
|
172
|
+
const envKeysList: string[] = [];
|
|
173
|
+
|
|
174
|
+
for (const pair of pairs) {
|
|
175
|
+
const isPublic = isPublicVarKey(pair.key);
|
|
176
|
+
let isSecret = forceSecret;
|
|
99
177
|
|
|
100
|
-
|
|
178
|
+
// Validate public vars cannot be secrets
|
|
179
|
+
if (isSecret && isPublic) {
|
|
180
|
+
tui.fatal(
|
|
181
|
+
`Cannot set public variables as secrets. '${pair.key}' (prefix ${PUBLIC_VAR_PREFIXES.join(', ')}) is exposed to the frontend.`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Auto-detect if this looks like a secret and offer to store as secret
|
|
186
|
+
// Skip auto-detect for public vars since they can never be secrets
|
|
187
|
+
if (!isSecret && !isPublic && looksLikeSecret(pair.key, pair.value)) {
|
|
188
|
+
if (pairs.length === 1) {
|
|
189
|
+
// Single pair: offer interactive prompt (existing behavior)
|
|
190
|
+
tui.warning(`The variable '${pair.key}' looks like it should be a secret.`);
|
|
191
|
+
const storeAsSecret = await tui.confirm('Store as a secret instead?', true);
|
|
192
|
+
if (storeAsSecret) {
|
|
193
|
+
isSecret = true;
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
// Multiple pairs: auto-detect silently
|
|
197
|
+
isSecret = true;
|
|
198
|
+
tui.info(`Auto-detected '${pair.key}' as a secret`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
101
201
|
|
|
102
|
-
if (
|
|
103
|
-
|
|
202
|
+
if (isSecret) {
|
|
203
|
+
secretPairs[pair.key] = pair.value;
|
|
204
|
+
secretKeysList.push(pair.key);
|
|
205
|
+
} else {
|
|
206
|
+
envPairs[pair.key] = pair.value;
|
|
207
|
+
envKeysList.push(pair.key);
|
|
104
208
|
}
|
|
105
209
|
}
|
|
106
210
|
|
|
107
|
-
const
|
|
211
|
+
const totalCount = pairs.length;
|
|
212
|
+
const allKeys = [...envKeysList, ...secretKeysList];
|
|
213
|
+
const secretSuffix =
|
|
214
|
+
secretKeysList.length > 0
|
|
215
|
+
? ` (${secretKeysList.length} secret${secretKeysList.length !== 1 ? 's' : ''})`
|
|
216
|
+
: '';
|
|
108
217
|
|
|
109
218
|
if (useOrgScope) {
|
|
110
219
|
// Organization scope
|
|
111
220
|
const orgId = await resolveOrgId(apiClient, config, opts!.org!);
|
|
112
221
|
|
|
113
|
-
const updatePayload
|
|
114
|
-
|
|
115
|
-
|
|
222
|
+
const updatePayload: {
|
|
223
|
+
id: string;
|
|
224
|
+
env?: Record<string, string>;
|
|
225
|
+
secrets?: Record<string, string>;
|
|
226
|
+
} = { id: orgId };
|
|
227
|
+
if (Object.keys(envPairs).length > 0) updatePayload.env = envPairs;
|
|
228
|
+
if (Object.keys(secretPairs).length > 0) updatePayload.secrets = secretPairs;
|
|
116
229
|
|
|
117
|
-
await tui.spinner(
|
|
118
|
-
|
|
119
|
-
|
|
230
|
+
await tui.spinner(
|
|
231
|
+
`Setting ${totalCount} organization variable${totalCount !== 1 ? 's' : ''} in cloud`,
|
|
232
|
+
() => {
|
|
233
|
+
return orgEnvUpdate(apiClient, updatePayload);
|
|
234
|
+
}
|
|
235
|
+
);
|
|
120
236
|
|
|
121
237
|
tui.success(
|
|
122
|
-
`Organization ${
|
|
238
|
+
`Organization variable${totalCount !== 1 ? 's' : ''} set successfully: ${allKeys.join(', ')}${secretSuffix}`
|
|
123
239
|
);
|
|
124
240
|
|
|
125
241
|
return {
|
|
126
242
|
success: true,
|
|
127
|
-
|
|
128
|
-
|
|
243
|
+
keys: allKeys,
|
|
244
|
+
secretKeys: secretKeysList,
|
|
245
|
+
envKeys: envKeysList,
|
|
129
246
|
scope: 'org' as const,
|
|
130
247
|
};
|
|
131
248
|
} else {
|
|
132
|
-
// Project scope
|
|
133
|
-
const updatePayload
|
|
134
|
-
|
|
135
|
-
|
|
249
|
+
// Project scope
|
|
250
|
+
const updatePayload: {
|
|
251
|
+
id: string;
|
|
252
|
+
env?: Record<string, string>;
|
|
253
|
+
secrets?: Record<string, string>;
|
|
254
|
+
} = { id: project!.projectId };
|
|
255
|
+
if (Object.keys(envPairs).length > 0) updatePayload.env = envPairs;
|
|
256
|
+
if (Object.keys(secretPairs).length > 0) updatePayload.secrets = secretPairs;
|
|
136
257
|
|
|
137
|
-
await tui.spinner(
|
|
138
|
-
|
|
139
|
-
|
|
258
|
+
await tui.spinner(
|
|
259
|
+
`Setting ${totalCount} variable${totalCount !== 1 ? 's' : ''} in cloud`,
|
|
260
|
+
() => {
|
|
261
|
+
return projectEnvUpdate(apiClient, updatePayload);
|
|
262
|
+
}
|
|
263
|
+
);
|
|
140
264
|
|
|
141
265
|
// Update local .env file only if we have a project directory
|
|
142
|
-
// (not when using --project-id without being in a project folder)
|
|
143
266
|
let envFilePath: string | undefined;
|
|
144
267
|
if (projectDir) {
|
|
145
268
|
envFilePath = await findExistingEnvFile(projectDir);
|
|
146
|
-
|
|
147
|
-
|
|
269
|
+
const allPairsForLocal: Record<string, string> = {
|
|
270
|
+
...envPairs,
|
|
271
|
+
...secretPairs,
|
|
272
|
+
};
|
|
273
|
+
await writeEnvFile(envFilePath, allPairsForLocal);
|
|
148
274
|
}
|
|
149
275
|
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
276
|
+
const locationMsg = envFilePath ? ` (cloud + ${envFilePath})` : ' (cloud only)';
|
|
277
|
+
tui.success(
|
|
278
|
+
`Variable${totalCount !== 1 ? 's' : ''} set successfully: ${allKeys.join(', ')}${secretSuffix}${locationMsg}`
|
|
279
|
+
);
|
|
154
280
|
|
|
155
281
|
return {
|
|
156
282
|
success: true,
|
|
157
|
-
|
|
283
|
+
keys: allKeys,
|
|
158
284
|
path: envFilePath,
|
|
159
|
-
|
|
285
|
+
secretKeys: secretKeysList,
|
|
286
|
+
envKeys: envKeysList,
|
|
160
287
|
scope: 'project' as const,
|
|
161
288
|
};
|
|
162
289
|
}
|
package/src/domain.ts
CHANGED
|
@@ -110,6 +110,28 @@ export async function checkCustomDomainForDNS(
|
|
|
110
110
|
|
|
111
111
|
return Promise.all(
|
|
112
112
|
domains.map(async (domain) => {
|
|
113
|
+
// Detect if user passed a URL instead of a domain name
|
|
114
|
+
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(domain)) {
|
|
115
|
+
try {
|
|
116
|
+
const url = new URL(domain);
|
|
117
|
+
return {
|
|
118
|
+
domain,
|
|
119
|
+
target: proxy,
|
|
120
|
+
recordType: 'CNAME',
|
|
121
|
+
success: false,
|
|
122
|
+
error: `Invalid domain format: "${domain}" appears to be a URL. Use just the domain name: "${url.hostname}"`,
|
|
123
|
+
} as DNSError;
|
|
124
|
+
} catch {
|
|
125
|
+
return {
|
|
126
|
+
domain,
|
|
127
|
+
target: proxy,
|
|
128
|
+
recordType: 'CNAME',
|
|
129
|
+
success: false,
|
|
130
|
+
error: `Invalid domain format: "${domain}" appears to be a URL. Use just the domain name without the protocol (e.g., "example.com" not "https://example.com")`,
|
|
131
|
+
} as DNSError;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
113
135
|
try {
|
|
114
136
|
let timeoutId: Timer | undefined;
|
|
115
137
|
|