frappe-builder 1.1.0-dev.24 → 1.1.0-dev.25
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/.fb/state.db +0 -0
- package/.frappe-builder/po-approval/implementation-artifacts/sprint-status.yaml +2 -2
- package/VrWyV9740LA8sENuDokCo/ssr/196abb03d76072784b62b46fb8bc3145b285d048 +318 -0
- package/VrWyV9740LA8sENuDokCo/ssr/38bf1c2ec078b4df1b452507602034ccf39ce1b1 +207 -0
- package/config/defaults.ts +0 -1
- package/config/loader.ts +18 -84
- package/dist/cli.mjs +7 -6
- package/dist/{init-Gp1MgJD2.mjs → init-CkLSZ_3g.mjs} +13 -123
- package/extensions/frappe-state.ts +0 -13
- package/extensions/frappe-tools.ts +3 -0
- package/package.json +1 -1
- package/state/db.ts +14 -2
- package/state/schema.ts +6 -0
- package/tools/frappe-query-tools.ts +36 -20
- package/tools/project-tools.ts +12 -11
package/.fb/state.db
CHANGED
|
Binary file
|
|
@@ -2,13 +2,13 @@ feature_id: po-approval
|
|
|
2
2
|
feature_name: "PO Approval"
|
|
3
3
|
mode: full
|
|
4
4
|
phase: testing
|
|
5
|
-
updated_at: 2026-03-28T14:
|
|
5
|
+
updated_at: 2026-03-28T14:37:54.966Z
|
|
6
6
|
|
|
7
7
|
components:
|
|
8
8
|
- id: final-comp
|
|
9
9
|
sort_order: 0
|
|
10
10
|
status: complete
|
|
11
|
-
completed_at: 2026-03-28T14:
|
|
11
|
+
completed_at: 2026-03-28T14:37:54.965Z
|
|
12
12
|
|
|
13
13
|
progress:
|
|
14
14
|
done: 1
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
__vite_ssr_exportName__("patchGitignore", () => { try { return patchGitignore } catch {} });
|
|
2
|
+
__vite_ssr_exportName__("runInit", () => { try { return runInit } catch {} });
|
|
3
|
+
__vite_ssr_exportName__("setupContextMode", () => { try { return setupContextMode } catch {} });
|
|
4
|
+
__vite_ssr_exportName__("setupMcp2cli", () => { try { return setupMcp2cli } catch {} });
|
|
5
|
+
const __vite_ssr_import_0__ = await __vite_ssr_import__("node:fs", {"importedNames":["mkdirSync","existsSync","readFileSync","writeFileSync","renameSync"]});
|
|
6
|
+
const __vite_ssr_import_1__ = await __vite_ssr_import__("node:path", {"importedNames":["join"]});
|
|
7
|
+
const __vite_ssr_import_2__ = await __vite_ssr_import__("node:os", {"importedNames":["homedir"]});
|
|
8
|
+
const __vite_ssr_import_3__ = await __vite_ssr_import__("node:readline", {"importedNames":["createInterface"]});
|
|
9
|
+
const __vite_ssr_import_4__ = await __vite_ssr_import__("node:child_process", {"importedNames":["spawnSync"]});
|
|
10
|
+
/**
|
|
11
|
+
* src/init.ts — interactive setup wizard for frappe-builder
|
|
12
|
+
*
|
|
13
|
+
* Handles: global config (~/.frappe-builder/config.json),
|
|
14
|
+
* project config (.frappe-builder-config.json),
|
|
15
|
+
* and .gitignore patching.
|
|
16
|
+
*
|
|
17
|
+
* No imports from state/, extensions/, or gates/.
|
|
18
|
+
* Uses Node.js built-ins only — no external prompt libraries.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
let cancelled = false;
|
|
26
|
+
process.on("SIGINT", () => {
|
|
27
|
+
cancelled = true;
|
|
28
|
+
});
|
|
29
|
+
function promptLine(question) {
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
if (cancelled) {
|
|
32
|
+
resolve("");
|
|
33
|
+
return;
|
|
34
|
+
};
|
|
35
|
+
const rl = (0,__vite_ssr_import_3__.createInterface)({
|
|
36
|
+
input: process.stdin,
|
|
37
|
+
output: process.stdout
|
|
38
|
+
});
|
|
39
|
+
rl.question(question, (answer) => {
|
|
40
|
+
rl.close();
|
|
41
|
+
resolve(answer);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
async function promptYN(question) {
|
|
46
|
+
const answer = await promptLine(question + " (y/N): ");
|
|
47
|
+
return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
|
|
48
|
+
}
|
|
49
|
+
function writeAtomic(filePath, content) {
|
|
50
|
+
const tmp = filePath + ".tmp";
|
|
51
|
+
(0,__vite_ssr_import_0__.writeFileSync)(tmp, content, "utf-8");
|
|
52
|
+
(0,__vite_ssr_import_0__.renameSync)(tmp, filePath);
|
|
53
|
+
}
|
|
54
|
+
/** Patches .gitignore to include the exact entry if not already present. */
|
|
55
|
+
function patchGitignore(projectRoot, entry) {
|
|
56
|
+
const gitignorePath = (0,__vite_ssr_import_1__.join)(projectRoot, ".gitignore");
|
|
57
|
+
if (!(0,__vite_ssr_import_0__.existsSync)(gitignorePath)) {
|
|
58
|
+
(0,__vite_ssr_import_0__.writeFileSync)(gitignorePath, entry + "\n", "utf-8");
|
|
59
|
+
return "created";
|
|
60
|
+
};
|
|
61
|
+
const content = (0,__vite_ssr_import_0__.readFileSync)(gitignorePath, "utf-8");
|
|
62
|
+
const lines = content.split("\n");
|
|
63
|
+
if (lines.includes(entry)) {
|
|
64
|
+
return "already-present";
|
|
65
|
+
};
|
|
66
|
+
const patched = content.endsWith("\n") ? content + entry + "\n" : content + "\n" + entry + "\n";
|
|
67
|
+
(0,__vite_ssr_import_0__.writeFileSync)(gitignorePath, patched, "utf-8");
|
|
68
|
+
return "patched";
|
|
69
|
+
};
|
|
70
|
+
async function runInit(opts = {}) {
|
|
71
|
+
const projectRoot = opts.projectRoot ?? process.cwd();
|
|
72
|
+
const homeDir = (0,__vite_ssr_import_2__.homedir)();
|
|
73
|
+
const globalConfigDir = (0,__vite_ssr_import_1__.join)(homeDir, ".frappe-builder");
|
|
74
|
+
const globalConfigPath = (0,__vite_ssr_import_1__.join)(globalConfigDir, "config.json");
|
|
75
|
+
const projectConfigPath = (0,__vite_ssr_import_1__.join)(projectRoot, ".frappe-builder-config.json");
|
|
76
|
+
console.log("\n=== frappe-builder Setup ===\n");
|
|
77
|
+
// ── Global config ────────────────────────────────────────────────────────
|
|
78
|
+
console.log(`[Global config: ${globalConfigPath}]`);
|
|
79
|
+
let globalConfig = {};
|
|
80
|
+
let globalAction = "written";
|
|
81
|
+
if ((0,__vite_ssr_import_0__.existsSync)(globalConfigPath)) {
|
|
82
|
+
try {
|
|
83
|
+
globalConfig = JSON.parse((0,__vite_ssr_import_0__.readFileSync)(globalConfigPath, "utf-8"));
|
|
84
|
+
} catch {};
|
|
85
|
+
if (!cancelled) {
|
|
86
|
+
const overwrite = await promptYN(`Overwrite existing ${globalConfigPath}?`);
|
|
87
|
+
if (cancelled) {
|
|
88
|
+
printCancelled();
|
|
89
|
+
return;
|
|
90
|
+
};
|
|
91
|
+
if (!overwrite) {
|
|
92
|
+
globalAction = "skipped";
|
|
93
|
+
console.log(" Keeping existing global config.\n");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
if (!cancelled && globalAction === "written") {
|
|
98
|
+
const llmKey = await promptLine("LLM API key (leave blank to skip): ");
|
|
99
|
+
if (cancelled) {
|
|
100
|
+
printCancelled();
|
|
101
|
+
return;
|
|
102
|
+
};
|
|
103
|
+
globalConfig.llm_api_key = llmKey.trim();
|
|
104
|
+
};
|
|
105
|
+
// ── Project config ───────────────────────────────────────────────────────
|
|
106
|
+
console.log(`\n[Project config: ${projectConfigPath}]`);
|
|
107
|
+
let projectConfig = {};
|
|
108
|
+
let projectAction = "written";
|
|
109
|
+
if ((0,__vite_ssr_import_0__.existsSync)(projectConfigPath)) {
|
|
110
|
+
try {
|
|
111
|
+
projectConfig = JSON.parse((0,__vite_ssr_import_0__.readFileSync)(projectConfigPath, "utf-8"));
|
|
112
|
+
} catch {};
|
|
113
|
+
if (!cancelled) {
|
|
114
|
+
const overwrite = await promptYN(`Overwrite existing ${projectConfigPath}?`);
|
|
115
|
+
if (cancelled) {
|
|
116
|
+
printCancelled();
|
|
117
|
+
return;
|
|
118
|
+
};
|
|
119
|
+
if (!overwrite) {
|
|
120
|
+
projectAction = "skipped";
|
|
121
|
+
console.log(" Keeping existing project config.\n");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
if (!cancelled && projectAction === "written") {
|
|
126
|
+
const siteUrl = await promptLine("Frappe site URL (e.g. http://site1.localhost): ");
|
|
127
|
+
if (cancelled) {
|
|
128
|
+
printCancelled();
|
|
129
|
+
return;
|
|
130
|
+
};
|
|
131
|
+
const apiKey = await promptLine("Frappe API key: ");
|
|
132
|
+
if (cancelled) {
|
|
133
|
+
printCancelled();
|
|
134
|
+
return;
|
|
135
|
+
};
|
|
136
|
+
const apiSecret = await promptLine("Frappe API secret: ");
|
|
137
|
+
if (cancelled) {
|
|
138
|
+
printCancelled();
|
|
139
|
+
return;
|
|
140
|
+
};
|
|
141
|
+
projectConfig = {
|
|
142
|
+
site_url: siteUrl.trim(),
|
|
143
|
+
api_key: apiKey.trim(),
|
|
144
|
+
api_secret: apiSecret.trim()
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
// ── All prompts collected — now write files ──────────────────────────────
|
|
148
|
+
const written = [];
|
|
149
|
+
const skipped = [];
|
|
150
|
+
// Global config
|
|
151
|
+
if (globalAction === "written") {
|
|
152
|
+
(0,__vite_ssr_import_0__.mkdirSync)(globalConfigDir, { recursive: true });
|
|
153
|
+
writeAtomic(globalConfigPath, JSON.stringify(globalConfig, null, 2) + "\n");
|
|
154
|
+
written.push(`~/.frappe-builder/config.json`);
|
|
155
|
+
} else {
|
|
156
|
+
skipped.push(`~/.frappe-builder/config.json`);
|
|
157
|
+
};
|
|
158
|
+
// Project config
|
|
159
|
+
if (projectAction === "written") {
|
|
160
|
+
writeAtomic(projectConfigPath, JSON.stringify(projectConfig, null, 2) + "\n");
|
|
161
|
+
written.push(`.frappe-builder-config.json`);
|
|
162
|
+
} else {
|
|
163
|
+
skipped.push(`.frappe-builder-config.json`);
|
|
164
|
+
};
|
|
165
|
+
// Gitignore patch
|
|
166
|
+
const gitignoreResult = patchGitignore(projectRoot, ".frappe-builder-config.json");
|
|
167
|
+
if (gitignoreResult === "patched") {
|
|
168
|
+
written.push(".gitignore (patched)");
|
|
169
|
+
} else if (gitignoreResult === "created") {
|
|
170
|
+
written.push(".gitignore (created)");
|
|
171
|
+
} else {
|
|
172
|
+
skipped.push(".gitignore (entry already present)");
|
|
173
|
+
};
|
|
174
|
+
// ── context-mode MCP extension ───────────────────────────────────────────
|
|
175
|
+
await setupContextMode(homeDir);
|
|
176
|
+
// ── mcp2cli skill + context-mode bake ────────────────────────────────────
|
|
177
|
+
setupMcp2cli(homeDir);
|
|
178
|
+
// ── Summary ──────────────────────────────────────────────────────────────
|
|
179
|
+
console.log("\nFiles written:");
|
|
180
|
+
for (const f of written) {
|
|
181
|
+
console.log(` ✓ ${f}`);
|
|
182
|
+
};
|
|
183
|
+
if (skipped.length > 0) {
|
|
184
|
+
console.log("Skipped:");
|
|
185
|
+
for (const f of skipped) {
|
|
186
|
+
console.log(` - ${f}`);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
console.log("\nReady. Run: frappe-builder\n");
|
|
190
|
+
};
|
|
191
|
+
function printCancelled() {
|
|
192
|
+
console.log("\nSetup cancelled. No files were written.\n");
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Installs and configures the context-mode pi MCP extension.
|
|
196
|
+
* Clones https://github.com/mksglu/context-mode into ~/.pi/extensions/context-mode,
|
|
197
|
+
* builds it, and patches ~/.pi/settings/mcp.json with the server entry.
|
|
198
|
+
*
|
|
199
|
+
* Non-fatal — failures are logged as warnings, never abort init.
|
|
200
|
+
*/
|
|
201
|
+
async function setupContextMode(homeDir) {
|
|
202
|
+
const extDir = (0,__vite_ssr_import_1__.join)(homeDir, ".pi", "extensions", "context-mode");
|
|
203
|
+
const mcpSettingsDir = (0,__vite_ssr_import_1__.join)(homeDir, ".pi", "settings");
|
|
204
|
+
const mcpSettingsPath = (0,__vite_ssr_import_1__.join)(mcpSettingsDir, "mcp.json");
|
|
205
|
+
const startScript = (0,__vite_ssr_import_1__.join)(extDir, "node_modules", "context-mode", "start.mjs");
|
|
206
|
+
console.log("\n[context-mode MCP extension]");
|
|
207
|
+
// ── Already installed? ──────────────────────────────────────────────────
|
|
208
|
+
if ((0,__vite_ssr_import_0__.existsSync)(extDir)) {
|
|
209
|
+
console.log(" ✓ context-mode already installed at ~/.pi/extensions/context-mode");
|
|
210
|
+
} else {
|
|
211
|
+
console.log(" context-mode not found — installing (requires git + Node.js)...");
|
|
212
|
+
(0,__vite_ssr_import_0__.mkdirSync)((0,__vite_ssr_import_1__.join)(homeDir, ".pi", "extensions"), { recursive: true });
|
|
213
|
+
const clone = (0,__vite_ssr_import_4__.spawnSync)("git", [
|
|
214
|
+
"clone",
|
|
215
|
+
"https://github.com/mksglu/context-mode.git",
|
|
216
|
+
extDir
|
|
217
|
+
], { stdio: "pipe" });
|
|
218
|
+
if (clone.status !== 0) {
|
|
219
|
+
console.warn(` ⚠ git clone failed: ${clone.stderr?.toString().trim()}`);
|
|
220
|
+
console.warn(" Skipping context-mode setup. Install manually: https://github.com/mksglu/context-mode");
|
|
221
|
+
return;
|
|
222
|
+
};
|
|
223
|
+
const install = (0,__vite_ssr_import_4__.spawnSync)("npm", ["install"], {
|
|
224
|
+
cwd: extDir,
|
|
225
|
+
stdio: "pipe"
|
|
226
|
+
});
|
|
227
|
+
if (install.status !== 0) {
|
|
228
|
+
console.warn(` ⚠ npm install failed: ${install.stderr?.toString().trim()}`);
|
|
229
|
+
return;
|
|
230
|
+
};
|
|
231
|
+
const build = (0,__vite_ssr_import_4__.spawnSync)("npm", ["run", "build"], {
|
|
232
|
+
cwd: extDir,
|
|
233
|
+
stdio: "pipe"
|
|
234
|
+
});
|
|
235
|
+
if (build.status !== 0) {
|
|
236
|
+
console.warn(` ⚠ npm run build failed: ${build.stderr?.toString().trim()}`);
|
|
237
|
+
return;
|
|
238
|
+
};
|
|
239
|
+
console.log(" ✓ context-mode installed and built");
|
|
240
|
+
};
|
|
241
|
+
// ── Patch ~/.pi/settings/mcp.json ──────────────────────────────────────
|
|
242
|
+
(0,__vite_ssr_import_0__.mkdirSync)(mcpSettingsDir, { recursive: true });
|
|
243
|
+
let mcpConfig = {};
|
|
244
|
+
if ((0,__vite_ssr_import_0__.existsSync)(mcpSettingsPath)) {
|
|
245
|
+
try {
|
|
246
|
+
mcpConfig = JSON.parse((0,__vite_ssr_import_0__.readFileSync)(mcpSettingsPath, "utf-8"));
|
|
247
|
+
} catch {}
|
|
248
|
+
};
|
|
249
|
+
const servers = mcpConfig.mcpServers ?? {};
|
|
250
|
+
if (servers["context-mode"]) {
|
|
251
|
+
console.log(" ✓ context-mode already in ~/.pi/settings/mcp.json");
|
|
252
|
+
return;
|
|
253
|
+
};
|
|
254
|
+
servers["context-mode"] = {
|
|
255
|
+
command: "node",
|
|
256
|
+
args: [startScript]
|
|
257
|
+
};
|
|
258
|
+
mcpConfig.mcpServers = servers;
|
|
259
|
+
writeAtomic(mcpSettingsPath, JSON.stringify(mcpConfig, null, 2) + "\n");
|
|
260
|
+
console.log(" ✓ Added context-mode to ~/.pi/settings/mcp.json");
|
|
261
|
+
console.log(" Restart pi (or frappe-builder) for context-mode to activate.");
|
|
262
|
+
};
|
|
263
|
+
/**
|
|
264
|
+
* Installs the mcp2cli Claude Code skill and bakes the context-mode connection
|
|
265
|
+
* so the agent can call `mcp2cli @context-mode <tool>` without repeating flags.
|
|
266
|
+
*
|
|
267
|
+
* Non-fatal — failures are logged as warnings, never abort init.
|
|
268
|
+
*/
|
|
269
|
+
function setupMcp2cli(homeDir) {
|
|
270
|
+
const startScript = (0,__vite_ssr_import_1__.join)(homeDir, ".pi", "extensions", "context-mode", "node_modules", "context-mode", "start.mjs");
|
|
271
|
+
console.log("\n[mcp2cli skill + context-mode bake]");
|
|
272
|
+
// ── Install mcp2cli Claude Code skill ───────────────────────────────────
|
|
273
|
+
const skillAdd = (0,__vite_ssr_import_4__.spawnSync)("npx", [
|
|
274
|
+
"skills",
|
|
275
|
+
"add",
|
|
276
|
+
"knowsuchagency/mcp2cli",
|
|
277
|
+
"--skill",
|
|
278
|
+
"mcp2cli"
|
|
279
|
+
], { stdio: "pipe" });
|
|
280
|
+
if (skillAdd.status !== 0) {
|
|
281
|
+
console.warn(` ⚠ mcp2cli skill install failed: ${skillAdd.stderr?.toString().trim()}`);
|
|
282
|
+
console.warn(" Install manually: npx skills add knowsuchagency/mcp2cli --skill mcp2cli");
|
|
283
|
+
} else {
|
|
284
|
+
console.log(" ✓ mcp2cli skill installed");
|
|
285
|
+
};
|
|
286
|
+
// ── Bake context-mode connection ─────────────────────────────────────────
|
|
287
|
+
// Check if already baked
|
|
288
|
+
const bakeShow = (0,__vite_ssr_import_4__.spawnSync)("mcp2cli", [
|
|
289
|
+
"bake",
|
|
290
|
+
"show",
|
|
291
|
+
"context-mode"
|
|
292
|
+
], { stdio: "pipe" });
|
|
293
|
+
if (bakeShow.status === 0) {
|
|
294
|
+
console.log(" ✓ mcp2cli @context-mode already baked");
|
|
295
|
+
return;
|
|
296
|
+
};
|
|
297
|
+
if (!(0,__vite_ssr_import_0__.existsSync)(startScript)) {
|
|
298
|
+
console.warn(" ⚠ context-mode start.mjs not found — skipping bake (run init again after context-mode installs)");
|
|
299
|
+
return;
|
|
300
|
+
};
|
|
301
|
+
const bakeCreate = (0,__vite_ssr_import_4__.spawnSync)("mcp2cli", [
|
|
302
|
+
"bake",
|
|
303
|
+
"create",
|
|
304
|
+
"context-mode",
|
|
305
|
+
"--mcp-stdio",
|
|
306
|
+
`node ${startScript}`
|
|
307
|
+
], { stdio: "pipe" });
|
|
308
|
+
if (bakeCreate.status !== 0) {
|
|
309
|
+
console.warn(` ⚠ mcp2cli bake failed: ${bakeCreate.stderr?.toString().trim()}`);
|
|
310
|
+
console.warn(" Install mcp2cli first: pip install mcp2cli");
|
|
311
|
+
} else {
|
|
312
|
+
console.log(" ✓ mcp2cli @context-mode baked — agent can now call: mcp2cli @context-mode <tool>");
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
//# sourceMappingSource=vite-generated
|
|
316
|
+
//# sourceMappingURL=data:application/json;base64,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LEdBQUc7QUFDekIsVUFBUSxLQUFLLHFDQUFxQyxTQUFTLFFBQVEsVUFBVSxDQUFDLE1BQU0sR0FBRztBQUN2RixVQUFRLEtBQUssNEVBQTRFO1FBQ3BGO0FBQ0wsVUFBUSxJQUFJLDhCQUE4Qjs7OztDQUs1QyxNQUFNLGNBQVcsaUNBQVUsV0FBVztFQUFDO0VBQVE7RUFBUTtFQUFlLEVBQUUsRUFBRSxPQUFPLFFBQVEsQ0FBQztBQUMxRixLQUFJLFNBQVMsV0FBVyxHQUFHO0FBQ3pCLFVBQVEsSUFBSSwwQ0FBMEM7QUFDdEQ7O0FBR0YsS0FBSSxJQUFDLGtDQUFXLFlBQVksRUFBRTtBQUM1QixVQUFRLEtBQUssb0dBQW9HO0FBQ2pIOztDQUdGLE1BQU0sZ0JBQWEsaUNBQ2pCLFdBQ0E7RUFBQztFQUFRO0VBQVU7RUFBZ0I7RUFBZSxRQUFRO0VBQWMsRUFDeEUsRUFBRSxPQUFPLFFBQVEsQ0FDbEI7QUFDRCxLQUFJLFdBQVcsV0FBVyxHQUFHO0FBQzNCLFVBQVEsS0FBSyw0QkFBNEIsV0FBVyxRQUFRLFVBQVUsQ0FBQyxNQUFNLEdBQUc7QUFDaEYsVUFBUSxLQUFLLCtDQUErQztRQUN2RDtBQUNMLFVBQVEsSUFBSSxxRkFBcUYiLCJuYW1lcyI6W10sImlnbm9yZUxpc3QiOltdLCJzb3VyY2VzIjpbImluaXQudHMiXSwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBzcmMvaW5pdC50cyDigJQgaW50ZXJhY3RpdmUgc2V0dXAgd2l6YXJkIGZvciBmcmFwcGUtYnVpbGRlclxuICpcbiAqIEhhbmRsZXM6IGdsb2JhbCBjb25maWcgKH4vLmZyYXBwZS1idWlsZGVyL2NvbmZpZy5qc29uKSxcbiAqICAgICAgICAgIHByb2plY3QgY29uZmlnICguZnJhcHBlLWJ1aWxkZXItY29uZmlnLmpzb24pLFxuICogICAgICAgICAgYW5kIC5naXRpZ25vcmUgcGF0Y2hpbmcuXG4gKlxuICogTm8gaW1wb3J0cyBmcm9tIHN0YXRlLywgZXh0ZW5zaW9ucy8sIG9yIGdhdGVzLy5cbiAqIFVzZXMgTm9kZS5qcyBidWlsdC1pbnMgb25seSDigJQgbm8gZXh0ZXJuYWwgcHJvbXB0IGxpYnJhcmllcy5cbiAqL1xuXG5pbXBvcnQgeyBta2RpclN5bmMsIGV4aXN0c1N5bmMsIHJlYWRGaWxlU3luYywgd3JpdGVGaWxlU3luYywgcmVuYW1lU3luYyB9IGZyb20gXCJub2RlOmZzXCI7XG5pbXBvcnQgeyBqb2luIH0gZnJvbSBcIm5vZGU6cGF0aFwiO1xuaW1wb3J0IHsgaG9tZWRpciB9IGZyb20gXCJub2RlOm9zXCI7XG5pbXBvcnQgeyBjcmVhdGVJbnRlcmZhY2UgfSBmcm9tIFwibm9kZTpyZWFkbGluZVwiO1xuaW1wb3J0IHsgc3Bhd25TeW5jIH0gZnJvbSBcIm5vZGU6Y2hpbGRfcHJvY2Vzc1wiO1xuXG5sZXQgY2FuY2VsbGVkID0gZmFsc2U7XG5cbnByb2Nlc3Mub24oXCJTSUdJTlRcIiwgKCkgPT4ge1xuICBjYW5jZWxsZWQgPSB0cnVlO1xufSk7XG5cbmZ1bmN0aW9uIHByb21wdExpbmUocXVlc3Rpb246IHN0cmluZyk6IFByb21pc2U8c3RyaW5nPiB7XG4gIHJldHVybiBuZXcgUHJvbWlzZSgocmVzb2x2ZSkgPT4ge1xuICAgIGlmIChjYW5jZWxsZWQpIHsgcmVzb2x2ZShcIlwiKTsgcmV0dXJuOyB9XG4gICAgY29uc3QgcmwgPSBjcmVhdGVJbnRlcmZhY2UoeyBpbnB1dDogcHJvY2Vzcy5zdGRpbiwgb3V0cHV0OiBwcm9jZXNzLnN0ZG91dCB9KTtcbiAgICBybC5xdWVzdGlvbihxdWVzdGlvbiwgKGFuc3dlcikgPT4ge1xuICAgICAgcmwuY2xvc2UoKTtcbiAgICAgIHJlc29sdmUoYW5zd2VyKTtcbiAgICB9KTtcbiAgfSk7XG59XG5cbmFzeW5jIGZ1bmN0aW9uIHByb21wdFlOKHF1ZXN0aW9uOiBzdHJpbmcpOiBQcm9taXNlPGJvb2xlYW4+IHtcbiAgY29uc3QgYW5zd2VyID0gYXdhaXQgcHJvbXB0TGluZShxdWVzdGlvbiArIFwiICh5L04pOiBcIik7XG4gIHJldHVybiBhbnN3ZXIudHJpbSgpLnRvTG93ZXJDYXNlKCkgPT09IFwieVwiIHx8IGFuc3dlci50cmltKCkudG9Mb3dlckNhc2UoKSA9PT0gXCJ5ZXNcIjtcbn1cblxuZnVuY3Rpb24gd3JpdGVBdG9taWMoZmlsZVBhdGg6IHN0cmluZywgY29udGVudDogc3RyaW5nKTogdm9pZCB7XG4gIGNvbnN0IHRtcCA9IGZpbGVQYXRoICsgXCIudG1wXCI7XG4gIHdyaXRlRmlsZVN5bmModG1wLCBjb250ZW50LCBcInV0Zi04XCIpO1xuICByZW5hbWVTeW5jKHRtcCwgZmlsZVBhdGgpO1xufVxuXG4vKiogUGF0Y2hlcyAuZ2l0aWdub3JlIHRvIGluY2x1ZGUgdGhlIGV4YWN0IGVudHJ5IGlmIG5vdCBhbHJlYWR5IHByZXNlbnQuICovXG5leHBvcnQgZnVuY3Rpb24gcGF0Y2hHaXRpZ25vcmUocHJvamVjdFJvb3Q6IHN0cmluZywgZW50cnk6IHN0cmluZyk6IFwicGF0Y2hlZFwiIHwgXCJhbHJlYWR5LXByZXNlbnRcIiB8IFwiY3JlYXRlZFwiIHtcbiAgY29uc3QgZ2l0aWdub3JlUGF0aCA9IGpvaW4ocHJvamVjdFJvb3QsIFwiLmdpdGlnbm9yZVwiKTtcbiAgaWYgKCFleGlzdHNTeW5jKGdpdGlnbm9yZVBhdGgpKSB7XG4gICAgd3JpdGVGaWxlU3luYyhnaXRpZ25vcmVQYXRoLCBlbnRyeSArIFwiXFxuXCIsIFwidXRmLThcIik7XG4gICAgcmV0dXJuIFwiY3JlYXRlZFwiO1xuICB9XG4gIGNvbnN0IGNvbnRlbnQgPSByZWFkRmlsZVN5bmMoZ2l0aWdub3JlUGF0aCwgXCJ1dGYtOFwiKTtcbiAgY29uc3QgbGluZXMgPSBjb250ZW50LnNwbGl0KFwiXFxuXCIpO1xuICBpZiAobGluZXMuaW5jbHVkZXMoZW50cnkpKSB7XG4gICAgcmV0dXJuIFwiYWxyZWFkeS1wcmVzZW50XCI7XG4gIH1cbiAgY29uc3QgcGF0Y2hlZCA9IGNvbnRlbnQuZW5kc1dpdGgoXCJcXG5cIikgPyBjb250ZW50ICsgZW50cnkgKyBcIlxcblwiIDogY29udGVudCArIFwiXFxuXCIgKyBlbnRyeSArIFwiXFxuXCI7XG4gIHdyaXRlRmlsZVN5bmMoZ2l0aWdub3JlUGF0aCwgcGF0Y2hlZCwgXCJ1dGYtOFwiKTtcbiAgcmV0dXJuIFwicGF0Y2hlZFwiO1xufVxuXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gcnVuSW5pdChvcHRzOiB7IHByb2plY3RSb290Pzogc3RyaW5nIH0gPSB7fSk6IFByb21pc2U8dm9pZD4ge1xuICBjb25zdCBwcm9qZWN0Um9vdCA9IG9wdHMucHJvamVjdFJvb3QgPz8gcHJvY2Vzcy5jd2QoKTtcbiAgY29uc3QgaG9tZURpciA9IGhvbWVkaXIoKTtcbiAgY29uc3QgZ2xvYmFsQ29uZmlnRGlyID0gam9pbihob21lRGlyLCBcIi5mcmFwcGUtYnVpbGRlclwiKTtcbiAgY29uc3QgZ2xvYmFsQ29uZmlnUGF0aCA9IGpvaW4oZ2xvYmFsQ29uZmlnRGlyLCBcImNvbmZpZy5qc29uXCIpO1xuICBjb25zdCBwcm9qZWN0Q29uZmlnUGF0aCA9IGpvaW4ocHJvamVjdFJvb3QsIFwiLmZyYXBwZS1idWlsZGVyLWNvbmZpZy5qc29uXCIpO1xuXG4gIGNvbnNvbGUubG9nKFwiXFxuPT09IGZyYXBwZS1idWlsZGVyIFNldHVwID09PVxcblwiKTtcblxuICAvLyDilIDilIAgR2xvYmFsIGNvbmZpZyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIBcbiAgY29uc29sZS5sb2coYFtHbG9iYWwgY29uZmlnOiAke2dsb2JhbENvbmZpZ1BhdGh9XWApO1xuXG4gIGxldCBnbG9iYWxDb25maWc6IFJlY29yZDxzdHJpbmcsIHVua25vd24+ID0ge307XG4gIGxldCBnbG9iYWxBY3Rpb246IFwid3JpdHRlblwiIHwgXCJza2lwcGVkXCIgPSBcIndyaXR0ZW5cIjtcblxuICBpZiAoZXhpc3RzU3luYyhnbG9iYWxDb25maWdQYXRoKSkge1xuICAgIHRyeSB7XG4gICAgICBnbG9iYWxDb25maWcgPSBKU09OLnBhcnNlKHJlYWRGaWxlU3luYyhnbG9iYWxDb25maWdQYXRoLCBcInV0Zi04XCIpKSBhcyBSZWNvcmQ8c3RyaW5nLCB1bmtub3duPjtcbiAgICB9IGNhdGNoIHsgLyogaWdub3JlIG1hbGZvcm1lZCBmaWxlIOKAlCBvdmVyd3JpdGUgKi8gfVxuICAgIGlmICghY2FuY2VsbGVkKSB7XG4gICAgICBjb25zdCBvdmVyd3JpdGUgPSBhd2FpdCBwcm9tcHRZTihgT3ZlcndyaXRlIGV4aXN0aW5nICR7Z2xvYmFsQ29uZmlnUGF0aH0/YCk7XG4gICAgICBpZiAoY2FuY2VsbGVkKSB7IHByaW50Q2FuY2VsbGVkKCk7IHJldHVybjsgfVxuICAgICAgaWYgKCFvdmVyd3JpdGUpIHtcbiAgICAgICAgZ2xvYmFsQWN0aW9uID0gXCJza2lwcGVkXCI7XG4gICAgICAgIGNvbnNvbGUubG9nKFwiICBLZWVwaW5nIGV4aXN0aW5nIGdsb2JhbCBjb25maWcuXFxuXCIpO1xuICAgICAgfVxuICAgIH1cbiAgfVxuXG4gIGlmICghY2FuY2VsbGVkICYmIGdsb2JhbEFjdGlvbiA9PT0gXCJ3cml0dGVuXCIpIHtcbiAgICBjb25zdCBsbG1LZXkgPSBhd2FpdCBwcm9tcHRMaW5lKFwiTExNIEFQSSBrZXkgKGxlYXZlIGJsYW5rIHRvIHNraXApOiBcIik7XG4gICAgaWYgKGNhbmNlbGxlZCkgeyBwcmludENhbmNlbGxlZCgpOyByZXR1cm47IH1cbiAgICBnbG9iYWxDb25maWcubGxtX2FwaV9rZXkgPSBsbG1LZXkudHJpbSgpO1xuICB9XG5cbiAgLy8g4pSA4pSAIFByb2plY3QgY29uZmlnIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgFxuICBjb25zb2xlLmxvZyhgXFxuW1Byb2plY3QgY29uZmlnOiAke3Byb2plY3RDb25maWdQYXRofV1gKTtcblxuICBsZXQgcHJvamVjdENvbmZpZzogUmVjb3JkPHN0cmluZywgdW5rbm93bj4gPSB7fTtcbiAgbGV0IHByb2plY3RBY3Rpb246IFwid3JpdHRlblwiIHwgXCJza2lwcGVkXCIgPSBcIndyaXR0ZW5cIjtcblxuICBpZiAoZXhpc3RzU3luYyhwcm9qZWN0Q29uZmlnUGF0aCkpIHtcbiAgICB0cnkge1xuICAgICAgcHJvamVjdENvbmZpZyA9IEpTT04ucGFyc2UocmVhZEZpbGVTeW5jKHByb2plY3RDb25maWdQYXRoLCBcInV0Zi04XCIpKSBhcyBSZWNvcmQ8c3RyaW5nLCB1bmtub3duPjtcbiAgICB9IGNhdGNoIHsgLyogaWdub3JlIG1hbGZvcm1lZCBmaWxlIOKAlCBvdmVyd3JpdGUgKi8gfVxuICAgIGlmICghY2FuY2VsbGVkKSB7XG4gICAgICBjb25zdCBvdmVyd3JpdGUgPSBhd2FpdCBwcm9tcHRZTihgT3ZlcndyaXRlIGV4aXN0aW5nICR7cHJvamVjdENvbmZpZ1BhdGh9P2ApO1xuICAgICAgaWYgKGNhbmNlbGxlZCkgeyBwcmludENhbmNlbGxlZCgpOyByZXR1cm47IH1cbiAgICAgIGlmICghb3ZlcndyaXRlKSB7XG4gICAgICAgIHByb2plY3RBY3Rpb24gPSBcInNraXBwZWRcIjtcbiAgICAgICAgY29uc29sZS5sb2coXCIgIEtlZXBpbmcgZXhpc3RpbmcgcHJvamVjdCBjb25maWcuXFxuXCIpO1xuICAgICAgfVxuICAgIH1cbiAgfVxuXG4gIGlmICghY2FuY2VsbGVkICYmIHByb2plY3RBY3Rpb24gPT09IFwid3JpdHRlblwiKSB7XG4gICAgY29uc3Qgc2l0ZVVybCA9IGF3YWl0IHByb21wdExpbmUoXCJGcmFwcGUgc2l0ZSBVUkwgKGUuZy4gaHR0cDovL3NpdGUxLmxvY2FsaG9zdCk6IFwiKTtcbiAgICBpZiAoY2FuY2VsbGVkKSB7IHByaW50Q2FuY2VsbGVkKCk7IHJldHVybjsgfVxuICAgIGNvbnN0IGFwaUtleSA9IGF3YWl0IHByb21wdExpbmUoXCJGcmFwcGUgQVBJIGtleTogXCIpO1xuICAgIGlmIChjYW5jZWxsZWQpIHsgcHJpbnRDYW5jZWxsZWQoKTsgcmV0dXJuOyB9XG4gICAgY29uc3QgYXBpU2VjcmV0ID0gYXdhaXQgcHJvbXB0TGluZShcIkZyYXBwZSBBUEkgc2VjcmV0OiBcIik7XG4gICAgaWYgKGNhbmNlbGxlZCkgeyBwcmludENhbmNlbGxlZCgpOyByZXR1cm47IH1cbiAgICBwcm9qZWN0Q29uZmlnID0ge1xuICAgICAgc2l0ZV91cmw6IHNpdGVVcmwudHJpbSgpLFxuICAgICAgYXBpX2tleTogYXBpS2V5LnRyaW0oKSxcbiAgICAgIGFwaV9zZWNyZXQ6IGFwaVNlY3JldC50cmltKCksXG4gICAgfTtcbiAgfVxuXG4gIC8vIOKUgOKUgCBBbGwgcHJvbXB0cyBjb2xsZWN0ZWQg4oCUIG5vdyB3cml0ZSBmaWxlcyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIBcbiAgY29uc3Qgd3JpdHRlbjogc3RyaW5nW10gPSBbXTtcbiAgY29uc3Qgc2tpcHBlZDogc3RyaW5nW10gPSBbXTtcblxuICAvLyBHbG9iYWwgY29uZmlnXG4gIGlmIChnbG9iYWxBY3Rpb24gPT09IFwid3JpdHRlblwiKSB7XG4gICAgbWtkaXJTeW5jKGdsb2JhbENvbmZpZ0RpciwgeyByZWN1cnNpdmU6IHRydWUgfSk7XG4gICAgd3JpdGVBdG9taWMoZ2xvYmFsQ29uZmlnUGF0aCwgSlNPTi5zdHJpbmdpZnkoZ2xvYmFsQ29uZmlnLCBudWxsLCAyKSArIFwiXFxuXCIpO1xuICAgIHdyaXR0ZW4ucHVzaChgfi8uZnJhcHBlLWJ1aWxkZXIvY29uZmlnLmpzb25gKTtcbiAgfSBlbHNlIHtcbiAgICBza2lwcGVkLnB1c2goYH4vLmZyYXBwZS1idWlsZGVyL2NvbmZpZy5qc29uYCk7XG4gIH1cblxuICAvLyBQcm9qZWN0IGNvbmZpZ1xuICBpZiAocHJvamVjdEFjdGlvbiA9PT0gXCJ3cml0dGVuXCIpIHtcbiAgICB3cml0ZUF0b21pYyhwcm9qZWN0Q29uZmlnUGF0aCwgSlNPTi5zdHJpbmdpZnkocHJvamVjdENvbmZpZywgbnVsbCwgMikgKyBcIlxcblwiKTtcbiAgICB3cml0dGVuLnB1c2goYC5mcmFwcGUtYnVpbGRlci1jb25maWcuanNvbmApO1xuICB9IGVsc2Uge1xuICAgIHNraXBwZWQucHVzaChgLmZyYXBwZS1idWlsZGVyLWNvbmZpZy5qc29uYCk7XG4gIH1cblxuICAvLyBHaXRpZ25vcmUgcGF0Y2hcbiAgY29uc3QgZ2l0aWdub3JlUmVzdWx0ID0gcGF0Y2hHaXRpZ25vcmUocHJvamVjdFJvb3QsIFwiLmZyYXBwZS1idWlsZGVyLWNvbmZpZy5qc29uXCIpO1xuICBpZiAoZ2l0aWdub3JlUmVzdWx0ID09PSBcInBhdGNoZWRcIikge1xuICAgIHdyaXR0ZW4ucHVzaChcIi5naXRpZ25vcmUgKHBhdGNoZWQpXCIpO1xuICB9IGVsc2UgaWYgKGdpdGlnbm9yZVJlc3VsdCA9PT0gXCJjcmVhdGVkXCIpIHtcbiAgICB3cml0dGVuLnB1c2goXCIuZ2l0aWdub3JlIChjcmVhdGVkKVwiKTtcbiAgfSBlbHNlIHtcbiAgICBza2lwcGVkLnB1c2goXCIuZ2l0aWdub3JlIChlbnRyeSBhbHJlYWR5IHByZXNlbnQpXCIpO1xuICB9XG5cbiAgLy8g4pSA4pSAIGNvbnRleHQtbW9kZSBNQ1AgZXh0ZW5zaW9uIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgFxuICBhd2FpdCBzZXR1cENvbnRleHRNb2RlKGhvbWVEaXIpO1xuXG4gIC8vIOKUgOKUgCBtY3AyY2xpIHNraWxsICsgY29udGV4dC1tb2RlIGJha2Ug4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSAXG4gIHNldHVwTWNwMmNsaShob21lRGlyKTtcblxuICAvLyDilIDilIAgU3VtbWFyeSDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIBcbiAgY29uc29sZS5sb2coXCJcXG5GaWxlcyB3cml0dGVuOlwiKTtcbiAgZm9yIChjb25zdCBmIG9mIHdyaXR0ZW4pIHtcbiAgICBjb25zb2xlLmxvZyhgICDinJMgJHtmfWApO1xuICB9XG4gIGlmIChza2lwcGVkLmxlbmd0aCA+IDApIHtcbiAgICBjb25zb2xlLmxvZyhcIlNraXBwZWQ6XCIpO1xuICAgIGZvciAoY29uc3QgZiBvZiBza2lwcGVkKSB7XG4gICAgICBjb25zb2xlLmxvZyhgICAtICR7Zn1gKTtcbiAgICB9XG4gIH1cblxuICBjb25zb2xlLmxvZyhcIlxcblJlYWR5LiBSdW46IGZyYXBwZS1idWlsZGVyXFxuXCIpO1xufVxuXG5mdW5jdGlvbiBwcmludENhbmNlbGxlZCgpOiB2b2lkIHtcbiAgY29uc29sZS5sb2coXCJcXG5TZXR1cCBjYW5jZWxsZWQuIE5vIGZpbGVzIHdlcmUgd3JpdHRlbi5cXG5cIik7XG59XG5cbi8qKlxuICogSW5zdGFsbHMgYW5kIGNvbmZpZ3VyZXMgdGhlIGNvbnRleHQtbW9kZSBwaSBNQ1AgZXh0ZW5zaW9uLlxuICogQ2xvbmVzIGh0dHBzOi8vZ2l0aHViLmNvbS9ta3NnbHUvY29udGV4dC1tb2RlIGludG8gfi8ucGkvZXh0ZW5zaW9ucy9jb250ZXh0LW1vZGUsXG4gKiBidWlsZHMgaXQsIGFuZCBwYXRjaGVzIH4vLnBpL3NldHRpbmdzL21jcC5qc29uIHdpdGggdGhlIHNlcnZlciBlbnRyeS5cbiAqXG4gKiBOb24tZmF0YWwg4oCUIGZhaWx1cmVzIGFyZSBsb2dnZWQgYXMgd2FybmluZ3MsIG5ldmVyIGFib3J0IGluaXQuXG4gKi9cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiBzZXR1cENvbnRleHRNb2RlKGhvbWVEaXI6IHN0cmluZyk6IFByb21pc2U8dm9pZD4ge1xuICBjb25zdCBleHREaXIgPSBqb2luKGhvbWVEaXIsIFwiLnBpXCIsIFwiZXh0ZW5zaW9uc1wiLCBcImNvbnRleHQtbW9kZVwiKTtcbiAgY29uc3QgbWNwU2V0dGluZ3NEaXIgPSBqb2luKGhvbWVEaXIsIFwiLnBpXCIsIFwic2V0dGluZ3NcIik7XG4gIGNvbnN0IG1jcFNldHRpbmdzUGF0aCA9IGpvaW4obWNwU2V0dGluZ3NEaXIsIFwibWNwLmpzb25cIik7XG4gIGNvbnN0IHN0YXJ0U2NyaXB0ID0gam9pbihleHREaXIsIFwibm9kZV9tb2R1bGVzXCIsIFwiY29udGV4dC1tb2RlXCIsIFwic3RhcnQubWpzXCIpO1xuXG4gIGNvbnNvbGUubG9nKFwiXFxuW2NvbnRleHQtbW9kZSBNQ1AgZXh0ZW5zaW9uXVwiKTtcblxuICAvLyDilIDilIAgQWxyZWFkeSBpbnN0YWxsZWQ/IOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgFxuICBpZiAoZXhpc3RzU3luYyhleHREaXIpKSB7XG4gICAgY29uc29sZS5sb2coXCIgIOKckyBjb250ZXh0LW1vZGUgYWxyZWFkeSBpbnN0YWxsZWQgYXQgfi8ucGkvZXh0ZW5zaW9ucy9jb250ZXh0LW1vZGVcIik7XG4gIH0gZWxzZSB7XG4gICAgY29uc29sZS5sb2coXCIgIGNvbnRleHQtbW9kZSBub3QgZm91bmQg4oCUIGluc3RhbGxpbmcgKHJlcXVpcmVzIGdpdCArIE5vZGUuanMpLi4uXCIpO1xuICAgIG1rZGlyU3luYyhqb2luKGhvbWVEaXIsIFwiLnBpXCIsIFwiZXh0ZW5zaW9uc1wiKSwgeyByZWN1cnNpdmU6IHRydWUgfSk7XG5cbiAgICBjb25zdCBjbG9uZSA9IHNwYXduU3luYyhcImdpdFwiLCBbXG4gICAgICBcImNsb25lXCIsIFwiaHR0cHM6Ly9naXRodWIuY29tL21rc2dsdS9jb250ZXh0LW1vZGUuZ2l0XCIsIGV4dERpcixcbiAgICBdLCB7IHN0ZGlvOiBcInBpcGVcIiB9KTtcblxuICAgIGlmIChjbG9uZS5zdGF0dXMgIT09IDApIHtcbiAgICAgIGNvbnNvbGUud2FybihgICDimqAgZ2l0IGNsb25lIGZhaWxlZDogJHtjbG9uZS5zdGRlcnI/LnRvU3RyaW5nKCkudHJpbSgpfWApO1xuICAgICAgY29uc29sZS53YXJuKFwiICBTa2lwcGluZyBjb250ZXh0LW1vZGUgc2V0dXAuIEluc3RhbGwgbWFudWFsbHk6IGh0dHBzOi8vZ2l0aHViLmNvbS9ta3NnbHUvY29udGV4dC1tb2RlXCIpO1xuICAgICAgcmV0dXJuO1xuICAgIH1cblxuICAgIGNvbnN0IGluc3RhbGwgPSBzcGF3blN5bmMoXCJucG1cIiwgW1wiaW5zdGFsbFwiXSwgeyBjd2Q6IGV4dERpciwgc3RkaW86IFwicGlwZVwiIH0pO1xuICAgIGlmIChpbnN0YWxsLnN0YXR1cyAhPT0gMCkge1xuICAgICAgY29uc29sZS53YXJuKGAgIOKaoCBucG0gaW5zdGFsbCBmYWlsZWQ6ICR7aW5zdGFsbC5zdGRlcnI/LnRvU3RyaW5nKCkudHJpbSgpfWApO1xuICAgICAgcmV0dXJuO1xuICAgIH1cblxuICAgIGNvbnN0IGJ1aWxkID0gc3Bhd25TeW5jKFwibnBtXCIsIFtcInJ1blwiLCBcImJ1aWxkXCJdLCB7IGN3ZDogZXh0RGlyLCBzdGRpbzogXCJwaXBlXCIgfSk7XG4gICAgaWYgKGJ1aWxkLnN0YXR1cyAhPT0gMCkge1xuICAgICAgY29uc29sZS53YXJuKGAgIOKaoCBucG0gcnVuIGJ1aWxkIGZhaWxlZDogJHtidWlsZC5zdGRlcnI/LnRvU3RyaW5nKCkudHJpbSgpfWApO1xuICAgICAgcmV0dXJuO1xuICAgIH1cblxuICAgIGNvbnNvbGUubG9nKFwiICDinJMgY29udGV4dC1tb2RlIGluc3RhbGxlZCBhbmQgYnVpbHRcIik7XG4gIH1cblxuICAvLyDilIDilIAgUGF0Y2ggfi8ucGkvc2V0dGluZ3MvbWNwLmpzb24g4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSAXG4gIG1rZGlyU3luYyhtY3BTZXR0aW5nc0RpciwgeyByZWN1cnNpdmU6IHRydWUgfSk7XG5cbiAgbGV0IG1jcENvbmZpZzogUmVjb3JkPHN0cmluZywgdW5rbm93bj4gPSB7fTtcbiAgaWYgKGV4aXN0c1N5bmMobWNwU2V0dGluZ3NQYXRoKSkge1xuICAgIHRyeSB7XG4gICAgICBtY3BDb25maWcgPSBKU09OLnBhcnNlKHJlYWRGaWxlU3luYyhtY3BTZXR0aW5nc1BhdGgsIFwidXRmLThcIikpIGFzIFJlY29yZDxzdHJpbmcsIHVua25vd24+O1xuICAgIH0gY2F0Y2ggeyAvKiBvdmVyd3JpdGUgbWFsZm9ybWVkIGZpbGUgKi8gfVxuICB9XG5cbiAgY29uc3Qgc2VydmVycyA9IChtY3BDb25maWcubWNwU2VydmVycyA/PyB7fSkgYXMgUmVjb3JkPHN0cmluZywgdW5rbm93bj47XG4gIGlmIChzZXJ2ZXJzW1wiY29udGV4dC1tb2RlXCJdKSB7XG4gICAgY29uc29sZS5sb2coXCIgIOKckyBjb250ZXh0LW1vZGUgYWxyZWFkeSBpbiB+Ly5waS9zZXR0aW5ncy9tY3AuanNvblwiKTtcbiAgICByZXR1cm47XG4gIH1cblxuICBzZXJ2ZXJzW1wiY29udGV4dC1tb2RlXCJdID0ge1xuICAgIGNvbW1hbmQ6IFwibm9kZVwiLFxuICAgIGFyZ3M6IFtzdGFydFNjcmlwdF0sXG4gIH07XG4gIG1jcENvbmZpZy5tY3BTZXJ2ZXJzID0gc2VydmVycztcblxuICB3cml0ZUF0b21pYyhtY3BTZXR0aW5nc1BhdGgsIEpTT04uc3RyaW5naWZ5KG1jcENvbmZpZywgbnVsbCwgMikgKyBcIlxcblwiKTtcbiAgY29uc29sZS5sb2coXCIgIOKckyBBZGRlZCBjb250ZXh0LW1vZGUgdG8gfi8ucGkvc2V0dGluZ3MvbWNwLmpzb25cIik7XG4gIGNvbnNvbGUubG9nKFwiICBSZXN0YXJ0IHBpIChvciBmcmFwcGUtYnVpbGRlcikgZm9yIGNvbnRleHQtbW9kZSB0byBhY3RpdmF0ZS5cIik7XG59XG5cbi8qKlxuICogSW5zdGFsbHMgdGhlIG1jcDJjbGkgQ2xhdWRlIENvZGUgc2tpbGwgYW5kIGJha2VzIHRoZSBjb250ZXh0LW1vZGUgY29ubmVjdGlvblxuICogc28gdGhlIGFnZW50IGNhbiBjYWxsIGBtY3AyY2xpIEBjb250ZXh0LW1vZGUgPHRvb2w+YCB3aXRob3V0IHJlcGVhdGluZyBmbGFncy5cbiAqXG4gKiBOb24tZmF0YWwg4oCUIGZhaWx1cmVzIGFyZSBsb2dnZWQgYXMgd2FybmluZ3MsIG5ldmVyIGFib3J0IGluaXQuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBzZXR1cE1jcDJjbGkoaG9tZURpcjogc3RyaW5nKTogdm9pZCB7XG4gIGNvbnN0IHN0YXJ0U2NyaXB0ID0gam9pbihcbiAgICBob21lRGlyLCBcIi5waVwiLCBcImV4dGVuc2lvbnNcIiwgXCJjb250ZXh0LW1vZGVcIixcbiAgICBcIm5vZGVfbW9kdWxlc1wiLCBcImNvbnRleHQtbW9kZVwiLCBcInN0YXJ0Lm1qc1wiXG4gICk7XG5cbiAgY29uc29sZS5sb2coXCJcXG5bbWNwMmNsaSBza2lsbCArIGNvbnRleHQtbW9kZSBiYWtlXVwiKTtcblxuICAvLyDilIDilIAgSW5zdGFsbCBtY3AyY2xpIENsYXVkZSBDb2RlIHNraWxsIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgFxuICBjb25zdCBza2lsbEFkZCA9IHNwYXduU3luYyhcbiAgICBcIm5weFwiLFxuICAgIFtcInNraWxsc1wiLCBcImFkZFwiLCBcImtub3dzdWNoYWdlbmN5L21jcDJjbGlcIiwgXCItLXNraWxsXCIsIFwibWNwMmNsaVwiXSxcbiAgICB7IHN0ZGlvOiBcInBpcGVcIiB9XG4gICk7XG4gIGlmIChza2lsbEFkZC5zdGF0dXMgIT09IDApIHtcbiAgICBjb25zb2xlLndhcm4oYCAg4pqgIG1jcDJjbGkgc2tpbGwgaW5zdGFsbCBmYWlsZWQ6ICR7c2tpbGxBZGQuc3RkZXJyPy50b1N0cmluZygpLnRyaW0oKX1gKTtcbiAgICBjb25zb2xlLndhcm4oXCIgIEluc3RhbGwgbWFudWFsbHk6IG5weCBza2lsbHMgYWRkIGtub3dzdWNoYWdlbmN5L21jcDJjbGkgLS1za2lsbCBtY3AyY2xpXCIpO1xuICB9IGVsc2Uge1xuICAgIGNvbnNvbGUubG9nKFwiICDinJMgbWNwMmNsaSBza2lsbCBpbnN0YWxsZWRcIik7XG4gIH1cblxuICAvLyDilIDilIAgQmFrZSBjb250ZXh0LW1vZGUgY29ubmVjdGlvbiDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIBcbiAgLy8gQ2hlY2sgaWYgYWxyZWFkeSBiYWtlZFxuICBjb25zdCBiYWtlU2hvdyA9IHNwYXduU3luYyhcIm1jcDJjbGlcIiwgW1wiYmFrZVwiLCBcInNob3dcIiwgXCJjb250ZXh0LW1vZGVcIl0sIHsgc3RkaW86IFwicGlwZVwiIH0pO1xuICBpZiAoYmFrZVNob3cuc3RhdHVzID09PSAwKSB7XG4gICAgY29uc29sZS5sb2coXCIgIOKckyBtY3AyY2xpIEBjb250ZXh0LW1vZGUgYWxyZWFkeSBiYWtlZFwiKTtcbiAgICByZXR1cm47XG4gIH1cblxuICBpZiAoIWV4aXN0c1N5bmMoc3RhcnRTY3JpcHQpKSB7XG4gICAgY29uc29sZS53YXJuKFwiICDimqAgY29udGV4dC1tb2RlIHN0YXJ0Lm1qcyBub3QgZm91bmQg4oCUIHNraXBwaW5nIGJha2UgKHJ1biBpbml0IGFnYWluIGFmdGVyIGNvbnRleHQtbW9kZSBpbnN0YWxscylcIik7XG4gICAgcmV0dXJuO1xuICB9XG5cbiAgY29uc3QgYmFrZUNyZWF0ZSA9IHNwYXduU3luYyhcbiAgICBcIm1jcDJjbGlcIixcbiAgICBbXCJiYWtlXCIsIFwiY3JlYXRlXCIsIFwiY29udGV4dC1tb2RlXCIsIFwiLS1tY3Atc3RkaW9cIiwgYG5vZGUgJHtzdGFydFNjcmlwdH1gXSxcbiAgICB7IHN0ZGlvOiBcInBpcGVcIiB9XG4gICk7XG4gIGlmIChiYWtlQ3JlYXRlLnN0YXR1cyAhPT0gMCkge1xuICAgIGNvbnNvbGUud2FybihgICDimqAgbWNwMmNsaSBiYWtlIGZhaWxlZDogJHtiYWtlQ3JlYXRlLnN0ZGVycj8udG9TdHJpbmcoKS50cmltKCl9YCk7XG4gICAgY29uc29sZS53YXJuKFwiICBJbnN0YWxsIG1jcDJjbGkgZmlyc3Q6IHBpcCBpbnN0YWxsIG1jcDJjbGlcIik7XG4gIH0gZWxzZSB7XG4gICAgY29uc29sZS5sb2coXCIgIOKckyBtY3AyY2xpIEBjb250ZXh0LW1vZGUgYmFrZWQg4oCUIGFnZW50IGNhbiBub3cgY2FsbDogbWNwMmNsaSBAY29udGV4dC1tb2RlIDx0b29sPlwiKTtcbiAgfVxufVxuIl0sImZpbGUiOiIvc3JjL2luaXQudHMifQ==
|
|
317
|
+
|
|
318
|
+
//# vitestCache=W3siZmlsZSI6IjEiLCJpZCI6IjEiLCJ1cmwiOiIyIiwiaW1wb3J0ZWRVcmxzIjoiMyIsIm1hcHBpbmdzIjpmYWxzZX0sIi9ob21lL3Jpei9mcmFwcGUtYnVpbGRlci9zcmMvaW5pdC50cyIsIi9zcmMvaW5pdC50cyIsW11d
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
const __vite_ssr_import_0__ = await __vite_ssr_import__("/node_modules/vitest/dist/index.js", {"importedNames":["describe","it","expect","vi","beforeEach","afterEach"]});
|
|
2
|
+
__vite_ssr_import_0__.vi.mock("node:os", async (importOriginal) => {
|
|
3
|
+
const actual = await importOriginal();
|
|
4
|
+
return {
|
|
5
|
+
...actual,
|
|
6
|
+
homedir: __vite_ssr_import_0__.vi.fn(() => actual.homedir())
|
|
7
|
+
};
|
|
8
|
+
});
|
|
9
|
+
__vite_ssr_import_0__.vi.mock("node:readline", () => ({ createInterface: __vite_ssr_import_0__.vi.fn(() => ({
|
|
10
|
+
question: __vite_ssr_import_0__.vi.fn((_q, cb) => {
|
|
11
|
+
cb(mockAnswers.shift() ?? "");
|
|
12
|
+
}),
|
|
13
|
+
close: __vite_ssr_import_0__.vi.fn()
|
|
14
|
+
})) }));
|
|
15
|
+
const __vi_import_0__ = await __vite_ssr_dynamic_import__("node:fs");
|
|
16
|
+
const __vi_import_1__ = await __vite_ssr_dynamic_import__("node:path");
|
|
17
|
+
const __vi_import_2__ = await __vite_ssr_dynamic_import__("node:os");
|
|
18
|
+
const __vi_import_3__ = await __vite_ssr_dynamic_import__("/src/init.ts");
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
// ── Top-level mocks — must be hoisted before any imports that use them ───────
|
|
23
|
+
// Shared answer queue — tests push answers before calling runInit
|
|
24
|
+
const mockAnswers = [];
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
// ── patchGitignore ───────────────────────────────────────────────────────────
|
|
28
|
+
(0,__vite_ssr_import_0__.describe)("patchGitignore", () => {
|
|
29
|
+
let tmpDir;
|
|
30
|
+
(0,__vite_ssr_import_0__.beforeEach)(() => {
|
|
31
|
+
tmpDir = __vi_import_0__.mkdtempSync(__vi_import_1__.join("/tmp", "fb-init-gitignore-"));
|
|
32
|
+
});
|
|
33
|
+
(0,__vite_ssr_import_0__.afterEach)(() => {
|
|
34
|
+
__vi_import_0__.rmSync(tmpDir, {
|
|
35
|
+
recursive: true,
|
|
36
|
+
force: true
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
(0,__vite_ssr_import_0__.it)("creates .gitignore from scratch when absent and adds the entry", () => {
|
|
40
|
+
const result = __vi_import_3__.patchGitignore(tmpDir, ".frappe-builder-config.json");
|
|
41
|
+
(0,__vite_ssr_import_0__.expect)(result).toBe("created");
|
|
42
|
+
const content = __vi_import_0__.readFileSync(__vi_import_1__.join(tmpDir, ".gitignore"), "utf-8");
|
|
43
|
+
(0,__vite_ssr_import_0__.expect)(content).toContain(".frappe-builder-config.json");
|
|
44
|
+
});
|
|
45
|
+
(0,__vite_ssr_import_0__.it)("appends line when .gitignore exists but entry is absent", () => {
|
|
46
|
+
__vi_import_0__.writeFileSync(__vi_import_1__.join(tmpDir, ".gitignore"), "node_modules\n.env\n", "utf-8");
|
|
47
|
+
const result = __vi_import_3__.patchGitignore(tmpDir, ".frappe-builder-config.json");
|
|
48
|
+
(0,__vite_ssr_import_0__.expect)(result).toBe("patched");
|
|
49
|
+
const content = __vi_import_0__.readFileSync(__vi_import_1__.join(tmpDir, ".gitignore"), "utf-8");
|
|
50
|
+
const lines = content.split("\n");
|
|
51
|
+
(0,__vite_ssr_import_0__.expect)(lines).toContain(".frappe-builder-config.json");
|
|
52
|
+
(0,__vite_ssr_import_0__.expect)(lines).toContain("node_modules");
|
|
53
|
+
(0,__vite_ssr_import_0__.expect)(lines).toContain(".env");
|
|
54
|
+
});
|
|
55
|
+
(0,__vite_ssr_import_0__.it)("does NOT duplicate entry when already present", () => {
|
|
56
|
+
__vi_import_0__.writeFileSync(__vi_import_1__.join(tmpDir, ".gitignore"), "node_modules\n.frappe-builder-config.json\n.env\n", "utf-8");
|
|
57
|
+
const result = __vi_import_3__.patchGitignore(tmpDir, ".frappe-builder-config.json");
|
|
58
|
+
(0,__vite_ssr_import_0__.expect)(result).toBe("already-present");
|
|
59
|
+
const content = __vi_import_0__.readFileSync(__vi_import_1__.join(tmpDir, ".gitignore"), "utf-8");
|
|
60
|
+
const count = content.split("\n").filter((l) => l === ".frappe-builder-config.json").length;
|
|
61
|
+
(0,__vite_ssr_import_0__.expect)(count).toBe(1);
|
|
62
|
+
});
|
|
63
|
+
(0,__vite_ssr_import_0__.it)("does NOT accept a glob pattern as a match — requires exact filename", () => {
|
|
64
|
+
__vi_import_0__.writeFileSync(__vi_import_1__.join(tmpDir, ".gitignore"), "*.json\n", "utf-8");
|
|
65
|
+
const result = __vi_import_3__.patchGitignore(tmpDir, ".frappe-builder-config.json");
|
|
66
|
+
(0,__vite_ssr_import_0__.expect)(result).toBe("patched");
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
// ── runInit — file writes ────────────────────────────────────────────────────
|
|
70
|
+
(0,__vite_ssr_import_0__.describe)("runInit — file writes", () => {
|
|
71
|
+
let projectDir;
|
|
72
|
+
let homeDir;
|
|
73
|
+
(0,__vite_ssr_import_0__.beforeEach)(() => {
|
|
74
|
+
projectDir = __vi_import_0__.mkdtempSync(__vi_import_1__.join("/tmp", "fb-init-proj-"));
|
|
75
|
+
homeDir = __vi_import_0__.mkdtempSync(__vi_import_1__.join("/tmp", "fb-init-home-"));
|
|
76
|
+
__vite_ssr_import_0__.vi.mocked(__vi_import_2__.homedir).mockReturnValue(homeDir);
|
|
77
|
+
mockAnswers.length = 0;
|
|
78
|
+
});
|
|
79
|
+
(0,__vite_ssr_import_0__.afterEach)(() => {
|
|
80
|
+
__vite_ssr_import_0__.vi.clearAllMocks();
|
|
81
|
+
__vi_import_0__.rmSync(projectDir, {
|
|
82
|
+
recursive: true,
|
|
83
|
+
force: true
|
|
84
|
+
});
|
|
85
|
+
__vi_import_0__.rmSync(homeDir, {
|
|
86
|
+
recursive: true,
|
|
87
|
+
force: true
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
(0,__vite_ssr_import_0__.it)("writes global config to ~/.frappe-builder/config.json", async () => {
|
|
91
|
+
// Answers: llm_api_key, site_url, api_key, api_secret
|
|
92
|
+
mockAnswers.push("sk-test", "http://erp.localhost", "key123", "secret456");
|
|
93
|
+
await __vi_import_3__.runInit({ projectRoot: projectDir });
|
|
94
|
+
const configPath = __vi_import_1__.join(homeDir, ".frappe-builder", "config.json");
|
|
95
|
+
(0,__vite_ssr_import_0__.expect)(__vi_import_0__.existsSync(configPath)).toBe(true);
|
|
96
|
+
const cfg = JSON.parse(__vi_import_0__.readFileSync(configPath, "utf-8"));
|
|
97
|
+
(0,__vite_ssr_import_0__.expect)(cfg.llm_api_key).toBe("sk-test");
|
|
98
|
+
});
|
|
99
|
+
(0,__vite_ssr_import_0__.it)("writes project config to {projectRoot}/.frappe-builder-config.json", async () => {
|
|
100
|
+
mockAnswers.push("sk-test", "http://erp.localhost", "key123", "secret456");
|
|
101
|
+
await __vi_import_3__.runInit({ projectRoot: projectDir });
|
|
102
|
+
const configPath = __vi_import_1__.join(projectDir, ".frappe-builder-config.json");
|
|
103
|
+
(0,__vite_ssr_import_0__.expect)(__vi_import_0__.existsSync(configPath)).toBe(true);
|
|
104
|
+
const cfg = JSON.parse(__vi_import_0__.readFileSync(configPath, "utf-8"));
|
|
105
|
+
(0,__vite_ssr_import_0__.expect)(cfg.site_url).toBe("http://erp.localhost");
|
|
106
|
+
(0,__vite_ssr_import_0__.expect)(cfg.api_key).toBe("key123");
|
|
107
|
+
(0,__vite_ssr_import_0__.expect)(cfg.api_secret).toBe("secret456");
|
|
108
|
+
});
|
|
109
|
+
(0,__vite_ssr_import_0__.it)("patches .gitignore automatically", async () => {
|
|
110
|
+
mockAnswers.push("sk-test", "http://erp.localhost", "key123", "secret456");
|
|
111
|
+
await __vi_import_3__.runInit({ projectRoot: projectDir });
|
|
112
|
+
const content = __vi_import_0__.readFileSync(__vi_import_1__.join(projectDir, ".gitignore"), "utf-8");
|
|
113
|
+
(0,__vite_ssr_import_0__.expect)(content).toContain(".frappe-builder-config.json");
|
|
114
|
+
});
|
|
115
|
+
(0,__vite_ssr_import_0__.it)("creates ~/.frappe-builder/ directory if it does not exist", async () => {
|
|
116
|
+
mockAnswers.push("", "http://site.localhost", "k", "s");
|
|
117
|
+
await __vi_import_3__.runInit({ projectRoot: projectDir });
|
|
118
|
+
(0,__vite_ssr_import_0__.expect)(__vi_import_0__.existsSync(__vi_import_1__.join(homeDir, ".frappe-builder"))).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
// ── runInit — overwrite guard ────────────────────────────────────────────────
|
|
122
|
+
(0,__vite_ssr_import_0__.describe)("runInit — overwrite guard", () => {
|
|
123
|
+
let projectDir;
|
|
124
|
+
let homeDir;
|
|
125
|
+
(0,__vite_ssr_import_0__.beforeEach)(() => {
|
|
126
|
+
projectDir = __vi_import_0__.mkdtempSync(__vi_import_1__.join("/tmp", "fb-init-guard-proj-"));
|
|
127
|
+
homeDir = __vi_import_0__.mkdtempSync(__vi_import_1__.join("/tmp", "fb-init-guard-home-"));
|
|
128
|
+
__vite_ssr_import_0__.vi.mocked(__vi_import_2__.homedir).mockReturnValue(homeDir);
|
|
129
|
+
mockAnswers.length = 0;
|
|
130
|
+
});
|
|
131
|
+
(0,__vite_ssr_import_0__.afterEach)(() => {
|
|
132
|
+
__vite_ssr_import_0__.vi.clearAllMocks();
|
|
133
|
+
__vi_import_0__.rmSync(projectDir, {
|
|
134
|
+
recursive: true,
|
|
135
|
+
force: true
|
|
136
|
+
});
|
|
137
|
+
__vi_import_0__.rmSync(homeDir, {
|
|
138
|
+
recursive: true,
|
|
139
|
+
force: true
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
(0,__vite_ssr_import_0__.it)("does NOT overwrite existing global config when user declines", async () => {
|
|
143
|
+
const configDir = __vi_import_1__.join(homeDir, ".frappe-builder");
|
|
144
|
+
__vi_import_0__.mkdirSync(configDir, { recursive: true });
|
|
145
|
+
const globalPath = __vi_import_1__.join(configDir, "config.json");
|
|
146
|
+
__vi_import_0__.writeFileSync(globalPath, JSON.stringify({ llm_api_key: "original-key" }), "utf-8");
|
|
147
|
+
// Overwrite global? → "n" | Overwrite project? → "n"
|
|
148
|
+
mockAnswers.push("n", "n");
|
|
149
|
+
await __vi_import_3__.runInit({ projectRoot: projectDir });
|
|
150
|
+
const cfg = JSON.parse(__vi_import_0__.readFileSync(globalPath, "utf-8"));
|
|
151
|
+
(0,__vite_ssr_import_0__.expect)(cfg.llm_api_key).toBe("original-key");
|
|
152
|
+
});
|
|
153
|
+
(0,__vite_ssr_import_0__.it)("overwrites existing global config when user confirms", async () => {
|
|
154
|
+
const configDir = __vi_import_1__.join(homeDir, ".frappe-builder");
|
|
155
|
+
__vi_import_0__.mkdirSync(configDir, { recursive: true });
|
|
156
|
+
const globalPath = __vi_import_1__.join(configDir, "config.json");
|
|
157
|
+
__vi_import_0__.writeFileSync(globalPath, JSON.stringify({ llm_api_key: "old-key" }), "utf-8");
|
|
158
|
+
// Overwrite global? → "y", new llm_key, site_url, api_key, api_secret
|
|
159
|
+
mockAnswers.push("y", "new-key", "http://site.localhost", "k", "s");
|
|
160
|
+
await __vi_import_3__.runInit({ projectRoot: projectDir });
|
|
161
|
+
const cfg = JSON.parse(__vi_import_0__.readFileSync(globalPath, "utf-8"));
|
|
162
|
+
(0,__vite_ssr_import_0__.expect)(cfg.llm_api_key).toBe("new-key");
|
|
163
|
+
});
|
|
164
|
+
(0,__vite_ssr_import_0__.it)("does NOT overwrite existing project config when user declines", async () => {
|
|
165
|
+
const projConfigPath = __vi_import_1__.join(projectDir, ".frappe-builder-config.json");
|
|
166
|
+
__vi_import_0__.writeFileSync(projConfigPath, JSON.stringify({
|
|
167
|
+
site_url: "http://original.localhost",
|
|
168
|
+
api_key: "orig",
|
|
169
|
+
api_secret: "s"
|
|
170
|
+
}), "utf-8");
|
|
171
|
+
// llm_api_key prompt (no existing global), overwrite project? → "n"
|
|
172
|
+
mockAnswers.push("sk-new", "n");
|
|
173
|
+
await __vi_import_3__.runInit({ projectRoot: projectDir });
|
|
174
|
+
const cfg = JSON.parse(__vi_import_0__.readFileSync(projConfigPath, "utf-8"));
|
|
175
|
+
(0,__vite_ssr_import_0__.expect)(cfg.site_url).toBe("http://original.localhost");
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
// ── runInit — clean exit ─────────────────────────────────────────────────────
|
|
179
|
+
(0,__vite_ssr_import_0__.describe)("runInit — clean exit", () => {
|
|
180
|
+
let projectDir;
|
|
181
|
+
let homeDir;
|
|
182
|
+
(0,__vite_ssr_import_0__.beforeEach)(() => {
|
|
183
|
+
projectDir = __vi_import_0__.mkdtempSync(__vi_import_1__.join("/tmp", "fb-init-exit-"));
|
|
184
|
+
homeDir = __vi_import_0__.mkdtempSync(__vi_import_1__.join("/tmp", "fb-init-exit-home-"));
|
|
185
|
+
__vite_ssr_import_0__.vi.mocked(__vi_import_2__.homedir).mockReturnValue(homeDir);
|
|
186
|
+
mockAnswers.length = 0;
|
|
187
|
+
});
|
|
188
|
+
(0,__vite_ssr_import_0__.afterEach)(() => {
|
|
189
|
+
__vite_ssr_import_0__.vi.clearAllMocks();
|
|
190
|
+
__vi_import_0__.rmSync(projectDir, {
|
|
191
|
+
recursive: true,
|
|
192
|
+
force: true
|
|
193
|
+
});
|
|
194
|
+
__vi_import_0__.rmSync(homeDir, {
|
|
195
|
+
recursive: true,
|
|
196
|
+
force: true
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
(0,__vite_ssr_import_0__.it)("resolves without throwing when all prompts return empty strings", async () => {
|
|
200
|
+
mockAnswers.push("", "", "", "");
|
|
201
|
+
await (0,__vite_ssr_import_0__.expect)(__vi_import_3__.runInit({ projectRoot: projectDir })).resolves.toBeUndefined();
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
//# sourceMappingSource=vite-generated
|
|
205
|
+
//# sourceMappingURL=data:application/json;base64,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b3JlTGlzdCI6W10sInNvdXJjZXMiOlsiaW5pdC50ZXN0LnRzIl0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IGRlc2NyaWJlLCBpdCwgZXhwZWN0LCB2aSwgYmVmb3JlRWFjaCwgYWZ0ZXJFYWNoIH0gZnJvbSBcInZpdGVzdFwiO1xuaW1wb3J0IHsgbWtkdGVtcFN5bmMsIHJtU3luYywgcmVhZEZpbGVTeW5jLCB3cml0ZUZpbGVTeW5jLCBleGlzdHNTeW5jLCBta2RpclN5bmMgfSBmcm9tIFwibm9kZTpmc1wiO1xuaW1wb3J0IHsgam9pbiB9IGZyb20gXCJub2RlOnBhdGhcIjtcblxuLy8g4pSA4pSAIFRvcC1sZXZlbCBtb2NrcyDigJQgbXVzdCBiZSBob2lzdGVkIGJlZm9yZSBhbnkgaW1wb3J0cyB0aGF0IHVzZSB0aGVtIOKUgOKUgOKUgOKUgOKUgOKUgOKUgFxuXG52aS5tb2NrKFwibm9kZTpvc1wiLCBhc3luYyAoaW1wb3J0T3JpZ2luYWwpID0+IHtcbiAgY29uc3QgYWN0dWFsID0gYXdhaXQgaW1wb3J0T3JpZ2luYWw8dHlwZW9mIGltcG9ydChcIm5vZGU6b3NcIik+KCk7XG4gIHJldHVybiB7IC4uLmFjdHVhbCwgaG9tZWRpcjogdmkuZm4oKCkgPT4gYWN0dWFsLmhvbWVkaXIoKSkgfTtcbn0pO1xuXG4vLyBTaGFyZWQgYW5zd2VyIHF1ZXVlIOKAlCB0ZXN0cyBwdXNoIGFuc3dlcnMgYmVmb3JlIGNhbGxpbmcgcnVuSW5pdFxuY29uc3QgbW9ja0Fuc3dlcnM6IHN0cmluZ1tdID0gW107XG5cbnZpLm1vY2soXCJub2RlOnJlYWRsaW5lXCIsICgpID0+ICh7XG4gIGNyZWF0ZUludGVyZmFjZTogdmkuZm4oKCkgPT4gKHtcbiAgICBxdWVzdGlvbjogdmkuZm4oKF9xOiBzdHJpbmcsIGNiOiAoYTogc3RyaW5nKSA9PiB2b2lkKSA9PiB7XG4gICAgICBjYihtb2NrQW5zd2Vycy5zaGlmdCgpID8/IFwiXCIpO1xuICAgIH0pLFxuICAgIGNsb3NlOiB2aS5mbigpLFxuICB9KSksXG59KSk7XG5cbmltcG9ydCAqIGFzIG9zIGZyb20gXCJub2RlOm9zXCI7XG5pbXBvcnQgeyBwYXRjaEdpdGlnbm9yZSwgcnVuSW5pdCB9IGZyb20gXCIuL2luaXQuanNcIjtcblxuLy8g4pSA4pSAIHBhdGNoR2l0aWdub3JlIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgFxuXG5kZXNjcmliZShcInBhdGNoR2l0aWdub3JlXCIsICgpID0+IHtcbiAgbGV0IHRtcERpcjogc3RyaW5nO1xuXG4gIGJlZm9yZUVhY2goKCkgPT4ge1xuICAgIHRtcERpciA9IG1rZHRlbXBTeW5jKGpvaW4oXCIvdG1wXCIsIFwiZmItaW5pdC1naXRpZ25vcmUtXCIpKTtcbiAgfSk7XG5cbiAgYWZ0ZXJFYWNoKCgpID0+IHtcbiAgICBybVN5bmModG1wRGlyLCB7IHJlY3Vyc2l2ZTogdHJ1ZSwgZm9yY2U6IHRydWUgfSk7XG4gIH0pO1xuXG4gIGl0KFwiY3JlYXRlcyAuZ2l0aWdub3JlIGZyb20gc2NyYXRjaCB3aGVuIGFic2VudCBhbmQgYWRkcyB0aGUgZW50cnlcIiwgKCkgPT4ge1xuICAgIGNvbnN0IHJlc3VsdCA9IHBhdGNoR2l0aWdub3JlKHRtcERpciwgXCIuZnJhcHBlLWJ1aWxkZXItY29uZmlnLmpzb25cIik7XG4gICAgZXhwZWN0KHJlc3VsdCkudG9CZShcImNyZWF0ZWRcIik7XG4gICAgY29uc3QgY29udGVudCA9IHJlYWRGaWxlU3luYyhqb2luKHRtcERpciwgXCIuZ2l0aWdub3JlXCIpLCBcInV0Zi04XCIpO1xuICAgIGV4cGVjdChjb250ZW50KS50b0NvbnRhaW4oXCIuZnJhcHBlLWJ1aWxkZXItY29uZmlnLmpzb25cIik7XG4gIH0pO1xuXG4gIGl0KFwiYXBwZW5kcyBsaW5lIHdoZW4gLmdpdGlnbm9yZSBleGlzdHMgYnV0IGVudHJ5IGlzIGFic2VudFwiLCAoKSA9PiB7XG4gICAgd3JpdGVGaWxlU3luYyhqb2luKHRtcERpciwgXCIuZ2l0aWdub3JlXCIpLCBcIm5vZGVfbW9kdWxlc1xcbi5lbnZcXG5cIiwgXCJ1dGYtOFwiKTtcbiAgICBjb25zdCByZXN1bHQgPSBwYXRjaEdpdGlnbm9yZSh0bXBEaXIsIFwiLmZyYXBwZS1idWlsZGVyLWNvbmZpZy5qc29uXCIpO1xuICAgIGV4cGVjdChyZXN1bHQpLnRvQmUoXCJwYXRjaGVkXCIpO1xuICAgIGNvbnN0IGNvbnRlbnQgPSByZWFkRmlsZVN5bmMoam9pbih0bXBEaXIsIFwiLmdpdGlnbm9yZVwiKSwgXCJ1dGYtOFwiKTtcbiAgICBjb25zdCBsaW5lcyA9IGNvbnRlbnQuc3BsaXQoXCJcXG5cIik7XG4gICAgZXhwZWN0KGxpbmVzKS50b0NvbnRhaW4oXCIuZnJhcHBlLWJ1aWxkZXItY29uZmlnLmpzb25cIik7XG4gICAgZXhwZWN0KGxpbmVzKS50b0NvbnRhaW4oXCJub2RlX21vZHVsZXNcIik7XG4gICAgZXhwZWN0KGxpbmVzKS50b0NvbnRhaW4oXCIuZW52XCIpO1xuICB9KTtcblxuICBpdChcImRvZXMgTk9UIGR1cGxpY2F0ZSBlbnRyeSB3aGVuIGFscmVhZHkgcHJlc2VudFwiLCAoKSA9PiB7XG4gICAgd3JpdGVGaWxlU3luYyhcbiAgICAgIGpvaW4odG1wRGlyLCBcIi5naXRpZ25vcmVcIiksXG4gICAgICBcIm5vZGVfbW9kdWxlc1xcbi5mcmFwcGUtYnVpbGRlci1jb25maWcuanNvblxcbi5lbnZcXG5cIixcbiAgICAgIFwidXRmLThcIlxuICAgICk7XG4gICAgY29uc3QgcmVzdWx0ID0gcGF0Y2hHaXRpZ25vcmUodG1wRGlyLCBcIi5mcmFwcGUtYnVpbGRlci1jb25maWcuanNvblwiKTtcbiAgICBleHBlY3QocmVzdWx0KS50b0JlKFwiYWxyZWFkeS1wcmVzZW50XCIpO1xuICAgIGNvbnN0IGNvbnRlbnQgPSByZWFkRmlsZVN5bmMoam9pbih0bXBEaXIsIFwiLmdpdGlnbm9yZVwiKSwgXCJ1dGYtOFwiKTtcbiAgICBjb25zdCBjb3VudCA9IGNvbnRlbnQuc3BsaXQoXCJcXG5cIikuZmlsdGVyKChsKSA9PiBsID09PSBcIi5mcmFwcGUtYnVpbGRlci1jb25maWcuanNvblwiKS5sZW5ndGg7XG4gICAgZXhwZWN0KGNvdW50KS50b0JlKDEpO1xuICB9KTtcblxuICBpdChcImRvZXMgTk9UIGFjY2VwdCBhIGdsb2IgcGF0dGVybiBhcyBhIG1hdGNoIOKAlCByZXF1aXJlcyBleGFjdCBmaWxlbmFtZVwiLCAoKSA9PiB7XG4gICAgd3JpdGVGaWxlU3luYyhqb2luKHRtcERpciwgXCIuZ2l0aWdub3JlXCIpLCBcIiouanNvblxcblwiLCBcInV0Zi04XCIpO1xuICAgIGNvbnN0IHJlc3VsdCA9IHBhdGNoR2l0aWdub3JlKHRtcERpciwgXCIuZnJhcHBlLWJ1aWxkZXItY29uZmlnLmpzb25cIik7XG4gICAgZXhwZWN0KHJlc3VsdCkudG9CZShcInBhdGNoZWRcIik7XG4gIH0pO1xufSk7XG5cbi8vIOKUgOKUgCBydW5Jbml0IOKAlCBmaWxlIHdyaXRlcyDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIBcblxuZGVzY3JpYmUoXCJydW5Jbml0IOKAlCBmaWxlIHdyaXRlc1wiLCAoKSA9PiB7XG4gIGxldCBwcm9qZWN0RGlyOiBzdHJpbmc7XG4gIGxldCBob21lRGlyOiBzdHJpbmc7XG5cbiAgYmVmb3JlRWFjaCgoKSA9PiB7XG4gICAgcHJvamVjdERpciA9IG1rZHRlbXBTeW5jKGpvaW4oXCIvdG1wXCIsIFwiZmItaW5pdC1wcm9qLVwiKSk7XG4gICAgaG9tZURpciA9IG1rZHRlbXBTeW5jKGpvaW4oXCIvdG1wXCIsIFwiZmItaW5pdC1ob21lLVwiKSk7XG4gICAgdmkubW9ja2VkKG9zLmhvbWVkaXIpLm1vY2tSZXR1cm5WYWx1ZShob21lRGlyKTtcbiAgICBtb2NrQW5zd2Vycy5sZW5ndGggPSAwO1xuICB9KTtcblxuICBhZnRlckVhY2goKCkgPT4ge1xuICAgIHZpLmNsZWFyQWxsTW9ja3MoKTtcbiAgICBybVN5bmMocHJvamVjdERpciwgeyByZWN1cnNpdmU6IHRydWUsIGZvcmNlOiB0cnVlIH0pO1xuICAgIHJtU3luYyhob21lRGlyLCB7IHJlY3Vyc2l2ZTogdHJ1ZSwgZm9yY2U6IHRydWUgfSk7XG4gIH0pO1xuXG4gIGl0KFwid3JpdGVzIGdsb2JhbCBjb25maWcgdG8gfi8uZnJhcHBlLWJ1aWxkZXIvY29uZmlnLmpzb25cIiwgYXN5bmMgKCkgPT4ge1xuICAgIC8vIEFuc3dlcnM6IGxsbV9hcGlfa2V5LCBzaXRlX3VybCwgYXBpX2tleSwgYXBpX3NlY3JldFxuICAgIG1vY2tBbnN3ZXJzLnB1c2goXCJzay10ZXN0XCIsIFwiaHR0cDovL2VycC5sb2NhbGhvc3RcIiwgXCJrZXkxMjNcIiwgXCJzZWNyZXQ0NTZcIik7XG4gICAgYXdhaXQgcnVuSW5pdCh7IHByb2plY3RSb290OiBwcm9qZWN0RGlyIH0pO1xuXG4gICAgY29uc3QgY29uZmlnUGF0aCA9IGpvaW4oaG9tZURpciwgXCIuZnJhcHBlLWJ1aWxkZXJcIiwgXCJjb25maWcuanNvblwiKTtcbiAgICBleHBlY3QoZXhpc3RzU3luYyhjb25maWdQYXRoKSkudG9CZSh0cnVlKTtcbiAgICBjb25zdCBjZmcgPSBKU09OLnBhcnNlKHJlYWRGaWxlU3luYyhjb25maWdQYXRoLCBcInV0Zi04XCIpKSBhcyB7IGxsbV9hcGlfa2V5OiBzdHJpbmcgfTtcbiAgICBleHBlY3QoY2ZnLmxsbV9hcGlfa2V5KS50b0JlKFwic2stdGVzdFwiKTtcbiAgfSk7XG5cbiAgaXQoXCJ3cml0ZXMgcHJvamVjdCBjb25maWcgdG8ge3Byb2plY3RSb290fS8uZnJhcHBlLWJ1aWxkZXItY29uZmlnLmpzb25cIiwgYXN5bmMgKCkgPT4ge1xuICAgIG1vY2tBbnN3ZXJzLnB1c2goXCJzay10ZXN0XCIsIFwiaHR0cDovL2VycC5sb2NhbGhvc3RcIiwgXCJrZXkxMjNcIiwgXCJzZWNyZXQ0NTZcIik7XG4gICAgYXdhaXQgcnVuSW5pdCh7IHByb2plY3RSb290OiBwcm9qZWN0RGlyIH0pO1xuXG4gICAgY29uc3QgY29uZmlnUGF0aCA9IGpvaW4ocHJvamVjdERpciwgXCIuZnJhcHBlLWJ1aWxkZXItY29uZmlnLmpzb25cIik7XG4gICAgZXhwZWN0KGV4aXN0c1N5bmMoY29uZmlnUGF0aCkpLnRvQmUodHJ1ZSk7XG4gICAgY29uc3QgY2ZnID0gSlNPTi5wYXJzZShyZWFkRmlsZVN5bmMoY29uZmlnUGF0aCwgXCJ1dGYtOFwiKSkgYXMge1xuICAgICAgc2l0ZV91cmw6IHN0cmluZztcbiAgICAgIGFwaV9rZXk6IHN0cmluZztcbiAgICAgIGFwaV9zZWNyZXQ6IHN0cmluZztcbiAgICB9O1xuICAgIGV4cGVjdChjZmcuc2l0ZV91cmwpLnRvQmUoXCJodHRwOi8vZXJwLmxvY2FsaG9zdFwiKTtcbiAgICBleHBlY3QoY2ZnLmFwaV9rZXkpLnRvQmUoXCJrZXkxMjNcIik7XG4gICAgZXhwZWN0KGNmZy5hcGlfc2VjcmV0KS50b0JlKFwic2VjcmV0NDU2XCIpO1xuICB9KTtcblxuICBpdChcInBhdGNoZXMgLmdpdGlnbm9yZSBhdXRvbWF0aWNhbGx5XCIsIGFzeW5jICgpID0+IHtcbiAgICBtb2NrQW5zd2Vycy5wdXNoKFwic2stdGVzdFwiLCBcImh0dHA6Ly9lcnAubG9jYWxob3N0XCIsIFwia2V5MTIzXCIsIFwic2VjcmV0NDU2XCIpO1xuICAgIGF3YWl0IHJ1bkluaXQoeyBwcm9qZWN0Um9vdDogcHJvamVjdERpciB9KTtcblxuICAgIGNvbnN0IGNvbnRlbnQgPSByZWFkRmlsZVN5bmMoam9pbihwcm9qZWN0RGlyLCBcIi5naXRpZ25vcmVcIiksIFwidXRmLThcIik7XG4gICAgZXhwZWN0KGNvbnRlbnQpLnRvQ29udGFpbihcIi5mcmFwcGUtYnVpbGRlci1jb25maWcuanNvblwiKTtcbiAgfSk7XG5cbiAgaXQoXCJjcmVhdGVzIH4vLmZyYXBwZS1idWlsZGVyLyBkaXJlY3RvcnkgaWYgaXQgZG9lcyBub3QgZXhpc3RcIiwgYXN5bmMgKCkgPT4ge1xuICAgIG1vY2tBbnN3ZXJzLnB1c2goXCJcIiwgXCJodHRwOi8vc2l0ZS5sb2NhbGhvc3RcIiwgXCJrXCIsIFwic1wiKTtcbiAgICBhd2FpdCBydW5Jbml0KHsgcHJvamVjdFJvb3Q6IHByb2plY3REaXIgfSk7XG5cbiAgICBleHBlY3QoZXhpc3RzU3luYyhqb2luKGhvbWVEaXIsIFwiLmZyYXBwZS1idWlsZGVyXCIpKSkudG9CZSh0cnVlKTtcbiAgfSk7XG59KTtcblxuLy8g4pSA4pSAIHJ1bkluaXQg4oCUIG92ZXJ3cml0ZSBndWFyZCDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIBcblxuZGVzY3JpYmUoXCJydW5Jbml0IOKAlCBvdmVyd3JpdGUgZ3VhcmRcIiwgKCkgPT4ge1xuICBsZXQgcHJvamVjdERpcjogc3RyaW5nO1xuICBsZXQgaG9tZURpcjogc3RyaW5nO1xuXG4gIGJlZm9yZUVhY2goKCkgPT4ge1xuICAgIHByb2plY3REaXIgPSBta2R0ZW1wU3luYyhqb2luKFwiL3RtcFwiLCBcImZiLWluaXQtZ3VhcmQtcHJvai1cIikpO1xuICAgIGhvbWVEaXIgPSBta2R0ZW1wU3luYyhqb2luKFwiL3RtcFwiLCBcImZiLWluaXQtZ3VhcmQtaG9tZS1cIikpO1xuICAgIHZpLm1vY2tlZChvcy5ob21lZGlyKS5tb2NrUmV0dXJuVmFsdWUoaG9tZURpcik7XG4gICAgbW9ja0Fuc3dlcnMubGVuZ3RoID0gMDtcbiAgfSk7XG5cbiAgYWZ0ZXJFYWNoKCgpID0+IHtcbiAgICB2aS5jbGVhckFsbE1vY2tzKCk7XG4gICAgcm1TeW5jKHByb2plY3REaXIsIHsgcmVjdXJzaXZlOiB0cnVlLCBmb3JjZTogdHJ1ZSB9KTtcbiAgICBybVN5bmMoaG9tZURpciwgeyByZWN1cnNpdmU6IHRydWUsIGZvcmNlOiB0cnVlIH0pO1xuICB9KTtcblxuICBpdChcImRvZXMgTk9UIG92ZXJ3cml0ZSBleGlzdGluZyBnbG9iYWwgY29uZmlnIHdoZW4gdXNlciBkZWNsaW5lc1wiLCBhc3luYyAoKSA9PiB7XG4gICAgY29uc3QgY29uZmlnRGlyID0gam9pbihob21lRGlyLCBcIi5mcmFwcGUtYnVpbGRlclwiKTtcbiAgICBta2RpclN5bmMoY29uZmlnRGlyLCB7IHJlY3Vyc2l2ZTogdHJ1ZSB9KTtcbiAgICBjb25zdCBnbG9iYWxQYXRoID0gam9pbihjb25maWdEaXIsIFwiY29uZmlnLmpzb25cIik7XG4gICAgd3JpdGVGaWxlU3luYyhnbG9iYWxQYXRoLCBKU09OLnN0cmluZ2lmeSh7IGxsbV9hcGlfa2V5OiBcIm9yaWdpbmFsLWtleVwiIH0pLCBcInV0Zi04XCIpO1xuXG4gICAgLy8gT3ZlcndyaXRlIGdsb2JhbD8g4oaSIFwiblwiIHwgT3ZlcndyaXRlIHByb2plY3Q/IOKGkiBcIm5cIlxuICAgIG1vY2tBbnN3ZXJzLnB1c2goXCJuXCIsIFwiblwiKTtcbiAgICBhd2FpdCBydW5Jbml0KHsgcHJvamVjdFJvb3Q6IHByb2plY3REaXIgfSk7XG5cbiAgICBjb25zdCBjZmcgPSBKU09OLnBhcnNlKHJlYWRGaWxlU3luYyhnbG9iYWxQYXRoLCBcInV0Zi04XCIpKSBhcyB7IGxsbV9hcGlfa2V5OiBzdHJpbmcgfTtcbiAgICBleHBlY3QoY2ZnLmxsbV9hcGlfa2V5KS50b0JlKFwib3JpZ2luYWwta2V5XCIpO1xuICB9KTtcblxuICBpdChcIm92ZXJ3cml0ZXMgZXhpc3RpbmcgZ2xvYmFsIGNvbmZpZyB3aGVuIHVzZXIgY29uZmlybXNcIiwgYXN5bmMgKCkgPT4ge1xuICAgIGNvbnN0IGNvbmZpZ0RpciA9IGpvaW4oaG9tZURpciwgXCIuZnJhcHBlLWJ1aWxkZXJcIik7XG4gICAgbWtkaXJTeW5jKGNvbmZpZ0RpciwgeyByZWN1cnNpdmU6IHRydWUgfSk7XG4gICAgY29uc3QgZ2xvYmFsUGF0aCA9IGpvaW4oY29uZmlnRGlyLCBcImNvbmZpZy5qc29uXCIpO1xuICAgIHdyaXRlRmlsZVN5bmMoZ2xvYmFsUGF0aCwgSlNPTi5zdHJpbmdpZnkoeyBsbG1fYXBpX2tleTogXCJvbGQta2V5XCIgfSksIFwidXRmLThcIik7XG5cbiAgICAvLyBPdmVyd3JpdGUgZ2xvYmFsPyDihpIgXCJ5XCIsIG5ldyBsbG1fa2V5LCBzaXRlX3VybCwgYXBpX2tleSwgYXBpX3NlY3JldFxuICAgIG1vY2tBbnN3ZXJzLnB1c2goXCJ5XCIsIFwibmV3LWtleVwiLCBcImh0dHA6Ly9zaXRlLmxvY2FsaG9zdFwiLCBcImtcIiwgXCJzXCIpO1xuICAgIGF3YWl0IHJ1bkluaXQoeyBwcm9qZWN0Um9vdDogcHJvamVjdERpciB9KTtcblxuICAgIGNvbnN0IGNmZyA9IEpTT04ucGFyc2UocmVhZEZpbGVTeW5jKGdsb2JhbFBhdGgsIFwidXRmLThcIikpIGFzIHsgbGxtX2FwaV9rZXk6IHN0cmluZyB9O1xuICAgIGV4cGVjdChjZmcubGxtX2FwaV9rZXkpLnRvQmUoXCJuZXcta2V5XCIpO1xuICB9KTtcblxuICBpdChcImRvZXMgTk9UIG92ZXJ3cml0ZSBleGlzdGluZyBwcm9qZWN0IGNvbmZpZyB3aGVuIHVzZXIgZGVjbGluZXNcIiwgYXN5bmMgKCkgPT4ge1xuICAgIGNvbnN0IHByb2pDb25maWdQYXRoID0gam9pbihwcm9qZWN0RGlyLCBcIi5mcmFwcGUtYnVpbGRlci1jb25maWcuanNvblwiKTtcbiAgICB3cml0ZUZpbGVTeW5jKFxuICAgICAgcHJvakNvbmZpZ1BhdGgsXG4gICAgICBKU09OLnN0cmluZ2lmeSh7IHNpdGVfdXJsOiBcImh0dHA6Ly9vcmlnaW5hbC5sb2NhbGhvc3RcIiwgYXBpX2tleTogXCJvcmlnXCIsIGFwaV9zZWNyZXQ6IFwic1wiIH0pLFxuICAgICAgXCJ1dGYtOFwiXG4gICAgKTtcblxuICAgIC8vIGxsbV9hcGlfa2V5IHByb21wdCAobm8gZXhpc3RpbmcgZ2xvYmFsKSwgb3ZlcndyaXRlIHByb2plY3Q/IOKGkiBcIm5cIlxuICAgIG1vY2tBbnN3ZXJzLnB1c2goXCJzay1uZXdcIiwgXCJuXCIpO1xuICAgIGF3YWl0IHJ1bkluaXQoeyBwcm9qZWN0Um9vdDogcHJvamVjdERpciB9KTtcblxuICAgIGNvbnN0IGNmZyA9IEpTT04ucGFyc2UocmVhZEZpbGVTeW5jKHByb2pDb25maWdQYXRoLCBcInV0Zi04XCIpKSBhcyB7IHNpdGVfdXJsOiBzdHJpbmcgfTtcbiAgICBleHBlY3QoY2ZnLnNpdGVfdXJsKS50b0JlKFwiaHR0cDovL29yaWdpbmFsLmxvY2FsaG9zdFwiKTtcbiAgfSk7XG59KTtcblxuLy8g4pSA4pSAIHJ1bkluaXQg4oCUIGNsZWFuIGV4aXQg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSAXG5cbmRlc2NyaWJlKFwicnVuSW5pdCDigJQgY2xlYW4gZXhpdFwiLCAoKSA9PiB7XG4gIGxldCBwcm9qZWN0RGlyOiBzdHJpbmc7XG4gIGxldCBob21lRGlyOiBzdHJpbmc7XG5cbiAgYmVmb3JlRWFjaCgoKSA9PiB7XG4gICAgcHJvamVjdERpciA9IG1rZHRlbXBTeW5jKGpvaW4oXCIvdG1wXCIsIFwiZmItaW5pdC1leGl0LVwiKSk7XG4gICAgaG9tZURpciA9IG1rZHRlbXBTeW5jKGpvaW4oXCIvdG1wXCIsIFwiZmItaW5pdC1leGl0LWhvbWUtXCIpKTtcbiAgICB2aS5tb2NrZWQob3MuaG9tZWRpcikubW9ja1JldHVyblZhbHVlKGhvbWVEaXIpO1xuICAgIG1vY2tBbnN3ZXJzLmxlbmd0aCA9IDA7XG4gIH0pO1xuXG4gIGFmdGVyRWFjaCgoKSA9PiB7XG4gICAgdmkuY2xlYXJBbGxNb2NrcygpO1xuICAgIHJtU3luYyhwcm9qZWN0RGlyLCB7IHJlY3Vyc2l2ZTogdHJ1ZSwgZm9yY2U6IHRydWUgfSk7XG4gICAgcm1TeW5jKGhvbWVEaXIsIHsgcmVjdXJzaXZlOiB0cnVlLCBmb3JjZTogdHJ1ZSB9KTtcbiAgfSk7XG5cbiAgaXQoXCJyZXNvbHZlcyB3aXRob3V0IHRocm93aW5nIHdoZW4gYWxsIHByb21wdHMgcmV0dXJuIGVtcHR5IHN0cmluZ3NcIiwgYXN5bmMgKCkgPT4ge1xuICAgIG1vY2tBbnN3ZXJzLnB1c2goXCJcIiwgXCJcIiwgXCJcIiwgXCJcIik7XG4gICAgYXdhaXQgZXhwZWN0KHJ1bkluaXQoeyBwcm9qZWN0Um9vdDogcHJvamVjdERpciB9KSkucmVzb2x2ZXMudG9CZVVuZGVmaW5lZCgpO1xuICB9KTtcbn0pO1xuIl0sImZpbGUiOiIvc3JjL2luaXQudGVzdC50cyJ9
|
|
206
|
+
|
|
207
|
+
//# vitestCache=W3siZmlsZSI6IjEiLCJpZCI6IjEiLCJ1cmwiOiIyIiwiaW1wb3J0ZWRVcmxzIjoiMyIsIm1hcHBpbmdzIjpmYWxzZX0sIi9ob21lL3Jpei9mcmFwcGUtYnVpbGRlci9zcmMvaW5pdC50ZXN0LnRzIiwiL3NyYy9pbml0LnRlc3QudHMiLFtdXQ==
|
package/config/defaults.ts
CHANGED
|
@@ -11,7 +11,6 @@ export interface AppConfig {
|
|
|
11
11
|
allowSubAgents: boolean;
|
|
12
12
|
requirePermission: boolean;
|
|
13
13
|
rules: SpawnRule[];
|
|
14
|
-
frappeMcpUrl?: string; // URL of Frappe MCP server for frappe_query (e.g. "http://localhost:8000")
|
|
15
14
|
defaultMode?: "full" | "quick"; // default feature mode: "quick" skips planning phases
|
|
16
15
|
chainModel?: string; // model for chain subprocess agents (inherits parent model when unset)
|
|
17
16
|
permissionMode?: PermissionMode; // agent autonomy level: auto | default | plan
|
package/config/loader.ts
CHANGED
|
@@ -1,96 +1,16 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { readFileSync, existsSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
4
4
|
import { defaults } from "./defaults.js";
|
|
5
5
|
import type { AppConfig } from "./defaults.js";
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export interface GlobalConfig {
|
|
10
|
-
llm_api_key: string;
|
|
11
|
-
llm_provider?: string;
|
|
12
|
-
[key: string]: unknown;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface ProjectConfig {
|
|
16
|
-
site_url: string;
|
|
17
|
-
api_key: string;
|
|
18
|
-
api_secret: string;
|
|
19
|
-
[key: string]: unknown;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface CredentialConfig {
|
|
23
|
-
global: GlobalConfig;
|
|
24
|
-
project: ProjectConfig;
|
|
25
|
-
sessionLogDir: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Always ~/.frappe-builder/sessions/ — never the project directory (NFR9). */
|
|
7
|
+
/** Always ~/.frappe-builder/sessions/ — never the project directory. */
|
|
29
8
|
export const SESSION_LOG_DIR = join(homedir(), ".frappe-builder", "sessions");
|
|
30
9
|
|
|
31
|
-
const GITIGNORE_ERROR =
|
|
32
|
-
"Site credentials file is not gitignored. Add .frappe-builder-config.json to .gitignore before proceeding.";
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Validates that {projectRoot}/.gitignore lists .frappe-builder-config.json.
|
|
36
|
-
* Throws with the exact AC error message if missing or absent.
|
|
37
|
-
*/
|
|
38
|
-
export function validateGitignore(projectRoot: string): void {
|
|
39
|
-
const gitignorePath = join(projectRoot, ".gitignore");
|
|
40
|
-
if (!existsSync(gitignorePath)) {
|
|
41
|
-
throw new Error(GITIGNORE_ERROR);
|
|
42
|
-
}
|
|
43
|
-
const contents = readFileSync(gitignorePath, "utf8");
|
|
44
|
-
const lines = contents.split("\n").map((l) => l.trim());
|
|
45
|
-
if (!lines.includes(".frappe-builder-config.json")) {
|
|
46
|
-
throw new Error(GITIGNORE_ERROR);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Reads LLM API keys from ~/.frappe-builder/config.json (user-global, never project).
|
|
52
|
-
* Creates ~/.frappe-builder/ on first run. Returns empty defaults if file absent.
|
|
53
|
-
*/
|
|
54
|
-
export function loadGlobalConfig(): GlobalConfig {
|
|
55
|
-
const configDir = join(homedir(), ".frappe-builder");
|
|
56
|
-
mkdirSync(configDir, { recursive: true });
|
|
57
|
-
const configPath = join(configDir, "config.json");
|
|
58
|
-
if (!existsSync(configPath)) return { llm_api_key: "" };
|
|
59
|
-
try {
|
|
60
|
-
return JSON.parse(readFileSync(configPath, "utf8")) as GlobalConfig;
|
|
61
|
-
} catch {
|
|
62
|
-
return { llm_api_key: "" };
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Reads Frappe site credentials from {projectRoot}/.frappe-builder-config.json.
|
|
68
|
-
* Returns empty defaults if file absent (caller should surface a warning).
|
|
69
|
-
*/
|
|
70
|
-
export function loadProjectConfig(projectRoot: string): ProjectConfig {
|
|
71
|
-
const configPath = join(projectRoot, ".frappe-builder-config.json");
|
|
72
|
-
if (!existsSync(configPath)) return { site_url: "", api_key: "", api_secret: "" };
|
|
73
|
-
try {
|
|
74
|
-
return JSON.parse(readFileSync(configPath, "utf8")) as ProjectConfig;
|
|
75
|
-
} catch {
|
|
76
|
-
return { site_url: "", api_key: "", api_secret: "" };
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Full credential loader: validates gitignore, loads global + project configs.
|
|
82
|
-
* Throws if .frappe-builder-config.json is not gitignored.
|
|
83
|
-
* API keys are only ever read from ~/.frappe-builder/ — never from projectRoot.
|
|
84
|
-
*/
|
|
85
|
-
export function loadCredentials(projectRoot: string): CredentialConfig {
|
|
86
|
-
validateGitignore(projectRoot);
|
|
87
|
-
const global = loadGlobalConfig();
|
|
88
|
-
const project = loadProjectConfig(projectRoot);
|
|
89
|
-
return { global, project, sessionLogDir: SESSION_LOG_DIR };
|
|
90
|
-
}
|
|
91
|
-
|
|
92
10
|
/**
|
|
93
11
|
* Loads ~/.frappe-builder/config.json and merges with defaults.
|
|
12
|
+
* Stores agent behaviour preferences (permissionMode, requirePermission, etc.).
|
|
13
|
+
* Site credentials live in the session DB — not in this file.
|
|
94
14
|
* Never throws — returns defaults on any read/parse failure.
|
|
95
15
|
*/
|
|
96
16
|
export function loadConfig(): AppConfig {
|
|
@@ -103,3 +23,17 @@ export function loadConfig(): AppConfig {
|
|
|
103
23
|
return { ...defaults };
|
|
104
24
|
}
|
|
105
25
|
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Writes a partial config merged over the current config to ~/.frappe-builder/config.json.
|
|
29
|
+
* Creates the directory if needed. Non-fatal — never throws.
|
|
30
|
+
*/
|
|
31
|
+
export function saveConfig(partial: Partial<AppConfig>): void {
|
|
32
|
+
try {
|
|
33
|
+
const configDir = join(homedir(), ".frappe-builder");
|
|
34
|
+
mkdirSync(configDir, { recursive: true });
|
|
35
|
+
const configPath = join(configDir, "config.json");
|
|
36
|
+
const merged = { ...loadConfig(), ...partial };
|
|
37
|
+
writeFileSync(configPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
38
|
+
} catch { /* non-fatal */ }
|
|
39
|
+
}
|
package/dist/cli.mjs
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { resolve } from "node:path";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
7
8
|
//#region src/cli.ts
|
|
8
9
|
/**
|
|
9
10
|
* src/cli.ts — frappe-builder CLI entry point
|
|
10
11
|
*
|
|
11
12
|
* Commands:
|
|
12
|
-
* frappe-builder init —
|
|
13
|
+
* frappe-builder init — bootstrap agent toolchain (run once per machine)
|
|
13
14
|
* frappe-builder — start a pi session with frappe-builder extensions loaded
|
|
14
15
|
*/
|
|
15
16
|
const cmd = process.argv[2];
|
|
16
17
|
if (cmd === "init") {
|
|
17
|
-
const { runInit } = await import("./init-
|
|
18
|
+
const { runInit } = await import("./init-CkLSZ_3g.mjs");
|
|
18
19
|
await runInit();
|
|
19
20
|
process.exit(0);
|
|
20
21
|
}
|
|
@@ -23,7 +24,7 @@ if (cmd === "--help" || cmd === "-h") {
|
|
|
23
24
|
Usage: frappe-builder [command]
|
|
24
25
|
|
|
25
26
|
Commands:
|
|
26
|
-
init
|
|
27
|
+
init Bootstrap agent toolchain: context-mode, mcp2cli, context7 (run once per machine)
|
|
27
28
|
(none) Start a frappe-builder session
|
|
28
29
|
|
|
29
30
|
Options:
|
|
@@ -31,8 +32,8 @@ Options:
|
|
|
31
32
|
`);
|
|
32
33
|
process.exit(0);
|
|
33
34
|
}
|
|
34
|
-
if (!existsSync(
|
|
35
|
-
console.error("frappe-builder
|
|
35
|
+
if (!existsSync(join(homedir(), ".frappe-builder", ".initialized"))) {
|
|
36
|
+
console.error("frappe-builder toolchain not set up.");
|
|
36
37
|
console.error("Run: frappe-builder init");
|
|
37
38
|
process.exit(1);
|
|
38
39
|
}
|
|
@@ -3,42 +3,16 @@ import { join } from "node:path";
|
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
|
-
import { createInterface } from "node:readline";
|
|
7
6
|
//#region src/init.ts
|
|
8
7
|
/**
|
|
9
|
-
* src/init.ts —
|
|
8
|
+
* src/init.ts — toolchain setup for frappe-builder
|
|
10
9
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* and .gitignore patching.
|
|
10
|
+
* Bootstraps the agent environment: context-mode, mcp2cli, context7.
|
|
11
|
+
* No credential prompts — site credentials are collected at runtime via set_active_project.
|
|
14
12
|
*
|
|
15
13
|
* No imports from state/, extensions/, or gates/.
|
|
16
|
-
* Uses Node.js built-ins only
|
|
14
|
+
* Uses Node.js built-ins only.
|
|
17
15
|
*/
|
|
18
|
-
let cancelled = false;
|
|
19
|
-
process.on("SIGINT", () => {
|
|
20
|
-
cancelled = true;
|
|
21
|
-
});
|
|
22
|
-
function promptLine(question) {
|
|
23
|
-
return new Promise((resolve) => {
|
|
24
|
-
if (cancelled) {
|
|
25
|
-
resolve("");
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
const rl = createInterface({
|
|
29
|
-
input: process.stdin,
|
|
30
|
-
output: process.stdout
|
|
31
|
-
});
|
|
32
|
-
rl.question(question, (answer) => {
|
|
33
|
-
rl.close();
|
|
34
|
-
resolve(answer);
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
async function promptYN(question) {
|
|
39
|
-
const answer = await promptLine(question + " (y/N): ");
|
|
40
|
-
return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
|
|
41
|
-
}
|
|
42
16
|
function writeAtomic(filePath, content) {
|
|
43
17
|
const tmp = filePath + ".tmp";
|
|
44
18
|
writeFileSync(tmp, content, "utf-8");
|
|
@@ -59,107 +33,23 @@ function patchGitignore(projectRoot, entry) {
|
|
|
59
33
|
async function runInit(opts = {}) {
|
|
60
34
|
const projectRoot = opts.projectRoot ?? process.cwd();
|
|
61
35
|
const homeDir = homedir();
|
|
62
|
-
const
|
|
63
|
-
const globalConfigPath = join(globalConfigDir, "config.json");
|
|
64
|
-
const projectConfigPath = join(projectRoot, ".frappe-builder-config.json");
|
|
36
|
+
const markerPath = join(homeDir, ".frappe-builder", ".initialized");
|
|
65
37
|
console.log("\n=== frappe-builder Setup ===\n");
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (existsSync(globalConfigPath)) {
|
|
70
|
-
try {
|
|
71
|
-
globalConfig = JSON.parse(readFileSync(globalConfigPath, "utf-8"));
|
|
72
|
-
} catch {}
|
|
73
|
-
if (!cancelled) {
|
|
74
|
-
const overwrite = await promptYN(`Overwrite existing ${globalConfigPath}?`);
|
|
75
|
-
if (cancelled) {
|
|
76
|
-
printCancelled();
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
if (!overwrite) {
|
|
80
|
-
globalAction = "skipped";
|
|
81
|
-
console.log(" Keeping existing global config.\n");
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
if (!cancelled && globalAction === "written") {
|
|
86
|
-
const llmKey = await promptLine("LLM API key (leave blank to skip): ");
|
|
87
|
-
if (cancelled) {
|
|
88
|
-
printCancelled();
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
globalConfig.llm_api_key = llmKey.trim();
|
|
92
|
-
}
|
|
93
|
-
console.log(`\n[Project config: ${projectConfigPath}]`);
|
|
94
|
-
let projectConfig = {};
|
|
95
|
-
let projectAction = "written";
|
|
96
|
-
if (existsSync(projectConfigPath)) {
|
|
97
|
-
try {
|
|
98
|
-
projectConfig = JSON.parse(readFileSync(projectConfigPath, "utf-8"));
|
|
99
|
-
} catch {}
|
|
100
|
-
if (!cancelled) {
|
|
101
|
-
const overwrite = await promptYN(`Overwrite existing ${projectConfigPath}?`);
|
|
102
|
-
if (cancelled) {
|
|
103
|
-
printCancelled();
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
if (!overwrite) {
|
|
107
|
-
projectAction = "skipped";
|
|
108
|
-
console.log(" Keeping existing project config.\n");
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
if (!cancelled && projectAction === "written") {
|
|
113
|
-
const siteUrl = await promptLine("Frappe site URL (e.g. http://site1.localhost): ");
|
|
114
|
-
if (cancelled) {
|
|
115
|
-
printCancelled();
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
const apiKey = await promptLine("Frappe API key: ");
|
|
119
|
-
if (cancelled) {
|
|
120
|
-
printCancelled();
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
const apiSecret = await promptLine("Frappe API secret: ");
|
|
124
|
-
if (cancelled) {
|
|
125
|
-
printCancelled();
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
projectConfig = {
|
|
129
|
-
site_url: siteUrl.trim(),
|
|
130
|
-
api_key: apiKey.trim(),
|
|
131
|
-
api_secret: apiSecret.trim()
|
|
132
|
-
};
|
|
133
|
-
}
|
|
38
|
+
mkdirSync(join(homeDir, ".frappe-builder"), { recursive: true });
|
|
39
|
+
writeFileSync(markerPath, (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
|
|
40
|
+
const gitignoreResult = patchGitignore(projectRoot, ".fb/");
|
|
134
41
|
const written = [];
|
|
135
|
-
|
|
136
|
-
if (
|
|
137
|
-
mkdirSync(globalConfigDir, { recursive: true });
|
|
138
|
-
writeAtomic(globalConfigPath, JSON.stringify(globalConfig, null, 2) + "\n");
|
|
139
|
-
written.push(`~/.frappe-builder/config.json`);
|
|
140
|
-
} else skipped.push(`~/.frappe-builder/config.json`);
|
|
141
|
-
if (projectAction === "written") {
|
|
142
|
-
writeAtomic(projectConfigPath, JSON.stringify(projectConfig, null, 2) + "\n");
|
|
143
|
-
written.push(`.frappe-builder-config.json`);
|
|
144
|
-
} else skipped.push(`.frappe-builder-config.json`);
|
|
145
|
-
const gitignoreResult = patchGitignore(projectRoot, ".frappe-builder-config.json");
|
|
146
|
-
if (gitignoreResult === "patched") written.push(".gitignore (patched)");
|
|
147
|
-
else if (gitignoreResult === "created") written.push(".gitignore (created)");
|
|
148
|
-
else skipped.push(".gitignore (entry already present)");
|
|
42
|
+
if (gitignoreResult === "patched") written.push(".gitignore (patched with .fb/)");
|
|
43
|
+
else if (gitignoreResult === "created") written.push(".gitignore (created with .fb/)");
|
|
149
44
|
await setupContextMode(homeDir);
|
|
150
45
|
setupMcp2cli(homeDir);
|
|
151
46
|
setupContext7();
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
console.log("Skipped:");
|
|
156
|
-
for (const f of skipped) console.log(` - ${f}`);
|
|
47
|
+
if (written.length > 0) {
|
|
48
|
+
console.log("\nFiles written:");
|
|
49
|
+
for (const f of written) console.log(` ✓ ${f}`);
|
|
157
50
|
}
|
|
158
51
|
console.log("\nReady. Run: frappe-builder\n");
|
|
159
52
|
}
|
|
160
|
-
function printCancelled() {
|
|
161
|
-
console.log("\nSetup cancelled. No files were written.\n");
|
|
162
|
-
}
|
|
163
53
|
/**
|
|
164
54
|
* Installs and configures the context-mode pi MCP extension.
|
|
165
55
|
* Clones https://github.com/mksglu/context-mode into ~/.pi/extensions/context-mode,
|
|
@@ -3,7 +3,6 @@ import type { TextContent } from "@mariozechner/pi-ai";
|
|
|
3
3
|
import { db, getCurrentPhase } from "../state/db.js";
|
|
4
4
|
import { appendEntry } from "../state/journal.js";
|
|
5
5
|
import { buildStateContext, loadSpecialist, loadDebuggerSpecialist } from "./frappe-session.js";
|
|
6
|
-
import { loadCredentials } from "../config/loader.js";
|
|
7
6
|
|
|
8
7
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
8
|
export default function (pi: any) {
|
|
@@ -110,18 +109,6 @@ function applyPhaseTransition(featureId: string, newPhase: string): void {
|
|
|
110
109
|
})();
|
|
111
110
|
}
|
|
112
111
|
|
|
113
|
-
/**
|
|
114
|
-
* Session start hook — validates gitignore and loads credentials before any session setup.
|
|
115
|
-
* Throws with AC error message if .frappe-builder-config.json is not gitignored.
|
|
116
|
-
* Call this early in the session_start lifecycle; on throw, halt and surface the message.
|
|
117
|
-
* projectRoot defaults to process.cwd() when not supplied by the pi extension system.
|
|
118
|
-
*
|
|
119
|
-
* TODO: wire this into the pi extension system's session_start event once the
|
|
120
|
-
* session_start hook type is confirmed in @mariozechner/pi-agent-core.
|
|
121
|
-
*/
|
|
122
|
-
export function handleSessionStart(projectRoot: string = process.cwd()): void {
|
|
123
|
-
loadCredentials(projectRoot);
|
|
124
|
-
}
|
|
125
112
|
|
|
126
113
|
/**
|
|
127
114
|
* afterToolCall hook — appends a JSONL journal entry and updates the sessions
|
|
@@ -82,6 +82,9 @@ export default function (pi: any) {
|
|
|
82
82
|
parameters: Type.Object({
|
|
83
83
|
projectId: Type.String({ description: "Project identifier (e.g. 'my-frappe-site')" }),
|
|
84
84
|
sitePath: Type.String({ description: "Absolute path to the Frappe bench site (e.g. '/home/user/frappe-bench/sites/site1.local')" }),
|
|
85
|
+
siteUrl: Type.Optional(Type.String({ description: "Frappe site URL (e.g. 'http://site1.localhost'). Required for frappe_query." })),
|
|
86
|
+
apiKey: Type.Optional(Type.String({ description: "Frappe API key from User > API Access. Required for frappe_query." })),
|
|
87
|
+
apiSecret: Type.Optional(Type.String({ description: "Frappe API secret from User > API Access. Required for frappe_query." })),
|
|
85
88
|
}),
|
|
86
89
|
execute: async (_toolCallId: string, params: ToolParams) => {
|
|
87
90
|
const result = await setActiveProject(params);
|
package/package.json
CHANGED
package/state/db.ts
CHANGED
|
@@ -27,7 +27,16 @@ export function setDb(instance: DatabaseType): void {
|
|
|
27
27
|
*
|
|
28
28
|
* sitePath and appPath are optional so existing callers without them continue to work.
|
|
29
29
|
*/
|
|
30
|
-
export
|
|
30
|
+
export interface ProjectCredentials {
|
|
31
|
+
sitePath?: string;
|
|
32
|
+
appPath?: string;
|
|
33
|
+
siteUrl?: string;
|
|
34
|
+
apiKey?: string;
|
|
35
|
+
apiSecret?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function switchProject(newProjectId: string, creds: ProjectCredentials = {}): void {
|
|
39
|
+
const { sitePath, appPath, siteUrl, apiKey, apiSecret } = creds;
|
|
31
40
|
db.transaction(() => {
|
|
32
41
|
// 1. Read current active session before closing
|
|
33
42
|
const current = db
|
|
@@ -63,13 +72,16 @@ export function switchProject(newProjectId: string, sitePath?: string, appPath?:
|
|
|
63
72
|
|
|
64
73
|
// 5. Create new session, restoring prior phase if available; feature_id defaults to NULL
|
|
65
74
|
db.prepare(
|
|
66
|
-
"INSERT INTO sessions (session_id, project_id, current_phase, site_path, app_path, started_at, is_active) VALUES (?, ?, ?, ?, ?, ?, 1)"
|
|
75
|
+
"INSERT INTO sessions (session_id, project_id, current_phase, site_path, app_path, site_url, api_key, api_secret, started_at, is_active) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1)"
|
|
67
76
|
).run(
|
|
68
77
|
crypto.randomUUID(),
|
|
69
78
|
newProjectId,
|
|
70
79
|
prior?.current_phase ?? "idle",
|
|
71
80
|
sitePath ?? null,
|
|
72
81
|
appPath ?? null,
|
|
82
|
+
siteUrl ?? null,
|
|
83
|
+
apiKey ?? null,
|
|
84
|
+
apiSecret ?? null,
|
|
73
85
|
new Date().toISOString()
|
|
74
86
|
);
|
|
75
87
|
})();
|
package/state/schema.ts
CHANGED
|
@@ -36,6 +36,9 @@ export function initSchema(db: Database): void {
|
|
|
36
36
|
current_phase TEXT NOT NULL DEFAULT 'idle',
|
|
37
37
|
site_path TEXT,
|
|
38
38
|
app_path TEXT,
|
|
39
|
+
site_url TEXT,
|
|
40
|
+
api_key TEXT,
|
|
41
|
+
api_secret TEXT,
|
|
39
42
|
chain_step TEXT,
|
|
40
43
|
chain_pid INTEGER,
|
|
41
44
|
feature_id TEXT,
|
|
@@ -62,6 +65,9 @@ export function migrateSchema(db: Database): void {
|
|
|
62
65
|
"ALTER TABLE sessions ADD COLUMN app_path TEXT",
|
|
63
66
|
"ALTER TABLE sessions ADD COLUMN chain_step TEXT",
|
|
64
67
|
"ALTER TABLE sessions ADD COLUMN chain_pid INTEGER",
|
|
68
|
+
"ALTER TABLE sessions ADD COLUMN site_url TEXT",
|
|
69
|
+
"ALTER TABLE sessions ADD COLUMN api_key TEXT",
|
|
70
|
+
"ALTER TABLE sessions ADD COLUMN api_secret TEXT",
|
|
65
71
|
];
|
|
66
72
|
for (const sql of alters) {
|
|
67
73
|
try { db.exec(sql); } catch { /* column already exists — safe to ignore */ }
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { routeThroughContextMode } from "./context-sandbox.js";
|
|
1
|
+
import { db } from "../state/db.js";
|
|
2
|
+
import { applyTruncationFallback } from "./context-sandbox.js";
|
|
4
3
|
|
|
5
4
|
export interface FrappeQueryArgs {
|
|
6
5
|
doctype: string;
|
|
@@ -12,33 +11,50 @@ export interface FrappeQueryResult {
|
|
|
12
11
|
error?: string;
|
|
13
12
|
}
|
|
14
13
|
|
|
14
|
+
interface SessionCredentials {
|
|
15
|
+
site_url: string | null;
|
|
16
|
+
api_key: string | null;
|
|
17
|
+
api_secret: string | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
21
|
+
* Queries Frappe data via direct REST API call using credentials stored in the
|
|
22
|
+
* active session (set via set_active_project).
|
|
23
|
+
* Output is truncation-guarded — never returns raw payloads over 8K tokens.
|
|
18
24
|
* Returns structured error on failure, never throws.
|
|
19
|
-
*
|
|
20
|
-
* Note: mcp2cli command syntax uses --mcp <url> --raw <tool_name> <json_args>.
|
|
21
|
-
* Adapt args array if actual mcp2cli CLI syntax differs.
|
|
22
25
|
*/
|
|
23
26
|
export async function frappeQuery({ doctype, filters }: FrappeQueryArgs): Promise<FrappeQueryResult> {
|
|
24
|
-
const
|
|
27
|
+
const session = db
|
|
28
|
+
.prepare("SELECT site_url, api_key, api_secret FROM sessions WHERE is_active = 1 LIMIT 1")
|
|
29
|
+
.get() as SessionCredentials | undefined;
|
|
25
30
|
|
|
26
|
-
if (!
|
|
27
|
-
return { error: "
|
|
31
|
+
if (!session?.site_url) {
|
|
32
|
+
return { error: "No site_url configured. Call set_active_project with siteUrl, apiKey, and apiSecret first." };
|
|
33
|
+
}
|
|
34
|
+
if (!session.api_key || !session.api_secret) {
|
|
35
|
+
return { error: "No API credentials configured. Call set_active_project with apiKey and apiSecret." };
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
try {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
const params = new URLSearchParams();
|
|
40
|
+
if (filters && Object.keys(filters).length > 0) {
|
|
41
|
+
params.set("filters", JSON.stringify(filters));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const url = `${session.site_url}/api/resource/${encodeURIComponent(doctype)}?${params.toString()}`;
|
|
45
|
+
const response = await fetch(url, {
|
|
46
|
+
headers: {
|
|
47
|
+
Authorization: `token ${session.api_key}:${session.api_secret}`,
|
|
48
|
+
"Content-Type": "application/json",
|
|
49
|
+
},
|
|
50
|
+
});
|
|
38
51
|
|
|
39
|
-
if (
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
return { error: `Frappe API error ${response.status}: ${response.statusText}` };
|
|
54
|
+
}
|
|
40
55
|
|
|
41
|
-
const
|
|
56
|
+
const raw = await response.text();
|
|
57
|
+
const summary = applyTruncationFallback(raw);
|
|
42
58
|
return { summary };
|
|
43
59
|
} catch (err) {
|
|
44
60
|
const msg = err instanceof Error ? err.message : String(err);
|
package/tools/project-tools.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { db, switchProject } from "../state/db.js";
|
|
1
|
+
import { db, switchProject, type ProjectCredentials } from "../state/db.js";
|
|
2
2
|
import { reloadSessionContext } from "../extensions/frappe-session.js";
|
|
3
3
|
|
|
4
4
|
export interface ComponentStatus {
|
|
@@ -78,31 +78,32 @@ interface SetActiveProjectArgs {
|
|
|
78
78
|
projectId: string;
|
|
79
79
|
sitePath: string;
|
|
80
80
|
appPath?: string;
|
|
81
|
+
siteUrl?: string;
|
|
82
|
+
apiKey?: string;
|
|
83
|
+
apiSecret?: string;
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
/**
|
|
84
87
|
* Switches the active Frappe project and site, flushes current session state,
|
|
85
88
|
* creates a new session, and reloads the system prompt context.
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* no second appendEntry() call here.
|
|
89
|
+
* Site credentials (siteUrl, apiKey, apiSecret) are stored in the session row
|
|
90
|
+
* and used by frappe_query for direct REST API calls.
|
|
89
91
|
*/
|
|
90
|
-
export async function setActiveProject({ projectId, sitePath, appPath }: SetActiveProjectArgs) {
|
|
91
|
-
|
|
92
|
-
switchProject(projectId,
|
|
92
|
+
export async function setActiveProject({ projectId, sitePath, appPath, siteUrl, apiKey, apiSecret }: SetActiveProjectArgs) {
|
|
93
|
+
const creds: ProjectCredentials = { sitePath, appPath, siteUrl, apiKey, apiSecret };
|
|
94
|
+
switchProject(projectId, creds);
|
|
93
95
|
|
|
94
|
-
// Reload system prompt with new project context
|
|
95
96
|
await reloadSessionContext();
|
|
96
97
|
|
|
97
|
-
// Read restored phase for return value
|
|
98
98
|
const session = db
|
|
99
|
-
.prepare("SELECT current_phase FROM sessions WHERE is_active = 1 LIMIT 1")
|
|
100
|
-
.get() as { current_phase: string } | undefined;
|
|
99
|
+
.prepare("SELECT current_phase, site_url FROM sessions WHERE is_active = 1 LIMIT 1")
|
|
100
|
+
.get() as { current_phase: string; site_url: string | null } | undefined;
|
|
101
101
|
|
|
102
102
|
return {
|
|
103
103
|
project_id: projectId,
|
|
104
104
|
site_path: sitePath,
|
|
105
105
|
app_path: appPath ?? null,
|
|
106
|
+
site_url: session?.site_url ?? null,
|
|
106
107
|
phase: session?.current_phase ?? "idle",
|
|
107
108
|
context_reloaded: true,
|
|
108
109
|
};
|