wiggum-cli 0.11.13 → 0.11.15
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/.claude/settings.local.json +10 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +9 -17
- package/dist/commands/config.js.map +1 -1
- package/dist/context/convert.d.ts +29 -0
- package/dist/context/convert.d.ts.map +1 -0
- package/dist/context/convert.js +123 -0
- package/dist/context/convert.js.map +1 -0
- package/dist/context/index.d.ts +4 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +3 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/storage.d.ts +28 -0
- package/dist/context/storage.d.ts.map +1 -0
- package/dist/context/storage.js +100 -0
- package/dist/context/storage.js.map +1 -0
- package/dist/context/types.d.ts +50 -0
- package/dist/context/types.d.ts.map +1 -0
- package/dist/context/types.js +6 -0
- package/dist/context/types.js.map +1 -0
- package/dist/repl/command-parser.d.ts +5 -0
- package/dist/repl/command-parser.d.ts.map +1 -1
- package/dist/repl/command-parser.js +5 -0
- package/dist/repl/command-parser.js.map +1 -1
- package/dist/templates/root/.gitignore.tmpl +2 -0
- package/dist/tui/hooks/index.d.ts +2 -0
- package/dist/tui/hooks/index.d.ts.map +1 -1
- package/dist/tui/hooks/index.js +1 -0
- package/dist/tui/hooks/index.js.map +1 -1
- package/dist/tui/hooks/useInit.js +1 -1
- package/dist/tui/hooks/useInit.js.map +1 -1
- package/dist/tui/hooks/useSync.d.ts +15 -0
- package/dist/tui/hooks/useSync.d.ts.map +1 -0
- package/dist/tui/hooks/useSync.js +52 -0
- package/dist/tui/hooks/useSync.js.map +1 -0
- package/dist/tui/screens/InitScreen.d.ts.map +1 -1
- package/dist/tui/screens/InitScreen.js +22 -26
- package/dist/tui/screens/InitScreen.js.map +1 -1
- package/dist/tui/screens/InterviewScreen.d.ts.map +1 -1
- package/dist/tui/screens/InterviewScreen.js +110 -84
- package/dist/tui/screens/InterviewScreen.js.map +1 -1
- package/dist/tui/screens/MainShell.d.ts.map +1 -1
- package/dist/tui/screens/MainShell.js +37 -2
- package/dist/tui/screens/MainShell.js.map +1 -1
- package/dist/utils/env.d.ts +16 -1
- package/dist/utils/env.d.ts.map +1 -1
- package/dist/utils/env.js +55 -4
- package/dist/utils/env.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/config.test.ts +225 -0
- package/src/commands/config.ts +9 -18
- package/src/context/convert.test.ts +129 -0
- package/src/context/convert.ts +159 -0
- package/src/context/index.ts +19 -0
- package/src/context/storage.test.ts +173 -0
- package/src/context/storage.ts +117 -0
- package/src/context/types.ts +52 -0
- package/src/repl/command-parser.ts +5 -0
- package/src/templates/root/.gitignore.tmpl +2 -0
- package/src/tui/hooks/index.ts +3 -0
- package/src/tui/hooks/useInit.ts +1 -1
- package/src/tui/hooks/useSync.ts +80 -0
- package/src/tui/screens/InitScreen.tsx +27 -27
- package/src/tui/screens/InterviewScreen.tsx +116 -75
- package/src/tui/screens/MainShell.tsx +39 -2
- package/src/utils/env.test.ts +220 -20
- package/src/utils/env.ts +60 -4
package/dist/utils/env.d.ts
CHANGED
|
@@ -11,12 +11,27 @@
|
|
|
11
11
|
* - Strips matching surrounding quotes (single or double) from values.
|
|
12
12
|
*/
|
|
13
13
|
export declare function parseEnvContent(content: string): Record<string, string>;
|
|
14
|
+
/**
|
|
15
|
+
* Write API keys to an env file, preserving existing content.
|
|
16
|
+
*
|
|
17
|
+
* - Merges keys into existing file content (preserves other keys).
|
|
18
|
+
* - Replaces existing key values if the key already exists.
|
|
19
|
+
* - Creates parent directories if they don't exist.
|
|
20
|
+
* - Creates the file if it doesn't exist.
|
|
21
|
+
* - Skips empty values (keys with empty strings are ignored).
|
|
22
|
+
*
|
|
23
|
+
* @param filePath - Absolute path to the .env.local file
|
|
24
|
+
* @param keys - Record of environment variable names to values
|
|
25
|
+
*/
|
|
26
|
+
export declare function writeKeysToEnvFile(filePath: string, keys: Record<string, string>): void;
|
|
14
27
|
/**
|
|
15
28
|
* Load known AI provider API keys from .ralph/.env.local into process.env.
|
|
16
29
|
*
|
|
30
|
+
* - Prefers .ralph/.env.local as the canonical source (if it exists).
|
|
31
|
+
* - Falls back to root .env.local if .ralph/.env.local does not exist (backward compatibility).
|
|
17
32
|
* - Only keys in KNOWN_API_KEYS are loaded; all others are ignored.
|
|
18
33
|
* - File values override existing process.env values (file takes precedence).
|
|
19
|
-
* - If
|
|
34
|
+
* - If neither file exists or cannot be read, this is a silent no-op.
|
|
20
35
|
* - Malformed lines are skipped without aborting.
|
|
21
36
|
*/
|
|
22
37
|
export declare function loadApiKeysFromEnvLocal(): void;
|
package/dist/utils/env.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../src/utils/env.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAOH;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA2BvE;AAED
|
|
1
|
+
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../src/utils/env.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAOH;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA2BvE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CA8BvF;AAED;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,IAAI,IAAI,CA0B9C"}
|
package/dist/utils/env.js
CHANGED
|
@@ -39,18 +39,69 @@ export function parseEnvContent(content) {
|
|
|
39
39
|
}
|
|
40
40
|
return result;
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Write API keys to an env file, preserving existing content.
|
|
44
|
+
*
|
|
45
|
+
* - Merges keys into existing file content (preserves other keys).
|
|
46
|
+
* - Replaces existing key values if the key already exists.
|
|
47
|
+
* - Creates parent directories if they don't exist.
|
|
48
|
+
* - Creates the file if it doesn't exist.
|
|
49
|
+
* - Skips empty values (keys with empty strings are ignored).
|
|
50
|
+
*
|
|
51
|
+
* @param filePath - Absolute path to the .env.local file
|
|
52
|
+
* @param keys - Record of environment variable names to values
|
|
53
|
+
*/
|
|
54
|
+
export function writeKeysToEnvFile(filePath, keys) {
|
|
55
|
+
// Ensure parent directory exists
|
|
56
|
+
const dir = path.dirname(filePath);
|
|
57
|
+
if (!fs.existsSync(dir)) {
|
|
58
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
59
|
+
}
|
|
60
|
+
// Read existing content if file exists
|
|
61
|
+
let envContent = '';
|
|
62
|
+
if (fs.existsSync(filePath)) {
|
|
63
|
+
envContent = fs.readFileSync(filePath, 'utf-8');
|
|
64
|
+
}
|
|
65
|
+
// Merge keys into content
|
|
66
|
+
const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
67
|
+
for (const [envVar, value] of Object.entries(keys)) {
|
|
68
|
+
if (!value)
|
|
69
|
+
continue; // Skip empty values
|
|
70
|
+
const keyRegex = new RegExp(`^${escapeRegex(envVar)}=.*$`, 'm');
|
|
71
|
+
if (keyRegex.test(envContent)) {
|
|
72
|
+
// Replace existing key
|
|
73
|
+
envContent = envContent.replace(keyRegex, `${envVar}=${value}`);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
// Append new key
|
|
77
|
+
envContent = envContent.trimEnd() + (envContent ? '\n' : '') + `${envVar}=${value}\n`;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
fs.writeFileSync(filePath, envContent);
|
|
81
|
+
}
|
|
42
82
|
/**
|
|
43
83
|
* Load known AI provider API keys from .ralph/.env.local into process.env.
|
|
44
84
|
*
|
|
85
|
+
* - Prefers .ralph/.env.local as the canonical source (if it exists).
|
|
86
|
+
* - Falls back to root .env.local if .ralph/.env.local does not exist (backward compatibility).
|
|
45
87
|
* - Only keys in KNOWN_API_KEYS are loaded; all others are ignored.
|
|
46
88
|
* - File values override existing process.env values (file takes precedence).
|
|
47
|
-
* - If
|
|
89
|
+
* - If neither file exists or cannot be read, this is a silent no-op.
|
|
48
90
|
* - Malformed lines are skipped without aborting.
|
|
49
91
|
*/
|
|
50
92
|
export function loadApiKeysFromEnvLocal() {
|
|
51
93
|
try {
|
|
52
|
-
const
|
|
53
|
-
|
|
94
|
+
const ralphEnvPath = path.join(process.cwd(), '.ralph', '.env.local');
|
|
95
|
+
const rootEnvPath = path.join(process.cwd(), '.env.local');
|
|
96
|
+
// Prefer .ralph/.env.local, fall back to root .env.local
|
|
97
|
+
let envPath = null;
|
|
98
|
+
if (fs.existsSync(ralphEnvPath)) {
|
|
99
|
+
envPath = ralphEnvPath;
|
|
100
|
+
}
|
|
101
|
+
else if (fs.existsSync(rootEnvPath)) {
|
|
102
|
+
envPath = rootEnvPath;
|
|
103
|
+
}
|
|
104
|
+
if (!envPath)
|
|
54
105
|
return;
|
|
55
106
|
const content = fs.readFileSync(envPath, 'utf8');
|
|
56
107
|
const parsed = parseEnvContent(content);
|
|
@@ -61,7 +112,7 @@ export function loadApiKeysFromEnvLocal() {
|
|
|
61
112
|
}
|
|
62
113
|
}
|
|
63
114
|
catch (err) {
|
|
64
|
-
logger.debug(`Failed to load
|
|
115
|
+
logger.debug(`Failed to load env file: ${err instanceof Error ? err.message : String(err)}`);
|
|
65
116
|
}
|
|
66
117
|
}
|
|
67
118
|
//# sourceMappingURL=env.js.map
|
package/dist/utils/env.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env.js","sourceRoot":"","sources":["../../src/utils/env.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,OAAe;IAC7C,MAAM,MAAM,GAA2B,EAAE,CAAC;IAE1C,KAAK,MAAM,OAAO,IAAI,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;QAC7C,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS;QAE5C,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC9B,IAAI,GAAG,KAAK,CAAC,CAAC;YAAE,SAAS;QAEzB,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;QACtC,IAAI,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAEvC,oCAAoC;QACpC,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;YACtB,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;YACvB,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YACrC,IAAI,CAAC,KAAK,KAAK,GAAG,IAAI,IAAI,KAAK,GAAG,CAAC,IAAI,CAAC,KAAK,KAAK,GAAG,IAAI,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;gBACvE,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YAC7B,CAAC;QACH,CAAC;QAED,IAAI,CAAC,GAAG;YAAE,SAAS;QACnB,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;IACtB,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED
|
|
1
|
+
{"version":3,"file":"env.js","sourceRoot":"","sources":["../../src/utils/env.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,OAAe;IAC7C,MAAM,MAAM,GAA2B,EAAE,CAAC;IAE1C,KAAK,MAAM,OAAO,IAAI,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;QAC7C,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS;QAE5C,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC9B,IAAI,GAAG,KAAK,CAAC,CAAC;YAAE,SAAS;QAEzB,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;QACtC,IAAI,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAEvC,oCAAoC;QACpC,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;YACtB,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;YACvB,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YACrC,IAAI,CAAC,KAAK,KAAK,GAAG,IAAI,IAAI,KAAK,GAAG,CAAC,IAAI,CAAC,KAAK,KAAK,GAAG,IAAI,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;gBACvE,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YAC7B,CAAC;QACH,CAAC;QAED,IAAI,CAAC,GAAG;YAAE,SAAS;QACnB,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;IACtB,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,kBAAkB,CAAC,QAAgB,EAAE,IAA4B;IAC/E,iCAAiC;IACjC,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACnC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACxB,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACzC,CAAC;IAED,uCAAuC;IACvC,IAAI,UAAU,GAAG,EAAE,CAAC;IACpB,IAAI,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC5B,UAAU,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAClD,CAAC;IAED,0BAA0B;IAC1B,MAAM,WAAW,GAAG,CAAC,KAAa,EAAU,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;IAE5F,KAAK,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACnD,IAAI,CAAC,KAAK;YAAE,SAAS,CAAC,oBAAoB;QAE1C,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAC,IAAI,WAAW,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAChE,IAAI,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;YAC9B,uBAAuB;YACvB,UAAU,GAAG,UAAU,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,MAAM,IAAI,KAAK,EAAE,CAAC,CAAC;QAClE,CAAC;aAAM,CAAC;YACN,iBAAiB;YACjB,UAAU,GAAG,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,GAAG,MAAM,IAAI,KAAK,IAAI,CAAC;QACxF,CAAC;IACH,CAAC;IAED,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;AACzC,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,uBAAuB;IACrC,IAAI,CAAC;QACH,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,YAAY,CAAC,CAAC;QACtE,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,YAAY,CAAC,CAAC;QAE3D,yDAAyD;QACzD,IAAI,OAAO,GAAkB,IAAI,CAAC;QAClC,IAAI,EAAE,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;YAChC,OAAO,GAAG,YAAY,CAAC;QACzB,CAAC;aAAM,IAAI,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;YACtC,OAAO,GAAG,WAAW,CAAC;QACxB,CAAC;QAED,IAAI,CAAC,OAAO;YAAE,OAAO;QAErB,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QACjD,MAAM,MAAM,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;QAExC,KAAK,MAAM,GAAG,IAAI,cAAc,EAAE,CAAC;YACjC,IAAI,MAAM,CAAC,GAAG,CAAC,KAAK,SAAS,IAAI,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,CAAC;gBACpD,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;YACjC,CAAC;QACH,CAAC;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,KAAK,CAAC,4BAA4B,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC/F,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { handleConfigCommand } from './config.js';
|
|
5
|
+
import type { SessionState } from '../repl/session-state.js';
|
|
6
|
+
|
|
7
|
+
// Mock logger to suppress console output during tests
|
|
8
|
+
vi.mock('../utils/logger.js', () => ({
|
|
9
|
+
logger: {
|
|
10
|
+
error: vi.fn(),
|
|
11
|
+
success: vi.fn(),
|
|
12
|
+
},
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
// Mock colors to avoid picocolors output during tests
|
|
16
|
+
vi.mock('../utils/colors.js', () => ({
|
|
17
|
+
simpson: {
|
|
18
|
+
yellow: (s: string) => s,
|
|
19
|
+
},
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
describe('handleConfigCommand - init guard', () => {
|
|
23
|
+
const mockState: SessionState = {
|
|
24
|
+
projectRoot: '/fake/project',
|
|
25
|
+
provider: 'openai',
|
|
26
|
+
model: 'gpt-4',
|
|
27
|
+
conversationHistory: [],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
vi.restoreAllMocks();
|
|
32
|
+
// Clear process.env
|
|
33
|
+
delete process.env.TAVILY_API_KEY;
|
|
34
|
+
delete process.env.CONTEXT7_API_KEY;
|
|
35
|
+
delete process.env.BRAINTRUST_API_KEY;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('throws error when .ralph/ does not exist', async () => {
|
|
39
|
+
const ralphDir = path.join(mockState.projectRoot, '.ralph');
|
|
40
|
+
|
|
41
|
+
vi.spyOn(fs, 'existsSync').mockImplementation((p) => {
|
|
42
|
+
// .ralph/ directory does not exist
|
|
43
|
+
return p !== ralphDir;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const writeSpy = vi.spyOn(fs, 'writeFileSync');
|
|
47
|
+
const mkdirSpy = vi.spyOn(fs, 'mkdirSync');
|
|
48
|
+
|
|
49
|
+
const args = ['set', 'tavily', 'tvly-test-key-1234567890'];
|
|
50
|
+
|
|
51
|
+
await handleConfigCommand(args, mockState);
|
|
52
|
+
|
|
53
|
+
// Should not create .ralph/ directory
|
|
54
|
+
expect(mkdirSpy).not.toHaveBeenCalled();
|
|
55
|
+
|
|
56
|
+
// Should not write to .env.local
|
|
57
|
+
expect(writeSpy).not.toHaveBeenCalled();
|
|
58
|
+
|
|
59
|
+
// Should not set environment variable
|
|
60
|
+
expect(process.env.TAVILY_API_KEY).toBeUndefined();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('writes to .ralph/.env.local when .ralph/ exists', async () => {
|
|
64
|
+
const ralphDir = path.join(mockState.projectRoot, '.ralph');
|
|
65
|
+
const envLocalPath = path.join(ralphDir, '.env.local');
|
|
66
|
+
|
|
67
|
+
vi.spyOn(fs, 'existsSync').mockImplementation((p) => {
|
|
68
|
+
// .ralph/ directory exists
|
|
69
|
+
if (p === ralphDir) return true;
|
|
70
|
+
// .env.local does not exist yet
|
|
71
|
+
if (p === envLocalPath) return false;
|
|
72
|
+
return false;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
vi.spyOn(fs, 'statSync').mockReturnValue({
|
|
76
|
+
isDirectory: () => true,
|
|
77
|
+
} as any);
|
|
78
|
+
|
|
79
|
+
vi.spyOn(fs, 'readFileSync').mockReturnValue('');
|
|
80
|
+
vi.spyOn(fs, 'mkdirSync').mockReturnValue(undefined);
|
|
81
|
+
const writeSpy = vi.spyOn(fs, 'writeFileSync').mockReturnValue(undefined);
|
|
82
|
+
|
|
83
|
+
const args = ['set', 'tavily', 'tvly-test-key-1234567890'];
|
|
84
|
+
|
|
85
|
+
await handleConfigCommand(args, mockState);
|
|
86
|
+
|
|
87
|
+
// Should write to .ralph/.env.local
|
|
88
|
+
expect(writeSpy).toHaveBeenCalledWith(
|
|
89
|
+
envLocalPath,
|
|
90
|
+
'TAVILY_API_KEY=tvly-test-key-1234567890\n'
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// Should set environment variable
|
|
94
|
+
expect(process.env.TAVILY_API_KEY).toBe('tvly-test-key-1234567890');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('merges new key into existing .ralph/.env.local', async () => {
|
|
98
|
+
const ralphDir = path.join(mockState.projectRoot, '.ralph');
|
|
99
|
+
const envLocalPath = path.join(ralphDir, '.env.local');
|
|
100
|
+
const existingContent = 'CONTEXT7_API_KEY=c7-existing-123\n';
|
|
101
|
+
|
|
102
|
+
vi.spyOn(fs, 'existsSync').mockImplementation((p) => {
|
|
103
|
+
// Both .ralph/ and .env.local exist
|
|
104
|
+
if (p === ralphDir) return true;
|
|
105
|
+
if (p === envLocalPath) return true;
|
|
106
|
+
return false;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
vi.spyOn(fs, 'statSync').mockReturnValue({
|
|
110
|
+
isDirectory: () => true,
|
|
111
|
+
} as any);
|
|
112
|
+
|
|
113
|
+
vi.spyOn(fs, 'readFileSync').mockReturnValue(existingContent);
|
|
114
|
+
const writeSpy = vi.spyOn(fs, 'writeFileSync').mockReturnValue(undefined);
|
|
115
|
+
|
|
116
|
+
const args = ['set', 'tavily', 'tvly-test-key-1234567890'];
|
|
117
|
+
|
|
118
|
+
await handleConfigCommand(args, mockState);
|
|
119
|
+
|
|
120
|
+
const writtenContent = (writeSpy as any).mock.calls[0][1];
|
|
121
|
+
|
|
122
|
+
// Should preserve existing key
|
|
123
|
+
expect(writtenContent).toContain('CONTEXT7_API_KEY=c7-existing-123');
|
|
124
|
+
|
|
125
|
+
// Should add new key
|
|
126
|
+
expect(writtenContent).toContain('TAVILY_API_KEY=tvly-test-key-1234567890');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('replaces existing key value in .ralph/.env.local', async () => {
|
|
130
|
+
const ralphDir = path.join(mockState.projectRoot, '.ralph');
|
|
131
|
+
const envLocalPath = path.join(ralphDir, '.env.local');
|
|
132
|
+
const existingContent = 'TAVILY_API_KEY=old-key-value\n';
|
|
133
|
+
|
|
134
|
+
vi.spyOn(fs, 'existsSync').mockImplementation((p) => {
|
|
135
|
+
if (p === ralphDir) return true;
|
|
136
|
+
if (p === envLocalPath) return true;
|
|
137
|
+
return false;
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
vi.spyOn(fs, 'statSync').mockReturnValue({
|
|
141
|
+
isDirectory: () => true,
|
|
142
|
+
} as any);
|
|
143
|
+
|
|
144
|
+
vi.spyOn(fs, 'readFileSync').mockReturnValue(existingContent);
|
|
145
|
+
const writeSpy = vi.spyOn(fs, 'writeFileSync').mockReturnValue(undefined);
|
|
146
|
+
|
|
147
|
+
const args = ['set', 'tavily', 'tvly-new-key-9876543210'];
|
|
148
|
+
|
|
149
|
+
await handleConfigCommand(args, mockState);
|
|
150
|
+
|
|
151
|
+
const writtenContent = (writeSpy as any).mock.calls[0][1];
|
|
152
|
+
|
|
153
|
+
// Should replace old value
|
|
154
|
+
expect(writtenContent).not.toContain('old-key-value');
|
|
155
|
+
expect(writtenContent).toContain('TAVILY_API_KEY=tvly-new-key-9876543210');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('validates API key length', async () => {
|
|
159
|
+
const ralphDir = path.join(mockState.projectRoot, '.ralph');
|
|
160
|
+
|
|
161
|
+
vi.spyOn(fs, 'existsSync').mockReturnValue(true);
|
|
162
|
+
vi.spyOn(fs, 'statSync').mockReturnValue({
|
|
163
|
+
isDirectory: () => true,
|
|
164
|
+
} as any);
|
|
165
|
+
|
|
166
|
+
const writeSpy = vi.spyOn(fs, 'writeFileSync');
|
|
167
|
+
|
|
168
|
+
// Key too short (< 10 chars)
|
|
169
|
+
const args = ['set', 'tavily', 'short'];
|
|
170
|
+
|
|
171
|
+
await handleConfigCommand(args, mockState);
|
|
172
|
+
|
|
173
|
+
// Should not write
|
|
174
|
+
expect(writeSpy).not.toHaveBeenCalled();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('rejects unknown service names', async () => {
|
|
178
|
+
const ralphDir = path.join(mockState.projectRoot, '.ralph');
|
|
179
|
+
|
|
180
|
+
vi.spyOn(fs, 'existsSync').mockReturnValue(true);
|
|
181
|
+
vi.spyOn(fs, 'statSync').mockReturnValue({
|
|
182
|
+
isDirectory: () => true,
|
|
183
|
+
} as any);
|
|
184
|
+
|
|
185
|
+
const writeSpy = vi.spyOn(fs, 'writeFileSync');
|
|
186
|
+
|
|
187
|
+
const args = ['set', 'unknown-service', 'some-key-1234567890'];
|
|
188
|
+
|
|
189
|
+
await handleConfigCommand(args, mockState);
|
|
190
|
+
|
|
191
|
+
// Should not write
|
|
192
|
+
expect(writeSpy).not.toHaveBeenCalled();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('handles all supported services', async () => {
|
|
196
|
+
const ralphDir = path.join(mockState.projectRoot, '.ralph');
|
|
197
|
+
const envLocalPath = path.join(ralphDir, '.env.local');
|
|
198
|
+
|
|
199
|
+
vi.spyOn(fs, 'existsSync').mockImplementation((p) => {
|
|
200
|
+
if (p === ralphDir) return true;
|
|
201
|
+
if (p === envLocalPath) return false;
|
|
202
|
+
return false;
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
vi.spyOn(fs, 'statSync').mockReturnValue({
|
|
206
|
+
isDirectory: () => true,
|
|
207
|
+
} as any);
|
|
208
|
+
|
|
209
|
+
vi.spyOn(fs, 'readFileSync').mockReturnValue('');
|
|
210
|
+
vi.spyOn(fs, 'mkdirSync').mockReturnValue(undefined);
|
|
211
|
+
const writeSpy = vi.spyOn(fs, 'writeFileSync').mockReturnValue(undefined);
|
|
212
|
+
|
|
213
|
+
// Test tavily
|
|
214
|
+
await handleConfigCommand(['set', 'tavily', 'tvly-key-1234567890'], mockState);
|
|
215
|
+
expect(writeSpy).toHaveBeenLastCalledWith(envLocalPath, 'TAVILY_API_KEY=tvly-key-1234567890\n');
|
|
216
|
+
|
|
217
|
+
// Test context7
|
|
218
|
+
await handleConfigCommand(['set', 'context7', 'c7-key-1234567890'], mockState);
|
|
219
|
+
expect(writeSpy).toHaveBeenLastCalledWith(envLocalPath, 'CONTEXT7_API_KEY=c7-key-1234567890\n');
|
|
220
|
+
|
|
221
|
+
// Test braintrust
|
|
222
|
+
await handleConfigCommand(['set', 'braintrust', 'bt-key-1234567890'], mockState);
|
|
223
|
+
expect(writeSpy).toHaveBeenLastCalledWith(envLocalPath, 'BRAINTRUST_API_KEY=bt-key-1234567890\n');
|
|
224
|
+
});
|
|
225
|
+
});
|
package/src/commands/config.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { logger } from '../utils/logger.js';
|
|
|
10
10
|
import { simpson } from '../utils/colors.js';
|
|
11
11
|
import type { SessionState } from '../repl/session-state.js';
|
|
12
12
|
import { getAvailableProvider, AVAILABLE_MODELS } from '../ai/providers.js';
|
|
13
|
+
import { writeKeysToEnvFile } from '../utils/env.js';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Supported services for API key configuration
|
|
@@ -40,27 +41,17 @@ function isServiceConfigured(service: ConfigurableService): boolean {
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
/**
|
|
43
|
-
* Save an API key to .env.local file
|
|
44
|
+
* Save an API key to .ralph/.env.local file
|
|
44
45
|
*/
|
|
45
46
|
function saveKeyToEnvLocal(projectRoot: string, envVar: string, value: string): void {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (fs.existsSync(envLocalPath)) {
|
|
51
|
-
envContent = fs.readFileSync(envLocalPath, 'utf-8');
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const keyRegex = new RegExp(`^${envVar}=.*$`, 'm');
|
|
55
|
-
if (keyRegex.test(envContent)) {
|
|
56
|
-
// Replace existing key
|
|
57
|
-
envContent = envContent.replace(keyRegex, `${envVar}=${value}`);
|
|
58
|
-
} else {
|
|
59
|
-
// Append new key
|
|
60
|
-
envContent = envContent.trimEnd() + (envContent ? '\n' : '') + `${envVar}=${value}\n`;
|
|
47
|
+
// Check that .ralph/ exists (project is initialized)
|
|
48
|
+
const ralphDir = path.join(projectRoot, '.ralph');
|
|
49
|
+
if (!fs.existsSync(ralphDir) || !fs.statSync(ralphDir).isDirectory()) {
|
|
50
|
+
throw new Error('This project is not initialized. Run \'ralph init\' to set up .ralph/ before using \'ralph config set\'.');
|
|
61
51
|
}
|
|
62
52
|
|
|
63
|
-
|
|
53
|
+
const envLocalPath = path.join(ralphDir, '.env.local');
|
|
54
|
+
writeKeysToEnvFile(envLocalPath, { [envVar]: value });
|
|
64
55
|
}
|
|
65
56
|
|
|
66
57
|
/**
|
|
@@ -160,7 +151,7 @@ export async function handleConfigCommand(
|
|
|
160
151
|
// Also set in current process environment
|
|
161
152
|
process.env[envVar] = apiKey;
|
|
162
153
|
|
|
163
|
-
logger.success(`${envVar} saved to .env.local`);
|
|
154
|
+
logger.success(`${envVar} saved to .ralph/.env.local`);
|
|
164
155
|
console.log(pc.dim('Restart Wiggum to apply changes to tool availability.'));
|
|
165
156
|
console.log('');
|
|
166
157
|
} catch (error) {
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// src/context/convert.test.ts
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import {
|
|
4
|
+
toPersistedScanResult,
|
|
5
|
+
toPersistedAIAnalysis,
|
|
6
|
+
toScanResultFromPersisted,
|
|
7
|
+
} from './convert.js';
|
|
8
|
+
import type { ScanResult } from '../scanner/types.js';
|
|
9
|
+
import type { AIAnalysisResult } from '../ai/enhancer.js';
|
|
10
|
+
|
|
11
|
+
describe('context/convert', () => {
|
|
12
|
+
describe('toPersistedScanResult', () => {
|
|
13
|
+
it('maps DetectedStack fields to flat persisted format', () => {
|
|
14
|
+
const scanResult: ScanResult = {
|
|
15
|
+
projectRoot: '/tmp/test',
|
|
16
|
+
scanTime: 100,
|
|
17
|
+
stack: {
|
|
18
|
+
framework: { name: 'Next.js', version: '14.0.0', variant: 'app-router', confidence: 95, evidence: [] },
|
|
19
|
+
packageManager: { name: 'pnpm', confidence: 90, evidence: [] },
|
|
20
|
+
testing: {
|
|
21
|
+
unit: { name: 'Vitest', confidence: 85, evidence: [] },
|
|
22
|
+
e2e: { name: 'Playwright', confidence: 80, evidence: [] },
|
|
23
|
+
},
|
|
24
|
+
styling: { name: 'Tailwind CSS', confidence: 90, evidence: [] },
|
|
25
|
+
database: { name: 'Supabase', confidence: 75, evidence: [] },
|
|
26
|
+
orm: { name: 'Prisma', confidence: 70, evidence: [] },
|
|
27
|
+
auth: { name: 'NextAuth', confidence: 65, evidence: [] },
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const result = toPersistedScanResult(scanResult);
|
|
32
|
+
|
|
33
|
+
expect(result.framework).toBe('Next.js');
|
|
34
|
+
expect(result.frameworkVersion).toBe('14.0.0');
|
|
35
|
+
expect(result.frameworkVariant).toBe('app-router');
|
|
36
|
+
expect(result.packageManager).toBe('pnpm');
|
|
37
|
+
expect(result.testing?.unit).toBe('Vitest');
|
|
38
|
+
expect(result.testing?.e2e).toBe('Playwright');
|
|
39
|
+
expect(result.styling).toBe('Tailwind CSS');
|
|
40
|
+
expect(result.database).toBe('Supabase');
|
|
41
|
+
expect(result.orm).toBe('Prisma');
|
|
42
|
+
expect(result.auth).toBe('NextAuth');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('handles missing optional fields gracefully', () => {
|
|
46
|
+
const scanResult: ScanResult = {
|
|
47
|
+
projectRoot: '/tmp/test',
|
|
48
|
+
scanTime: 50,
|
|
49
|
+
stack: {},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const result = toPersistedScanResult(scanResult);
|
|
53
|
+
|
|
54
|
+
expect(result.framework).toBeUndefined();
|
|
55
|
+
expect(result.packageManager).toBeUndefined();
|
|
56
|
+
expect(result.testing?.unit).toBeNull();
|
|
57
|
+
expect(result.testing?.e2e).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('toPersistedAIAnalysis', () => {
|
|
62
|
+
it('maps AIAnalysisResult to persisted format', () => {
|
|
63
|
+
const analysis: AIAnalysisResult = {
|
|
64
|
+
projectContext: {
|
|
65
|
+
entryPoints: ['src/index.ts'],
|
|
66
|
+
keyDirectories: { 'src/api': 'API routes' },
|
|
67
|
+
namingConventions: 'camelCase',
|
|
68
|
+
},
|
|
69
|
+
commands: { test: 'npm test', build: 'npm run build' },
|
|
70
|
+
implementationGuidelines: ['Use TypeScript strict mode'],
|
|
71
|
+
technologyPractices: {
|
|
72
|
+
projectType: 'Web App',
|
|
73
|
+
practices: ['SSR first'],
|
|
74
|
+
antiPatterns: ['No inline styles'],
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const result = toPersistedAIAnalysis(analysis);
|
|
79
|
+
|
|
80
|
+
expect(result.projectContext?.entryPoints).toEqual(['src/index.ts']);
|
|
81
|
+
expect(result.projectContext?.keyDirectories).toEqual({ 'src/api': 'API routes' });
|
|
82
|
+
expect(result.commands?.test).toBe('npm test');
|
|
83
|
+
expect(result.implementationGuidelines).toEqual(['Use TypeScript strict mode']);
|
|
84
|
+
expect(result.technologyPractices?.projectType).toBe('Web App');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('returns empty object for undefined analysis', () => {
|
|
88
|
+
const result = toPersistedAIAnalysis(undefined);
|
|
89
|
+
expect(result).toEqual({});
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('toScanResultFromPersisted', () => {
|
|
94
|
+
it('rehydrates minimal ScanResult for codebase summary', () => {
|
|
95
|
+
const persisted = {
|
|
96
|
+
framework: 'Next.js',
|
|
97
|
+
frameworkVersion: '14.0.0',
|
|
98
|
+
frameworkVariant: 'app-router',
|
|
99
|
+
packageManager: 'pnpm',
|
|
100
|
+
testing: { unit: 'Vitest', e2e: 'Playwright' },
|
|
101
|
+
styling: 'Tailwind CSS',
|
|
102
|
+
database: 'Supabase',
|
|
103
|
+
orm: 'Prisma',
|
|
104
|
+
auth: 'NextAuth',
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const scan = toScanResultFromPersisted(persisted, '/tmp/project');
|
|
108
|
+
|
|
109
|
+
expect(scan.projectRoot).toBe('/tmp/project');
|
|
110
|
+
expect(scan.stack.framework?.name).toBe('Next.js');
|
|
111
|
+
expect(scan.stack.framework?.version).toBe('14.0.0');
|
|
112
|
+
expect(scan.stack.framework?.variant).toBe('app-router');
|
|
113
|
+
expect(scan.stack.packageManager?.name).toBe('pnpm');
|
|
114
|
+
expect(scan.stack.testing?.unit?.name).toBe('Vitest');
|
|
115
|
+
expect(scan.stack.testing?.e2e?.name).toBe('Playwright');
|
|
116
|
+
expect(scan.stack.styling?.name).toBe('Tailwind CSS');
|
|
117
|
+
expect(scan.stack.database?.name).toBe('Supabase');
|
|
118
|
+
expect(scan.stack.orm?.name).toBe('Prisma');
|
|
119
|
+
expect(scan.stack.auth?.name).toBe('NextAuth');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('handles empty persisted fields without crashing', () => {
|
|
123
|
+
const scan = toScanResultFromPersisted({}, '/tmp/project');
|
|
124
|
+
expect(scan.projectRoot).toBe('/tmp/project');
|
|
125
|
+
expect(scan.stack.framework).toBeUndefined();
|
|
126
|
+
expect(scan.stack.testing).toBeUndefined();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
});
|