@agentuity/cli 0.0.42 → 0.0.43
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/bin/cli.ts +7 -5
- package/dist/cli.d.ts.map +1 -1
- package/dist/cmd/auth/index.d.ts.map +1 -1
- package/dist/cmd/auth/whoami.d.ts +2 -0
- package/dist/cmd/auth/whoami.d.ts.map +1 -0
- package/dist/cmd/bundle/index.d.ts +1 -1
- package/dist/cmd/bundle/index.d.ts.map +1 -1
- package/dist/cmd/cloud/deploy.d.ts +2 -0
- package/dist/cmd/cloud/deploy.d.ts.map +1 -0
- package/dist/cmd/cloud/index.d.ts +2 -0
- package/dist/cmd/cloud/index.d.ts.map +1 -0
- package/dist/cmd/dev/index.d.ts.map +1 -1
- package/dist/cmd/env/delete.d.ts +2 -0
- package/dist/cmd/env/delete.d.ts.map +1 -0
- package/dist/cmd/env/get.d.ts +2 -0
- package/dist/cmd/env/get.d.ts.map +1 -0
- package/dist/cmd/env/import.d.ts +2 -0
- package/dist/cmd/env/import.d.ts.map +1 -0
- package/dist/cmd/env/index.d.ts +2 -0
- package/dist/cmd/env/index.d.ts.map +1 -0
- package/dist/cmd/env/list.d.ts +2 -0
- package/dist/cmd/env/list.d.ts.map +1 -0
- package/dist/cmd/env/pull.d.ts +2 -0
- package/dist/cmd/env/pull.d.ts.map +1 -0
- package/dist/cmd/env/push.d.ts +2 -0
- package/dist/cmd/env/push.d.ts.map +1 -0
- package/dist/cmd/env/set.d.ts +2 -0
- package/dist/cmd/env/set.d.ts.map +1 -0
- package/dist/cmd/project/download.d.ts +1 -1
- package/dist/cmd/project/download.d.ts.map +1 -1
- package/dist/cmd/project/template-flow.d.ts +1 -1
- package/dist/cmd/project/template-flow.d.ts.map +1 -1
- package/dist/cmd/secret/delete.d.ts +2 -0
- package/dist/cmd/secret/delete.d.ts.map +1 -0
- package/dist/cmd/secret/get.d.ts +2 -0
- package/dist/cmd/secret/get.d.ts.map +1 -0
- package/dist/cmd/secret/import.d.ts +2 -0
- package/dist/cmd/secret/import.d.ts.map +1 -0
- package/dist/cmd/secret/index.d.ts +2 -0
- package/dist/cmd/secret/index.d.ts.map +1 -0
- package/dist/cmd/secret/list.d.ts +2 -0
- package/dist/cmd/secret/list.d.ts.map +1 -0
- package/dist/cmd/secret/pull.d.ts +2 -0
- package/dist/cmd/secret/pull.d.ts.map +1 -0
- package/dist/cmd/secret/push.d.ts +2 -0
- package/dist/cmd/secret/push.d.ts.map +1 -0
- package/dist/cmd/secret/set.d.ts +2 -0
- package/dist/cmd/secret/set.d.ts.map +1 -0
- package/dist/cmd/version/index.d.ts.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/env-util.d.ts +67 -0
- package/dist/env-util.d.ts.map +1 -0
- package/dist/env-util.test.d.ts +2 -0
- package/dist/env-util.test.d.ts.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/schema-parser.d.ts.map +1 -1
- package/dist/steps.d.ts.map +1 -1
- package/dist/tui.d.ts +1 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +45 -4
- package/src/cmd/auth/index.ts +2 -1
- package/src/cmd/auth/whoami.ts +69 -0
- package/src/cmd/bundle/index.ts +2 -2
- package/src/cmd/cloud/deploy.ts +129 -0
- package/src/cmd/cloud/index.ts +8 -0
- package/src/cmd/dev/index.ts +5 -3
- package/src/cmd/env/delete.ts +62 -0
- package/src/cmd/env/get.ts +66 -0
- package/src/cmd/env/import.ts +117 -0
- package/src/cmd/env/index.ts +22 -0
- package/src/cmd/env/list.ts +69 -0
- package/src/cmd/env/pull.ts +93 -0
- package/src/cmd/env/push.ts +55 -0
- package/src/cmd/env/set.ts +86 -0
- package/src/cmd/project/download.ts +1 -1
- package/src/cmd/project/template-flow.ts +42 -2
- package/src/cmd/secret/delete.ts +55 -0
- package/src/cmd/secret/get.ts +67 -0
- package/src/cmd/secret/import.ts +79 -0
- package/src/cmd/secret/index.ts +22 -0
- package/src/cmd/secret/list.ts +69 -0
- package/src/cmd/secret/pull.ts +91 -0
- package/src/cmd/secret/push.ts +55 -0
- package/src/cmd/secret/set.ts +60 -0
- package/src/cmd/version/index.ts +2 -1
- package/src/config.ts +35 -5
- package/src/env-util.test.ts +194 -0
- package/src/env-util.ts +290 -0
- package/src/index.ts +5 -1
- package/src/schema-parser.ts +2 -3
- package/src/steps.ts +79 -4
- package/src/tui.ts +18 -9
- package/src/types.ts +1 -1
- package/dist/logger.d.ts +0 -24
- package/dist/logger.d.ts.map +0 -1
- package/src/logger.ts +0 -235
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { createSubcommand } from '../../types';
|
|
3
|
+
import * as tui from '../../tui';
|
|
4
|
+
import { projectEnvUpdate } from '@agentuity/server';
|
|
5
|
+
import { getAPIBaseURL, APIClient } from '../../api';
|
|
6
|
+
import { loadProjectConfig } from '../../config';
|
|
7
|
+
import { findEnvFile, readEnvFile, writeEnvFile, filterAgentuitySdkKeys } from '../../env-util';
|
|
8
|
+
|
|
9
|
+
export const setSubcommand = createSubcommand({
|
|
10
|
+
name: 'set',
|
|
11
|
+
description: 'Set a secret',
|
|
12
|
+
requiresAuth: true,
|
|
13
|
+
schema: {
|
|
14
|
+
args: z.object({
|
|
15
|
+
key: z.string().min(1, 'key must not be empty').describe('the secret key'),
|
|
16
|
+
value: z.string().min(1, 'value must not be empty').describe('the secret value'),
|
|
17
|
+
}),
|
|
18
|
+
options: z.object({
|
|
19
|
+
dir: z.string().optional().describe('project directory (default: current directory)'),
|
|
20
|
+
}),
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
async handler(ctx) {
|
|
24
|
+
const { args, opts, config } = ctx;
|
|
25
|
+
const dir = opts?.dir ?? process.cwd();
|
|
26
|
+
|
|
27
|
+
// Validate key doesn't start with AGENTUITY_
|
|
28
|
+
if (args.key.startsWith('AGENTUITY_')) {
|
|
29
|
+
tui.fatal('Cannot set AGENTUITY_ prefixed variables. These are reserved for system use.');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Load project config to get project ID
|
|
33
|
+
const projectConfig = await loadProjectConfig(dir);
|
|
34
|
+
if (!projectConfig) {
|
|
35
|
+
tui.fatal(`No Agentuity project found in ${dir}. Missing agentuity.json`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const apiUrl = getAPIBaseURL(config);
|
|
39
|
+
const client = new APIClient(apiUrl, config);
|
|
40
|
+
|
|
41
|
+
// Set in cloud (using secrets field)
|
|
42
|
+
await tui.spinner('Setting secret in cloud', () => {
|
|
43
|
+
return projectEnvUpdate(client, {
|
|
44
|
+
id: projectConfig.projectId,
|
|
45
|
+
secrets: { [args.key]: args.value },
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Update local .env.production file
|
|
50
|
+
const envFilePath = await findEnvFile(dir);
|
|
51
|
+
const currentEnv = await readEnvFile(envFilePath);
|
|
52
|
+
currentEnv[args.key] = args.value;
|
|
53
|
+
|
|
54
|
+
// Filter out AGENTUITY_ keys before writing
|
|
55
|
+
const filteredEnv = filterAgentuitySdkKeys(currentEnv);
|
|
56
|
+
await writeEnvFile(envFilePath, filteredEnv);
|
|
57
|
+
|
|
58
|
+
tui.success(`Secret '${args.key}' set successfully (cloud + ${envFilePath})`);
|
|
59
|
+
},
|
|
60
|
+
});
|
package/src/cmd/version/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createCommand } from '../../types';
|
|
2
2
|
import { getVersion } from '../../version';
|
|
3
|
-
import {
|
|
3
|
+
import { createLogger } from '@agentuity/server';
|
|
4
4
|
|
|
5
5
|
export const command = createCommand({
|
|
6
6
|
name: 'version',
|
|
@@ -10,6 +10,7 @@ export const command = createCommand({
|
|
|
10
10
|
try {
|
|
11
11
|
console.log(getVersion());
|
|
12
12
|
} catch (error) {
|
|
13
|
+
const logger = createLogger();
|
|
13
14
|
logger.fatal('Failed to retrieve version: %s', error);
|
|
14
15
|
}
|
|
15
16
|
},
|
package/src/config.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { YAML } from 'bun';
|
|
2
|
-
import { join, extname, basename } from 'node:path';
|
|
2
|
+
import { join, extname, basename, resolve, normalize } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { mkdir, readdir, readFile, writeFile, chmod } from 'node:fs/promises';
|
|
5
5
|
import type { Config, Profile, AuthData } from './types';
|
|
@@ -189,8 +189,18 @@ export async function saveConfig(config: Config, customPath?: string): Promise<v
|
|
|
189
189
|
await chmod(configPath, 0o600);
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
async function getOrInitConfig(): Promise<Config> {
|
|
193
|
+
const config = await loadConfig();
|
|
194
|
+
if (config) {
|
|
195
|
+
return config;
|
|
196
|
+
}
|
|
197
|
+
const profilePath = await getProfile();
|
|
198
|
+
const name = basename(profilePath, '.yaml');
|
|
199
|
+
return { name };
|
|
200
|
+
}
|
|
201
|
+
|
|
192
202
|
export async function saveAuth(auth: AuthData): Promise<void> {
|
|
193
|
-
const config =
|
|
203
|
+
const config = await getOrInitConfig();
|
|
194
204
|
config.auth = {
|
|
195
205
|
api_key: auth.apiKey,
|
|
196
206
|
user_id: auth.userId,
|
|
@@ -202,7 +212,7 @@ export async function saveAuth(auth: AuthData): Promise<void> {
|
|
|
202
212
|
}
|
|
203
213
|
|
|
204
214
|
export async function clearAuth(): Promise<void> {
|
|
205
|
-
const config =
|
|
215
|
+
const config = await getOrInitConfig();
|
|
206
216
|
config.auth = {
|
|
207
217
|
api_key: '',
|
|
208
218
|
user_id: '',
|
|
@@ -213,6 +223,21 @@ export async function clearAuth(): Promise<void> {
|
|
|
213
223
|
await saveConfig(config);
|
|
214
224
|
}
|
|
215
225
|
|
|
226
|
+
export async function saveProjectDir(projectDir: string): Promise<void> {
|
|
227
|
+
const config = await getOrInitConfig();
|
|
228
|
+
config.preferences = config.preferences || {};
|
|
229
|
+
const normalized = resolve(normalize(projectDir));
|
|
230
|
+
(config.preferences as Record<string, unknown>).project_dir = normalized;
|
|
231
|
+
await saveConfig(config);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export async function saveOrgId(orgId: string): Promise<void> {
|
|
235
|
+
const config = await getOrInitConfig();
|
|
236
|
+
config.preferences = config.preferences || {};
|
|
237
|
+
(config.preferences as Record<string, unknown>).orgId = orgId;
|
|
238
|
+
await saveConfig(config);
|
|
239
|
+
}
|
|
240
|
+
|
|
216
241
|
export async function getAuth(): Promise<AuthData | null> {
|
|
217
242
|
const config = await loadConfig();
|
|
218
243
|
if (!config) return null;
|
|
@@ -357,9 +382,14 @@ export async function createProjectConfig(dir: string, config: InitialProjectCon
|
|
|
357
382
|
const configFile = Bun.file(configPath);
|
|
358
383
|
await configFile.write(JSON.stringify(sanitizedConfig, null, 2));
|
|
359
384
|
|
|
385
|
+
// Write SDK key to .env with comment
|
|
360
386
|
const envPath = join(dir, '.env');
|
|
361
|
-
const
|
|
362
|
-
|
|
387
|
+
const comment =
|
|
388
|
+
'# AGENTUITY_SDK_KEY is a sensitive value and should not be committed to version control.';
|
|
389
|
+
const content = `${comment}\nAGENTUITY_SDK_KEY=${apiKey}\n`;
|
|
390
|
+
await Bun.write(envPath, content);
|
|
391
|
+
// Set restrictive permissions (owner read/write only) to protect sensitive key
|
|
392
|
+
await chmod(envPath, 0o600);
|
|
363
393
|
}
|
|
364
394
|
|
|
365
395
|
export async function loadBuildMetadata(dir: string): Promise<BuildMetadata> {
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import { looksLikeSecret } from './env-util';
|
|
3
|
+
|
|
4
|
+
describe('looksLikeSecret', () => {
|
|
5
|
+
describe('key name patterns', () => {
|
|
6
|
+
test('detects _SECRET suffix', () => {
|
|
7
|
+
expect(looksLikeSecret('API_SECRET', 'value')).toBe(true);
|
|
8
|
+
expect(looksLikeSecret('DB_SECRET', 'value')).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('detects _KEY suffix', () => {
|
|
12
|
+
expect(looksLikeSecret('API_KEY', 'value')).toBe(true);
|
|
13
|
+
expect(looksLikeSecret('STRIPE_KEY', 'value')).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('detects _TOKEN suffix', () => {
|
|
17
|
+
expect(looksLikeSecret('AUTH_TOKEN', 'value')).toBe(true);
|
|
18
|
+
expect(looksLikeSecret('GITHUB_TOKEN', 'value')).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('detects _PASSWORD suffix', () => {
|
|
22
|
+
expect(looksLikeSecret('DB_PASSWORD', 'value')).toBe(true);
|
|
23
|
+
expect(looksLikeSecret('ADMIN_PASSWORD', 'value')).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('detects _PRIVATE suffix', () => {
|
|
27
|
+
expect(looksLikeSecret('SSH_PRIVATE', 'value')).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('detects _CERT and _CERTIFICATE suffixes', () => {
|
|
31
|
+
expect(looksLikeSecret('SSL_CERT', 'value')).toBe(true);
|
|
32
|
+
expect(looksLikeSecret('SSL_CERTIFICATE', 'value')).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('detects SECRET_ prefix', () => {
|
|
36
|
+
expect(looksLikeSecret('SECRET_VALUE', 'value')).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('detects APIKEY and API_KEY patterns', () => {
|
|
40
|
+
expect(looksLikeSecret('APIKEY', 'value')).toBe(true);
|
|
41
|
+
expect(looksLikeSecret('API_KEY', 'value')).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('detects JWT prefix', () => {
|
|
45
|
+
expect(looksLikeSecret('JWT_SECRET', 'value')).toBe(true);
|
|
46
|
+
expect(looksLikeSecret('JWT', 'value')).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('detects PASSWORD in key name', () => {
|
|
50
|
+
expect(looksLikeSecret('DATABASE_PASSWORD', 'value')).toBe(true);
|
|
51
|
+
expect(looksLikeSecret('PASSWORD', 'value')).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('detects CREDENTIAL in key name', () => {
|
|
55
|
+
expect(looksLikeSecret('AWS_CREDENTIALS', 'value')).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('detects AUTH.*KEY pattern', () => {
|
|
59
|
+
expect(looksLikeSecret('AUTH_API_KEY', 'value')).toBe(true);
|
|
60
|
+
expect(looksLikeSecret('AUTHKEY', 'value')).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('is case insensitive for key patterns', () => {
|
|
64
|
+
expect(looksLikeSecret('api_secret', 'value')).toBe(true);
|
|
65
|
+
expect(looksLikeSecret('Api_Key', 'value')).toBe(true);
|
|
66
|
+
expect(looksLikeSecret('AUTH_token', 'value')).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('value patterns', () => {
|
|
71
|
+
test('detects JWT tokens', () => {
|
|
72
|
+
const jwt =
|
|
73
|
+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
|
|
74
|
+
expect(looksLikeSecret('TOKEN', jwt)).toBe(true);
|
|
75
|
+
expect(looksLikeSecret('SOME_VAR', jwt)).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('detects Bearer tokens', () => {
|
|
79
|
+
expect(looksLikeSecret('AUTH', 'Bearer abc123def456ghi789jkl012mno345pqr')).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('detects AWS access keys', () => {
|
|
83
|
+
expect(looksLikeSecret('AWS', 'AKIAIOSFODNN7EXAMPLE')).toBe(true);
|
|
84
|
+
expect(looksLikeSecret('AWS', 'ASIAIOSFODNN7EXAMPLE')).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('detects GitHub tokens', () => {
|
|
88
|
+
expect(looksLikeSecret('GH', 'ghp_1234567890abcdefghijklmnopqrstuvwxyz')).toBe(true);
|
|
89
|
+
expect(looksLikeSecret('GH', 'ghs_1234567890abcdefghijklmnopqrstuvwxyz')).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('detects long alphanumeric strings (API keys)', () => {
|
|
93
|
+
// 32+ characters, mixed alphanumeric
|
|
94
|
+
expect(
|
|
95
|
+
looksLikeSecret('KEY', 'sk_test_51A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6')
|
|
96
|
+
).toBe(true);
|
|
97
|
+
expect(looksLikeSecret('KEY', 'abc123def456ghi789jkl012mno345pqr678stu901vwx234yz')).toBe(
|
|
98
|
+
true
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('does not flag numeric-only long strings', () => {
|
|
103
|
+
expect(looksLikeSecret('ID', '12345678901234567890123456789012')).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('detects PEM certificates', () => {
|
|
107
|
+
expect(looksLikeSecret('CERT', '-----BEGIN CERTIFICATE-----\nMIIC...')).toBe(true);
|
|
108
|
+
expect(looksLikeSecret('CERT', '-----BEGIN PRIVATE KEY-----\nMIIC...')).toBe(true);
|
|
109
|
+
expect(looksLikeSecret('CERT', '-----BEGIN RSA PRIVATE KEY-----\nMIIC...')).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('does not flag short values', () => {
|
|
113
|
+
expect(looksLikeSecret('VAR', 'short')).toBe(false);
|
|
114
|
+
expect(looksLikeSecret('VAR', '1234567')).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('does not flag empty values', () => {
|
|
118
|
+
expect(looksLikeSecret('VAR', '')).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('non-secret patterns', () => {
|
|
123
|
+
test('regular environment variables are not flagged', () => {
|
|
124
|
+
expect(looksLikeSecret('NODE_ENV', 'production')).toBe(false);
|
|
125
|
+
expect(looksLikeSecret('PORT', '3000')).toBe(false);
|
|
126
|
+
expect(looksLikeSecret('HOST', 'localhost')).toBe(false);
|
|
127
|
+
expect(looksLikeSecret('DATABASE_URL', 'postgres://localhost:5432/mydb')).toBe(false);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('configuration values are not flagged', () => {
|
|
131
|
+
expect(looksLikeSecret('LOG_LEVEL', 'debug')).toBe(false);
|
|
132
|
+
expect(looksLikeSecret('CACHE_TTL', '3600')).toBe(false);
|
|
133
|
+
expect(looksLikeSecret('MAX_CONNECTIONS', '100')).toBe(false);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('URLs without secrets are not flagged', () => {
|
|
137
|
+
expect(looksLikeSecret('API_URL', 'https://api.example.com')).toBe(false);
|
|
138
|
+
expect(looksLikeSecret('WEBHOOK_URL', 'https://example.com/webhook')).toBe(false);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('paths are not flagged', () => {
|
|
142
|
+
expect(looksLikeSecret('DATA_PATH', '/var/data/app')).toBe(false);
|
|
143
|
+
expect(looksLikeSecret('CONFIG_FILE', '/etc/app/config.json')).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('edge cases', () => {
|
|
148
|
+
test('handles mixed key and value patterns', () => {
|
|
149
|
+
// Key pattern triggers detection
|
|
150
|
+
expect(looksLikeSecret('API_KEY', 'simple')).toBe(true);
|
|
151
|
+
|
|
152
|
+
// Value pattern triggers detection even without key pattern
|
|
153
|
+
const jwt =
|
|
154
|
+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
|
|
155
|
+
expect(looksLikeSecret('CONFIG', jwt)).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('real-world API key formats', () => {
|
|
159
|
+
// Stripe (contains underscore, 32+ chars)
|
|
160
|
+
expect(looksLikeSecret('STRIPE', 'sk_test_51HqL7xAbCdEfGhIjK12345678901234567890')).toBe(
|
|
161
|
+
true
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// Long API key format (32+ alphanumeric)
|
|
165
|
+
expect(looksLikeSecret('OPENAI', 'sk-proj-1234567890abcdefghijklmnopqrstuvwxyz')).toBe(
|
|
166
|
+
true
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// Contains dots (periods not in our pattern, but key name helps)
|
|
170
|
+
expect(
|
|
171
|
+
looksLikeSecret(
|
|
172
|
+
'SENDGRID_API_KEY',
|
|
173
|
+
'SG.1234567890abcdefghijklmnopqrstuvwxyz.1234567890abcdefghijklmnopqrstuvwxyz'
|
|
174
|
+
)
|
|
175
|
+
).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('UUIDs are correctly identified as non-secrets', () => {
|
|
179
|
+
// Standard UUID format should not be flagged
|
|
180
|
+
expect(looksLikeSecret('REQUEST_ID', '550e8400-e29b-41d4-a716-446655440000')).toBe(false);
|
|
181
|
+
expect(looksLikeSecret('USER_ID', '123e4567-e89b-12d3-a456-426614174000')).toBe(false);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('hex hashes are flagged (better safe than sorry)', () => {
|
|
185
|
+
// 32+ character hex strings could be secrets or hashes - we flag them
|
|
186
|
+
// Users can confirm they're just hashes if needed
|
|
187
|
+
expect(looksLikeSecret('BUILD_HASH', 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6')).toBe(true);
|
|
188
|
+
|
|
189
|
+
// But with context that clearly indicates it's not a secret, the key name won't trigger
|
|
190
|
+
// So short hex strings without secret-like key names won't be flagged
|
|
191
|
+
expect(looksLikeSecret('COMMIT', 'abc123def')).toBe(false);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
});
|
package/src/env-util.ts
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for handling .env files
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
export interface EnvVars {
|
|
8
|
+
[key: string]: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Find the appropriate .env file to use for user environment variables.
|
|
13
|
+
* Always returns .env.production path (will be created if needed).
|
|
14
|
+
* .env should only contain AGENTUITY_SDK_KEY.
|
|
15
|
+
*/
|
|
16
|
+
export async function findEnvFile(dir: string): Promise<string> {
|
|
17
|
+
return join(dir, '.env.production');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Find an existing env file for reading.
|
|
22
|
+
* Preference: .env.production > .env
|
|
23
|
+
*/
|
|
24
|
+
export async function findExistingEnvFile(dir: string): Promise<string> {
|
|
25
|
+
const productionEnv = join(dir, '.env.production');
|
|
26
|
+
const defaultEnv = join(dir, '.env');
|
|
27
|
+
|
|
28
|
+
if (await Bun.file(productionEnv).exists()) {
|
|
29
|
+
return productionEnv;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return defaultEnv;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parse a single line from an .env file
|
|
37
|
+
* Handles comments, empty lines, and quoted values
|
|
38
|
+
*/
|
|
39
|
+
export function parseEnvLine(line: string): { key: string; value: string } | null {
|
|
40
|
+
const trimmed = line.trim();
|
|
41
|
+
|
|
42
|
+
// Skip empty lines and comments
|
|
43
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const equalIndex = trimmed.indexOf('=');
|
|
48
|
+
if (equalIndex === -1) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const key = trimmed.slice(0, equalIndex).trim();
|
|
53
|
+
let value = trimmed.slice(equalIndex + 1).trim();
|
|
54
|
+
|
|
55
|
+
// Remove surrounding quotes if present
|
|
56
|
+
if (
|
|
57
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
58
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
59
|
+
) {
|
|
60
|
+
value = value.slice(1, -1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { key, value };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Read and parse an .env file
|
|
68
|
+
*/
|
|
69
|
+
export async function readEnvFile(path: string): Promise<EnvVars> {
|
|
70
|
+
const file = Bun.file(path);
|
|
71
|
+
|
|
72
|
+
if (!(await file.exists())) {
|
|
73
|
+
return {};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const content = await file.text();
|
|
77
|
+
const lines = content.split('\n');
|
|
78
|
+
const env: EnvVars = {};
|
|
79
|
+
|
|
80
|
+
for (const line of lines) {
|
|
81
|
+
const parsed = parseEnvLine(line);
|
|
82
|
+
if (parsed) {
|
|
83
|
+
env[parsed.key] = parsed.value;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return env;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Write environment variables to an .env file
|
|
92
|
+
* Optionally skip certain keys (like AGENTUITY_SDK_KEY)
|
|
93
|
+
*/
|
|
94
|
+
export async function writeEnvFile(
|
|
95
|
+
path: string,
|
|
96
|
+
vars: EnvVars,
|
|
97
|
+
options?: {
|
|
98
|
+
skipKeys?: string[];
|
|
99
|
+
addComment?: (key: string) => string | null;
|
|
100
|
+
}
|
|
101
|
+
): Promise<void> {
|
|
102
|
+
const skipKeys = options?.skipKeys || [];
|
|
103
|
+
const lines: string[] = [];
|
|
104
|
+
|
|
105
|
+
// Sort keys for consistent output
|
|
106
|
+
const sortedKeys = Object.keys(vars).sort();
|
|
107
|
+
|
|
108
|
+
for (const key of sortedKeys) {
|
|
109
|
+
if (skipKeys.includes(key)) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const value = vars[key];
|
|
114
|
+
|
|
115
|
+
// Add comment if provided
|
|
116
|
+
if (options?.addComment) {
|
|
117
|
+
const comment = options.addComment(key);
|
|
118
|
+
if (comment) {
|
|
119
|
+
lines.push(`# ${comment}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Write key=value
|
|
124
|
+
lines.push(`${key}=${value}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const content = lines.join('\n') + '\n';
|
|
128
|
+
await Bun.write(path, content);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Merge environment variables with special handling
|
|
133
|
+
* - Later values override earlier values
|
|
134
|
+
* - Can filter out keys (like AGENTUITY_* keys)
|
|
135
|
+
*/
|
|
136
|
+
export function mergeEnvVars(
|
|
137
|
+
base: EnvVars,
|
|
138
|
+
updates: EnvVars,
|
|
139
|
+
options?: {
|
|
140
|
+
filterPrefix?: string;
|
|
141
|
+
}
|
|
142
|
+
): EnvVars {
|
|
143
|
+
const merged = { ...base };
|
|
144
|
+
const filterPrefix = options?.filterPrefix;
|
|
145
|
+
|
|
146
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
147
|
+
// Skip keys with filter prefix if specified
|
|
148
|
+
if (filterPrefix && key.startsWith(filterPrefix)) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
merged[key] = value;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return merged;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Filter out AGENTUITY_ prefixed keys from env vars
|
|
160
|
+
* This is used when pushing to the cloud to avoid sending SDK keys
|
|
161
|
+
*/
|
|
162
|
+
export function filterAgentuitySdkKeys(vars: EnvVars): EnvVars {
|
|
163
|
+
const filtered: EnvVars = {};
|
|
164
|
+
|
|
165
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
166
|
+
if (!key.startsWith('AGENTUITY_')) {
|
|
167
|
+
filtered[key] = value;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return filtered;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Split env vars into env and secrets based on key names
|
|
176
|
+
* Convention: Keys ending with _SECRET, _KEY, _TOKEN, _PASSWORD are secrets
|
|
177
|
+
*/
|
|
178
|
+
export function splitEnvAndSecrets(vars: EnvVars): {
|
|
179
|
+
env: EnvVars;
|
|
180
|
+
secrets: EnvVars;
|
|
181
|
+
} {
|
|
182
|
+
const env: EnvVars = {};
|
|
183
|
+
const secrets: EnvVars = {};
|
|
184
|
+
|
|
185
|
+
const secretSuffixes = ['_SECRET', '_KEY', '_TOKEN', '_PASSWORD', '_PRIVATE'];
|
|
186
|
+
|
|
187
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
188
|
+
// Skip AGENTUITY_ prefixed keys
|
|
189
|
+
if (key.startsWith('AGENTUITY_')) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const isSecret = secretSuffixes.some((suffix) => key.endsWith(suffix));
|
|
194
|
+
|
|
195
|
+
if (isSecret) {
|
|
196
|
+
secrets[key] = value;
|
|
197
|
+
} else {
|
|
198
|
+
env[key] = value;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return { env, secrets };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Mask a secret value for display
|
|
207
|
+
*/
|
|
208
|
+
export function maskSecret(value: string): string {
|
|
209
|
+
if (!value) {
|
|
210
|
+
return '';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (value.length <= 8) {
|
|
214
|
+
return '***';
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Show first 4 and last 4 characters
|
|
218
|
+
return `${value.slice(0, 4)}...${value.slice(-4)}`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Detect if a key or value looks like it should be a secret
|
|
223
|
+
*/
|
|
224
|
+
export function looksLikeSecret(key: string, value: string): boolean {
|
|
225
|
+
// Check key name for secret-like patterns
|
|
226
|
+
const secretKeyPatterns = [
|
|
227
|
+
/_SECRET$/i,
|
|
228
|
+
/_KEY$/i,
|
|
229
|
+
/_TOKEN$/i,
|
|
230
|
+
/_PASSWORD$/i,
|
|
231
|
+
/_PRIVATE$/i,
|
|
232
|
+
/_CERT$/i,
|
|
233
|
+
/_CERTIFICATE$/i,
|
|
234
|
+
/^SECRET_/i,
|
|
235
|
+
/^API_?KEY/i,
|
|
236
|
+
/^JWT/i,
|
|
237
|
+
/PASSWORD/i,
|
|
238
|
+
/CREDENTIAL/i,
|
|
239
|
+
/AUTH.*KEY/i,
|
|
240
|
+
];
|
|
241
|
+
|
|
242
|
+
const keyLooksSecret = secretKeyPatterns.some((pattern) => pattern.test(key));
|
|
243
|
+
if (keyLooksSecret) {
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Check value for secret-like patterns
|
|
248
|
+
if (!value || value.length < 8) {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// JWT pattern (header.payload.signature)
|
|
253
|
+
if (/^eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(value)) {
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Bearer token pattern
|
|
258
|
+
if (/^Bearer\s+[A-Za-z0-9_-]{20,}$/i.test(value)) {
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// AWS/Cloud provider key patterns
|
|
263
|
+
if (/^(AKIA|ASIA)[A-Z0-9]{16}$/.test(value)) {
|
|
264
|
+
// AWS access key
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// GitHub token patterns
|
|
269
|
+
if (/^gh[ps]_[A-Za-z0-9_]{36,}$/.test(value)) {
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Generic long alphanumeric strings (likely API keys)
|
|
274
|
+
// Exclude UUIDs (8-4-4-4-12 format) and simple alphanumeric IDs
|
|
275
|
+
const isUUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(value);
|
|
276
|
+
if (!isUUID && /^[A-Za-z0-9_-]{32,}$/.test(value) && !/^[0-9]+$/.test(value)) {
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// PEM-encoded certificates or private keys
|
|
281
|
+
if (
|
|
282
|
+
value.includes('BEGIN CERTIFICATE') ||
|
|
283
|
+
value.includes('BEGIN PRIVATE KEY') ||
|
|
284
|
+
value.includes('BEGIN RSA PRIVATE KEY')
|
|
285
|
+
) {
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return false;
|
|
290
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -17,7 +17,11 @@ export {
|
|
|
17
17
|
getAuth,
|
|
18
18
|
} from './config';
|
|
19
19
|
export { APIClient, getAPIBaseURL, getAppBaseURL } from './api';
|
|
20
|
-
export {
|
|
20
|
+
export {
|
|
21
|
+
ConsoleLogger,
|
|
22
|
+
createLogger,
|
|
23
|
+
type ColorScheme as LoggerColorScheme,
|
|
24
|
+
} from '@agentuity/server';
|
|
21
25
|
export { showBanner } from './banner';
|
|
22
26
|
export { discoverCommands } from './cmd';
|
|
23
27
|
export { detectColorScheme } from './terminal';
|
package/src/schema-parser.ts
CHANGED
|
@@ -203,9 +203,8 @@ export function buildValidationInput(
|
|
|
203
203
|
if (schemas.options) {
|
|
204
204
|
const parsed = parseOptionsSchema(schemas.options);
|
|
205
205
|
for (const opt of parsed) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
206
|
+
// Always include the option value (even if undefined) so zod can apply defaults
|
|
207
|
+
result.options[opt.name] = rawOptions[opt.name];
|
|
209
208
|
}
|
|
210
209
|
}
|
|
211
210
|
|