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 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:23:24.878Z
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:23:24.878Z
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==
@@ -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
- // ── Credential config types (NFR6, NFR9) ────────────────────────────────────
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 — interactive setup wizard (run once per project)
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-Gp1MgJD2.mjs");
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 Configure frappe-builder for this project (run once per project)
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(resolve(process.cwd(), ".frappe-builder-config.json"))) {
35
- console.error("frappe-builder is not configured in this directory.");
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 — interactive setup wizard for frappe-builder
8
+ * src/init.ts — toolchain setup for frappe-builder
10
9
  *
11
- * Handles: global config (~/.frappe-builder/config.json),
12
- * project config (.frappe-builder-config.json),
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 — no external prompt libraries.
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 globalConfigDir = join(homeDir, ".frappe-builder");
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
- console.log(`[Global config: ${globalConfigPath}]`);
67
- let globalConfig = {};
68
- let globalAction = "written";
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
- const skipped = [];
136
- if (globalAction === "written") {
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
- console.log("\nFiles written:");
153
- for (const f of written) console.log(` ✓ ${f}`);
154
- if (skipped.length > 0) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frappe-builder",
3
- "version": "1.1.0-dev.24",
3
+ "version": "1.1.0-dev.25",
4
4
  "description": "Frappe-native AI co-pilot for building and customising Frappe/ERPNext applications",
5
5
  "type": "module",
6
6
  "bin": {
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 function switchProject(newProjectId: string, sitePath?: string, appPath?: string): void {
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 { execa } from "execa";
2
- import { loadConfig } from "../config/loader.js";
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
- * Dispatches a Frappe get_list query via mcp2cli subprocess.
17
- * Raw output is routed through context-mode sandbox — never returned directly.
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 config = loadConfig();
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 (!config.frappeMcpUrl) {
27
- return { error: "frappeMcpUrl not configured in ~/.frappe-builder/config.json" };
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 payload = JSON.stringify({ doctype, filters: filters ?? {} });
32
- const { stdout, stderr } = await execa("mcp2cli", [
33
- "--mcp", config.frappeMcpUrl,
34
- "--raw",
35
- "frappe_get_list",
36
- payload,
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 (stderr) console.warn(`[frappe_query mcp2cli stderr: ${stderr}]`);
52
+ if (!response.ok) {
53
+ return { error: `Frappe API error ${response.status}: ${response.statusText}` };
54
+ }
40
55
 
41
- const summary = await routeThroughContextMode(stdout);
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);
@@ -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
- * The state_transition JSONL entry is written inside switchProject()
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
- // Flush current state + create new session (JSONL entry written internally)
92
- switchProject(projectId, sitePath, appPath);
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
  };