frappe-builder 1.1.0-dev.8 → 1.1.0-dev.9
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/dist/cli.mjs +64 -0
- package/dist/coverage-check-DLGO_qwW.mjs +55 -0
- package/dist/db-Cx_EyUEu.mjs +58 -0
- package/dist/frappe-gates-c4HHJp-4.mjs +349 -0
- package/dist/frappe-session-BfFveYq1.mjs +5 -0
- package/dist/frappe-session-BzM5oUCb.mjs +5 -0
- package/dist/frappe-state-k--gX3wq.mjs +6 -0
- package/dist/frappe-tools-Dwz0eEQ-.mjs +13 -0
- package/dist/frappe-ui-htmQgO8t.mjs +3 -0
- package/dist/frappe-workflow-VId2tr9e.mjs +4 -0
- package/dist/fsm-DkLob1CA.mjs +3 -0
- package/dist/init-ChmHonBN.mjs +159 -0
- package/dist/loader-DC2PlJU7.mjs +68 -0
- package/package.json +4 -1
- package/tsdown.config.ts +1 -0
package/.fb/state.db
CHANGED
|
Binary file
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { t as loadCredentials } from "./loader-DC2PlJU7.mjs";
|
|
3
|
+
import { n as setSessionsDir, t as db } from "./db-Cx_EyUEu.mjs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { mkdirSync } from "node:fs";
|
|
7
|
+
//#region src/cli.ts
|
|
8
|
+
/**
|
|
9
|
+
* src/cli.ts — standalone binary entry point for frappe-builder
|
|
10
|
+
*
|
|
11
|
+
* Built with: bun build --compile --minify src/cli.ts --outfile frappe-builder
|
|
12
|
+
* ESM top-level await is supported by bun compile.
|
|
13
|
+
*
|
|
14
|
+
* Extension load order is critical — do NOT reorder imports.
|
|
15
|
+
* frappe-session must reconstruct state before FSM guard or tools activate.
|
|
16
|
+
*/
|
|
17
|
+
const cmd = process.argv[2];
|
|
18
|
+
if (cmd === "init") {
|
|
19
|
+
const { runInit } = await import("./init-ChmHonBN.mjs");
|
|
20
|
+
await runInit();
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
if (cmd === "--help" || cmd === "-h") {
|
|
24
|
+
console.log(`
|
|
25
|
+
Usage: frappe-builder [command]
|
|
26
|
+
|
|
27
|
+
Commands:
|
|
28
|
+
init Configure frappe-builder for this project (run once per project)
|
|
29
|
+
(none) Start a frappe-builder session
|
|
30
|
+
|
|
31
|
+
Options:
|
|
32
|
+
--help, -h Show this help message
|
|
33
|
+
`);
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
const SESSION_LOG_DIR = join(homedir(), ".frappe-builder", "sessions");
|
|
37
|
+
mkdirSync(SESSION_LOG_DIR, { recursive: true });
|
|
38
|
+
setSessionsDir(SESSION_LOG_DIR);
|
|
39
|
+
process.on("SIGINT", () => {
|
|
40
|
+
try {
|
|
41
|
+
db.close();
|
|
42
|
+
} catch {}
|
|
43
|
+
process.exit(0);
|
|
44
|
+
});
|
|
45
|
+
process.on("SIGTERM", () => {
|
|
46
|
+
try {
|
|
47
|
+
db.close();
|
|
48
|
+
} catch {}
|
|
49
|
+
process.exit(0);
|
|
50
|
+
});
|
|
51
|
+
try {
|
|
52
|
+
loadCredentials(process.cwd());
|
|
53
|
+
} catch (err) {
|
|
54
|
+
console.error(err.message);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
await import("./frappe-session-BfFveYq1.mjs");
|
|
58
|
+
await import("./frappe-state-k--gX3wq.mjs");
|
|
59
|
+
await import("./frappe-workflow-VId2tr9e.mjs");
|
|
60
|
+
await import("./frappe-tools-Dwz0eEQ-.mjs");
|
|
61
|
+
await import("./frappe-gates-c4HHJp-4.mjs");
|
|
62
|
+
await import("./frappe-ui-htmQgO8t.mjs");
|
|
63
|
+
//#endregion
|
|
64
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execa } from "execa";
|
|
3
|
+
//#region \0rolldown/runtime.js
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __exportAll = (all, no_symbols) => {
|
|
6
|
+
let target = {};
|
|
7
|
+
for (var name in all) __defProp(target, name, {
|
|
8
|
+
get: all[name],
|
|
9
|
+
enumerable: true
|
|
10
|
+
});
|
|
11
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
12
|
+
return target;
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region gates/coverage-check.ts
|
|
16
|
+
/**
|
|
17
|
+
* gates/coverage-check.ts — Async coverage gate (NOT a pure function).
|
|
18
|
+
* Runs `bench run-tests --coverage` via execa and parses the result.
|
|
19
|
+
*
|
|
20
|
+
* NOT wired via frappe-gates.ts — integrated directly in completeComponent().
|
|
21
|
+
* Triggers when the final component is completed in `testing` phase. (FR29)
|
|
22
|
+
*/
|
|
23
|
+
var coverage_check_exports = /* @__PURE__ */ __exportAll({ coverageCheck: () => coverageCheck });
|
|
24
|
+
function parseCoverage(stdout) {
|
|
25
|
+
const match = stdout.match(/^TOTAL\s+\d+\s+\d+\s+(\d+)%/m);
|
|
26
|
+
return match ? parseInt(match[1], 10) : 0;
|
|
27
|
+
}
|
|
28
|
+
async function coverageCheck(_context) {
|
|
29
|
+
try {
|
|
30
|
+
const { stdout } = await execa("bench", ["run-tests", "--coverage"], {
|
|
31
|
+
cwd: process.env.FRAPPE_BENCH_PATH ?? process.cwd(),
|
|
32
|
+
reject: false,
|
|
33
|
+
timeout: 12e4
|
|
34
|
+
});
|
|
35
|
+
const coverage = parseCoverage(stdout ?? "");
|
|
36
|
+
if (coverage < 70) return {
|
|
37
|
+
passed: false,
|
|
38
|
+
coverage,
|
|
39
|
+
required: "70%"
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
passed: true,
|
|
43
|
+
coverage
|
|
44
|
+
};
|
|
45
|
+
} catch (err) {
|
|
46
|
+
return {
|
|
47
|
+
passed: false,
|
|
48
|
+
coverage: 0,
|
|
49
|
+
required: "70%",
|
|
50
|
+
error: err.message
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
55
|
+
export { __exportAll as n, coverage_check_exports as t };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "node:os";
|
|
3
|
+
import "node:path";
|
|
4
|
+
import { mkdirSync } from "node:fs";
|
|
5
|
+
import Database from "better-sqlite3";
|
|
6
|
+
//#region state/schema.ts
|
|
7
|
+
/**
|
|
8
|
+
* Runs all CREATE TABLE IF NOT EXISTS statements against the provided database.
|
|
9
|
+
* Safe to call multiple times — idempotent by design.
|
|
10
|
+
*/
|
|
11
|
+
function initSchema(db) {
|
|
12
|
+
db.exec(`
|
|
13
|
+
CREATE TABLE IF NOT EXISTS features (
|
|
14
|
+
feature_id TEXT PRIMARY KEY,
|
|
15
|
+
name TEXT NOT NULL,
|
|
16
|
+
mode TEXT NOT NULL DEFAULT 'full' CHECK (mode IN ('full', 'quick')),
|
|
17
|
+
current_phase TEXT NOT NULL DEFAULT 'idle',
|
|
18
|
+
created_at TEXT NOT NULL,
|
|
19
|
+
updated_at TEXT,
|
|
20
|
+
progress_done INTEGER DEFAULT 0,
|
|
21
|
+
progress_total INTEGER DEFAULT 0
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
CREATE TABLE IF NOT EXISTS components (
|
|
25
|
+
component_id TEXT NOT NULL,
|
|
26
|
+
feature_id TEXT NOT NULL,
|
|
27
|
+
status TEXT NOT NULL DEFAULT 'in-progress',
|
|
28
|
+
completed_at TEXT,
|
|
29
|
+
PRIMARY KEY (feature_id, component_id),
|
|
30
|
+
FOREIGN KEY (feature_id) REFERENCES features(feature_id)
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
34
|
+
session_id TEXT PRIMARY KEY,
|
|
35
|
+
project_id TEXT NOT NULL,
|
|
36
|
+
current_phase TEXT NOT NULL DEFAULT 'idle',
|
|
37
|
+
site_path TEXT,
|
|
38
|
+
feature_id TEXT,
|
|
39
|
+
last_tool TEXT,
|
|
40
|
+
started_at TEXT NOT NULL,
|
|
41
|
+
ended_at TEXT,
|
|
42
|
+
is_active INTEGER NOT NULL DEFAULT 1
|
|
43
|
+
);
|
|
44
|
+
`);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Overrides the sessions directory — intended for test use only.
|
|
48
|
+
* Pass null to reset to the default ~/.frappe-builder/sessions path.
|
|
49
|
+
*/
|
|
50
|
+
function setSessionsDir(dir) {}
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region state/db.ts
|
|
53
|
+
mkdirSync(".fb", { recursive: true });
|
|
54
|
+
/** Singleton SQLite connection — the only Database instance in the codebase. */
|
|
55
|
+
let db = new Database(".fb/state.db");
|
|
56
|
+
initSchema(db);
|
|
57
|
+
//#endregion
|
|
58
|
+
export { setSessionsDir as n, db as t };
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { n as __exportAll, t as coverage_check_exports } from "./coverage-check-DLGO_qwW.mjs";
|
|
3
|
+
import "./db-Cx_EyUEu.mjs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
//#region gates/frappe-native-check.ts
|
|
6
|
+
const NON_NATIVE_PATTERNS = [
|
|
7
|
+
{
|
|
8
|
+
pattern: /import\s+axios/,
|
|
9
|
+
label: "axios (use frappe.call)"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
pattern: /require\(['"]axios['"]\)/,
|
|
13
|
+
label: "axios (use frappe.call)"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
pattern: /import\s+requests/,
|
|
17
|
+
label: "requests (use frappe.call)"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
pattern: /from\s+requests\s+import/,
|
|
21
|
+
label: "requests (use frappe.call)"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
pattern: /cursor\.execute\s*\(/,
|
|
25
|
+
label: "raw SQL cursor (use frappe.db.sql or ORM)"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
pattern: /pymysql|psycopg2|sqlite3\.connect/,
|
|
29
|
+
label: "direct DB driver (use Frappe ORM)"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
pattern: /mongoose|sequelize|typeorm/i,
|
|
33
|
+
label: "external ORM (use Frappe DocType)"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
pattern: /jwt\.sign|jsonwebtoken/,
|
|
37
|
+
label: "JWT auth (use frappe.whitelist + session)"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
pattern: /passport\.authenticate/,
|
|
41
|
+
label: "passport auth (use Frappe auth)"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
pattern: /import\s+pandas/,
|
|
45
|
+
label: "pandas (use Frappe Script Report)"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
pattern: /import\s+numpy/,
|
|
49
|
+
label: "numpy (use Frappe computation)"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
pattern: /fs\.writeFileSync|fs\.readFileSync/,
|
|
53
|
+
label: "raw fs (use frappe.get_file_url or File DocType)"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
pattern: /express\(\)|fastapi|flask\.Flask/,
|
|
57
|
+
label: "external web framework (use Frappe whitelist)"
|
|
58
|
+
}
|
|
59
|
+
];
|
|
60
|
+
/**
|
|
61
|
+
* Scans `content` for non-Frappe-native patterns.
|
|
62
|
+
*
|
|
63
|
+
* - No detected patterns → { passed: true }
|
|
64
|
+
* - Detected + no justification → { passed: false, requiresJustification: true, ... }
|
|
65
|
+
* - Detected + justification provided → { passed: true, result: "override", ... }
|
|
66
|
+
*/
|
|
67
|
+
function checkFrappeNative(content, justification) {
|
|
68
|
+
const detectedPatterns = NON_NATIVE_PATTERNS.filter(({ pattern }) => pattern.test(content)).map(({ label }) => label);
|
|
69
|
+
if (detectedPatterns.length === 0) return { passed: true };
|
|
70
|
+
if (justification) return {
|
|
71
|
+
passed: true,
|
|
72
|
+
result: "override",
|
|
73
|
+
justification,
|
|
74
|
+
detectedPatterns
|
|
75
|
+
};
|
|
76
|
+
return {
|
|
77
|
+
passed: false,
|
|
78
|
+
requiresJustification: true,
|
|
79
|
+
message: "This uses a non-Frappe-native approach. Justification required to proceed.",
|
|
80
|
+
detectedPatterns
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
//#endregion
|
|
84
|
+
//#region gates/permission-check.ts
|
|
85
|
+
var permission_check_exports = /* @__PURE__ */ __exportAll({ permissionCheck: () => permissionCheck });
|
|
86
|
+
const PERMISSION_PATTERNS = [
|
|
87
|
+
/frappe\.has_permission\s*\(/,
|
|
88
|
+
/frappe\.only_for\s*\(/,
|
|
89
|
+
/frappe\.check_permission\s*\(/,
|
|
90
|
+
/frappe\.has_role\s*\(/,
|
|
91
|
+
/frappe\.session\.user/
|
|
92
|
+
];
|
|
93
|
+
function getLineNumber(code, offset) {
|
|
94
|
+
return code.slice(0, offset).split("\n").length;
|
|
95
|
+
}
|
|
96
|
+
function extractFunctionBody(code, defStart) {
|
|
97
|
+
const bodyStart = code.indexOf("\n", defStart) + 1;
|
|
98
|
+
const nextTopLevel = /\n(?=def |class |@)/g;
|
|
99
|
+
nextTopLevel.lastIndex = bodyStart;
|
|
100
|
+
const match = nextTopLevel.exec(code);
|
|
101
|
+
return match ? code.slice(bodyStart, match.index) : code.slice(bodyStart);
|
|
102
|
+
}
|
|
103
|
+
function hasPermissionCheck(body) {
|
|
104
|
+
return PERMISSION_PATTERNS.some((p) => p.test(body));
|
|
105
|
+
}
|
|
106
|
+
function permissionCheck(code, context) {
|
|
107
|
+
if (!context.file.endsWith(".py")) return { passed: true };
|
|
108
|
+
const WHITELIST_DECORATOR = /@frappe\.whitelist\([^)]*\)\s*\ndef\s+(\w+)/g;
|
|
109
|
+
const violations = [];
|
|
110
|
+
let match;
|
|
111
|
+
while ((match = WHITELIST_DECORATOR.exec(code)) !== null) {
|
|
112
|
+
const decoratorOffset = match.index;
|
|
113
|
+
const methodName = match[1];
|
|
114
|
+
const line = getLineNumber(code, decoratorOffset);
|
|
115
|
+
if (!hasPermissionCheck(extractFunctionBody(code, code.indexOf("\ndef ", decoratorOffset) + 1))) violations.push({
|
|
116
|
+
method: methodName,
|
|
117
|
+
line,
|
|
118
|
+
reason: "missing permission check"
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
if (violations.length > 0) return {
|
|
122
|
+
passed: false,
|
|
123
|
+
violations
|
|
124
|
+
};
|
|
125
|
+
return { passed: true };
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region gates/query-check.ts
|
|
129
|
+
var query_check_exports = /* @__PURE__ */ __exportAll({ queryCheck: () => queryCheck });
|
|
130
|
+
const SQL_KEYWORD = /\b(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE)\b/i;
|
|
131
|
+
const INJECTION_PATTERNS = [
|
|
132
|
+
{
|
|
133
|
+
pattern: /f["'].*\{[^}]+\}.*["']/,
|
|
134
|
+
label: "f-string interpolation"
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
pattern: /["']\s*\+\s*\w/,
|
|
138
|
+
label: "string concatenation (+)"
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
pattern: /["']\s*%\s*\w/,
|
|
142
|
+
label: "%-format interpolation"
|
|
143
|
+
}
|
|
144
|
+
];
|
|
145
|
+
const REASON$1 = "SQL string concatenation detected — use parameterised queries";
|
|
146
|
+
function queryCheck(code, context) {
|
|
147
|
+
if (!context.file.endsWith(".py")) return { passed: true };
|
|
148
|
+
const lines = code.split("\n");
|
|
149
|
+
const violations = [];
|
|
150
|
+
for (let i = 0; i < lines.length; i++) {
|
|
151
|
+
const line = lines[i];
|
|
152
|
+
if (!SQL_KEYWORD.test(line)) continue;
|
|
153
|
+
for (const { pattern, label } of INJECTION_PATTERNS) if (pattern.test(line)) {
|
|
154
|
+
violations.push({
|
|
155
|
+
line: i + 1,
|
|
156
|
+
pattern: label,
|
|
157
|
+
reason: REASON$1
|
|
158
|
+
});
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return violations.length > 0 ? {
|
|
163
|
+
passed: false,
|
|
164
|
+
violations
|
|
165
|
+
} : { passed: true };
|
|
166
|
+
}
|
|
167
|
+
//#endregion
|
|
168
|
+
//#region gates/server-side-check.ts
|
|
169
|
+
var server_side_check_exports = /* @__PURE__ */ __exportAll({ serverSideCheck: () => serverSideCheck });
|
|
170
|
+
const BUSINESS_LOGIC_PATTERNS = [
|
|
171
|
+
{
|
|
172
|
+
pattern: /frm\.doc\.\w+\s*=\s*frm\.doc\.\w+\s*[*\/+\-]\s*frm\.doc\.\w+/,
|
|
173
|
+
label: "client-side financial calculation"
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
pattern: /frappe\.session\.user\s*[=!]={1,2}\s*["']/,
|
|
177
|
+
label: "client-side permission decision (user identity)"
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
pattern: /frappe\.user\.has_role\s*\(/,
|
|
181
|
+
label: "client-side permission decision (role check)"
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
pattern: /frappe\.user_roles\b/,
|
|
185
|
+
label: "client-side permission decision (user_roles)"
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
pattern: /frappe\.db\.(get_value|get_list|set_value|insert|delete_doc)\s*\(/,
|
|
189
|
+
label: "direct DB access from client-side (use frappe.call instead)"
|
|
190
|
+
}
|
|
191
|
+
];
|
|
192
|
+
const REASON = "business logic must be server-side";
|
|
193
|
+
function isTestFile(file) {
|
|
194
|
+
return file.includes("/test/") || file.includes(".test.") || file.includes(".spec.");
|
|
195
|
+
}
|
|
196
|
+
function serverSideCheck(code, context) {
|
|
197
|
+
if (!context.file.endsWith(".js") && !context.file.endsWith(".ts")) return { passed: true };
|
|
198
|
+
if (isTestFile(context.file)) return { passed: true };
|
|
199
|
+
const lines = code.split("\n");
|
|
200
|
+
const violations = [];
|
|
201
|
+
for (let i = 0; i < lines.length; i++) {
|
|
202
|
+
const line = lines[i];
|
|
203
|
+
for (const { pattern, label } of BUSINESS_LOGIC_PATTERNS) if (pattern.test(line)) {
|
|
204
|
+
violations.push({
|
|
205
|
+
line: i + 1,
|
|
206
|
+
pattern: label,
|
|
207
|
+
reason: REASON
|
|
208
|
+
});
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return violations.length > 0 ? {
|
|
213
|
+
passed: false,
|
|
214
|
+
violations
|
|
215
|
+
} : { passed: true };
|
|
216
|
+
}
|
|
217
|
+
//#endregion
|
|
218
|
+
//#region gates/style-check.ts
|
|
219
|
+
var style_check_exports = /* @__PURE__ */ __exportAll({ styleCheck: () => styleCheck });
|
|
220
|
+
const DEF_LINE = /^([^\S\n]*)def\s+(\w+)\s*\([^)]*\)\s*:/gm;
|
|
221
|
+
const NON_DESCRIPTIVE = /^(\s*)(x|y|z|data|temp|tmp|val|var|foo|bar|test|obj|res|ret)\s*=/gm;
|
|
222
|
+
const SQL_STRING = /frappe\.db\.sql\s*\(\s*["'`]{1,3}([\s\S]*?)["'`]{1,3}/g;
|
|
223
|
+
const CAMEL_IDENTIFIER = /\b([a-z][a-zA-Z0-9]*[A-Z][a-zA-Z0-9]*)\b/g;
|
|
224
|
+
const STUB_BODY = /^\s*(pass|return|return None|super\(\)\.__init__\(.*?\))\s*$/;
|
|
225
|
+
function lineNumber(code, index) {
|
|
226
|
+
return code.slice(0, index).split("\n").length;
|
|
227
|
+
}
|
|
228
|
+
function hasDocstring(code, defStart) {
|
|
229
|
+
const bodyStart = code.indexOf("\n", defStart) + 1;
|
|
230
|
+
const bodyTrimmed = code.slice(bodyStart).trimStart();
|
|
231
|
+
return bodyTrimmed.startsWith("\"\"\"") || bodyTrimmed.startsWith("'''");
|
|
232
|
+
}
|
|
233
|
+
function isStubBody(code, defStart) {
|
|
234
|
+
const newline = code.indexOf("\n", defStart);
|
|
235
|
+
if (newline === -1) return true;
|
|
236
|
+
const nextNewline = code.indexOf("\n", newline + 1);
|
|
237
|
+
const bodyLine = nextNewline === -1 ? code.slice(newline + 1) : code.slice(newline + 1, nextNewline);
|
|
238
|
+
return STUB_BODY.test(bodyLine);
|
|
239
|
+
}
|
|
240
|
+
function checkPython(code, violations) {
|
|
241
|
+
let m;
|
|
242
|
+
DEF_LINE.lastIndex = 0;
|
|
243
|
+
while ((m = DEF_LINE.exec(code)) !== null) {
|
|
244
|
+
const fnName = m[2];
|
|
245
|
+
const defStart = m.index;
|
|
246
|
+
if (!hasDocstring(code, defStart) && !isStubBody(code, defStart)) violations.push({
|
|
247
|
+
function: fnName,
|
|
248
|
+
line: lineNumber(code, defStart),
|
|
249
|
+
reason: "missing docstring"
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
NON_DESCRIPTIVE.lastIndex = 0;
|
|
253
|
+
while ((m = NON_DESCRIPTIVE.exec(code)) !== null) {
|
|
254
|
+
const varName = m[2];
|
|
255
|
+
violations.push({
|
|
256
|
+
line: lineNumber(code, m.index),
|
|
257
|
+
variable: varName,
|
|
258
|
+
reason: "non-descriptive variable name"
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
SQL_STRING.lastIndex = 0;
|
|
262
|
+
while ((m = SQL_STRING.exec(code)) !== null) {
|
|
263
|
+
const sqlContent = m[1];
|
|
264
|
+
const sqlStart = m.index;
|
|
265
|
+
CAMEL_IDENTIFIER.lastIndex = 0;
|
|
266
|
+
let cm;
|
|
267
|
+
while ((cm = CAMEL_IDENTIFIER.exec(sqlContent)) !== null) violations.push({
|
|
268
|
+
line: lineNumber(code, sqlStart),
|
|
269
|
+
column: cm[1],
|
|
270
|
+
reason: "SQL column must be snake_case"
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function checkFilename(context, violations) {
|
|
275
|
+
const basename = path.basename(context.file).replace(/\.(test|spec)\.(ts|js)$/, "").replace(/\.(ts|js)$/, "");
|
|
276
|
+
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(basename)) violations.push({
|
|
277
|
+
file: context.file,
|
|
278
|
+
reason: "TypeScript filename must be kebab-case"
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
function styleCheck(code, context) {
|
|
282
|
+
const violations = [];
|
|
283
|
+
const ext = path.extname(context.file);
|
|
284
|
+
if (ext === ".py") checkPython(code, violations);
|
|
285
|
+
else if (ext === ".ts" || ext === ".js") checkFilename(context, violations);
|
|
286
|
+
if (violations.length > 0) return {
|
|
287
|
+
passed: false,
|
|
288
|
+
violations
|
|
289
|
+
};
|
|
290
|
+
return { passed: true };
|
|
291
|
+
}
|
|
292
|
+
//#endregion
|
|
293
|
+
//#region extensions/frappe-gates.ts
|
|
294
|
+
function nativeAdapter(code, _context) {
|
|
295
|
+
const r = checkFrappeNative(code);
|
|
296
|
+
if (r.passed) return { passed: true };
|
|
297
|
+
return {
|
|
298
|
+
passed: false,
|
|
299
|
+
violations: [{
|
|
300
|
+
reason: r.message,
|
|
301
|
+
detectedPatterns: r.detectedPatterns
|
|
302
|
+
}]
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function tryLoadGate(mod, exportName, gateName) {
|
|
306
|
+
const fn = mod[exportName];
|
|
307
|
+
if (typeof fn === "function") return {
|
|
308
|
+
name: gateName,
|
|
309
|
+
fn
|
|
310
|
+
};
|
|
311
|
+
console.warn(`[GATE WARNING: ${gateName} gate not yet implemented — skipping]`);
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
const GATE_REGISTRY = [{
|
|
315
|
+
name: "frappe_native",
|
|
316
|
+
fn: nativeAdapter
|
|
317
|
+
}];
|
|
318
|
+
for (const [mod, exportName, gateName] of [
|
|
319
|
+
[
|
|
320
|
+
permission_check_exports,
|
|
321
|
+
"permissionCheck",
|
|
322
|
+
"permission_check"
|
|
323
|
+
],
|
|
324
|
+
[
|
|
325
|
+
query_check_exports,
|
|
326
|
+
"queryCheck",
|
|
327
|
+
"query_check"
|
|
328
|
+
],
|
|
329
|
+
[
|
|
330
|
+
server_side_check_exports,
|
|
331
|
+
"serverSideCheck",
|
|
332
|
+
"server_side_check"
|
|
333
|
+
],
|
|
334
|
+
[
|
|
335
|
+
coverage_check_exports,
|
|
336
|
+
"coverageCheck",
|
|
337
|
+
"coverage_check"
|
|
338
|
+
],
|
|
339
|
+
[
|
|
340
|
+
style_check_exports,
|
|
341
|
+
"styleCheck",
|
|
342
|
+
"style_check"
|
|
343
|
+
]
|
|
344
|
+
]) {
|
|
345
|
+
const entry = tryLoadGate(mod, exportName, gateName);
|
|
346
|
+
if (entry) GATE_REGISTRY.push(entry);
|
|
347
|
+
}
|
|
348
|
+
//#endregion
|
|
349
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "./coverage-check-DLGO_qwW.mjs";
|
|
3
|
+
import "./loader-DC2PlJU7.mjs";
|
|
4
|
+
import "./db-Cx_EyUEu.mjs";
|
|
5
|
+
import "./fsm-DkLob1CA.mjs";
|
|
6
|
+
import "./frappe-session-BzM5oUCb.mjs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import "node:fs";
|
|
10
|
+
import "execa";
|
|
11
|
+
join(join(homedir(), ".frappe-builder"), "allowed-commands.json");
|
|
12
|
+
//#endregion
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { createInterface } from "node:readline";
|
|
6
|
+
//#region src/init.ts
|
|
7
|
+
/**
|
|
8
|
+
* src/init.ts — interactive setup wizard for frappe-builder
|
|
9
|
+
*
|
|
10
|
+
* Handles: global config (~/.frappe-builder/config.json),
|
|
11
|
+
* project config (.frappe-builder-config.json),
|
|
12
|
+
* and .gitignore patching.
|
|
13
|
+
*
|
|
14
|
+
* No imports from state/, extensions/, or gates/.
|
|
15
|
+
* Uses Node.js built-ins only — no external prompt libraries.
|
|
16
|
+
*/
|
|
17
|
+
let cancelled = false;
|
|
18
|
+
process.on("SIGINT", () => {
|
|
19
|
+
cancelled = true;
|
|
20
|
+
});
|
|
21
|
+
function promptLine(question) {
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
if (cancelled) {
|
|
24
|
+
resolve("");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const rl = createInterface({
|
|
28
|
+
input: process.stdin,
|
|
29
|
+
output: process.stdout
|
|
30
|
+
});
|
|
31
|
+
rl.question(question, (answer) => {
|
|
32
|
+
rl.close();
|
|
33
|
+
resolve(answer);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
async function promptYN(question) {
|
|
38
|
+
const answer = await promptLine(question + " (y/N): ");
|
|
39
|
+
return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
|
|
40
|
+
}
|
|
41
|
+
function writeAtomic(filePath, content) {
|
|
42
|
+
const tmp = filePath + ".tmp";
|
|
43
|
+
writeFileSync(tmp, content, "utf-8");
|
|
44
|
+
renameSync(tmp, filePath);
|
|
45
|
+
}
|
|
46
|
+
/** Patches .gitignore to include the exact entry if not already present. */
|
|
47
|
+
function patchGitignore(projectRoot, entry) {
|
|
48
|
+
const gitignorePath = join(projectRoot, ".gitignore");
|
|
49
|
+
if (!existsSync(gitignorePath)) {
|
|
50
|
+
writeFileSync(gitignorePath, entry + "\n", "utf-8");
|
|
51
|
+
return "created";
|
|
52
|
+
}
|
|
53
|
+
const content = readFileSync(gitignorePath, "utf-8");
|
|
54
|
+
if (content.split("\n").includes(entry)) return "already-present";
|
|
55
|
+
writeFileSync(gitignorePath, content.endsWith("\n") ? content + entry + "\n" : content + "\n" + entry + "\n", "utf-8");
|
|
56
|
+
return "patched";
|
|
57
|
+
}
|
|
58
|
+
async function runInit(opts = {}) {
|
|
59
|
+
const projectRoot = opts.projectRoot ?? process.cwd();
|
|
60
|
+
const globalConfigDir = join(homedir(), ".frappe-builder");
|
|
61
|
+
const globalConfigPath = join(globalConfigDir, "config.json");
|
|
62
|
+
const projectConfigPath = join(projectRoot, ".frappe-builder-config.json");
|
|
63
|
+
console.log("\n=== frappe-builder Setup ===\n");
|
|
64
|
+
console.log(`[Global config: ${globalConfigPath}]`);
|
|
65
|
+
let globalConfig = {};
|
|
66
|
+
let globalAction = "written";
|
|
67
|
+
if (existsSync(globalConfigPath)) {
|
|
68
|
+
try {
|
|
69
|
+
globalConfig = JSON.parse(readFileSync(globalConfigPath, "utf-8"));
|
|
70
|
+
} catch {}
|
|
71
|
+
if (!cancelled) {
|
|
72
|
+
const overwrite = await promptYN(`Overwrite existing ${globalConfigPath}?`);
|
|
73
|
+
if (cancelled) {
|
|
74
|
+
printCancelled();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (!overwrite) {
|
|
78
|
+
globalAction = "skipped";
|
|
79
|
+
console.log(" Keeping existing global config.\n");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (!cancelled && globalAction === "written") {
|
|
84
|
+
const llmKey = await promptLine("LLM API key (leave blank to skip): ");
|
|
85
|
+
if (cancelled) {
|
|
86
|
+
printCancelled();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
globalConfig.llm_api_key = llmKey.trim();
|
|
90
|
+
}
|
|
91
|
+
console.log(`\n[Project config: ${projectConfigPath}]`);
|
|
92
|
+
let projectConfig = {};
|
|
93
|
+
let projectAction = "written";
|
|
94
|
+
if (existsSync(projectConfigPath)) {
|
|
95
|
+
try {
|
|
96
|
+
projectConfig = JSON.parse(readFileSync(projectConfigPath, "utf-8"));
|
|
97
|
+
} catch {}
|
|
98
|
+
if (!cancelled) {
|
|
99
|
+
const overwrite = await promptYN(`Overwrite existing ${projectConfigPath}?`);
|
|
100
|
+
if (cancelled) {
|
|
101
|
+
printCancelled();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (!overwrite) {
|
|
105
|
+
projectAction = "skipped";
|
|
106
|
+
console.log(" Keeping existing project config.\n");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (!cancelled && projectAction === "written") {
|
|
111
|
+
const siteUrl = await promptLine("Frappe site URL (e.g. http://site1.localhost): ");
|
|
112
|
+
if (cancelled) {
|
|
113
|
+
printCancelled();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const apiKey = await promptLine("Frappe API key: ");
|
|
117
|
+
if (cancelled) {
|
|
118
|
+
printCancelled();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const apiSecret = await promptLine("Frappe API secret: ");
|
|
122
|
+
if (cancelled) {
|
|
123
|
+
printCancelled();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
projectConfig = {
|
|
127
|
+
site_url: siteUrl.trim(),
|
|
128
|
+
api_key: apiKey.trim(),
|
|
129
|
+
api_secret: apiSecret.trim()
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const written = [];
|
|
133
|
+
const skipped = [];
|
|
134
|
+
if (globalAction === "written") {
|
|
135
|
+
mkdirSync(globalConfigDir, { recursive: true });
|
|
136
|
+
writeAtomic(globalConfigPath, JSON.stringify(globalConfig, null, 2) + "\n");
|
|
137
|
+
written.push(`~/.frappe-builder/config.json`);
|
|
138
|
+
} else skipped.push(`~/.frappe-builder/config.json`);
|
|
139
|
+
if (projectAction === "written") {
|
|
140
|
+
writeAtomic(projectConfigPath, JSON.stringify(projectConfig, null, 2) + "\n");
|
|
141
|
+
written.push(`.frappe-builder-config.json`);
|
|
142
|
+
} else skipped.push(`.frappe-builder-config.json`);
|
|
143
|
+
const gitignoreResult = patchGitignore(projectRoot, ".frappe-builder-config.json");
|
|
144
|
+
if (gitignoreResult === "patched") written.push(".gitignore (patched)");
|
|
145
|
+
else if (gitignoreResult === "created") written.push(".gitignore (created)");
|
|
146
|
+
else skipped.push(".gitignore (entry already present)");
|
|
147
|
+
console.log("\nFiles written:");
|
|
148
|
+
for (const f of written) console.log(` ✓ ${f}`);
|
|
149
|
+
if (skipped.length > 0) {
|
|
150
|
+
console.log("Skipped:");
|
|
151
|
+
for (const f of skipped) console.log(` - ${f}`);
|
|
152
|
+
}
|
|
153
|
+
console.log("\nReady. Run: frappe-builder\n");
|
|
154
|
+
}
|
|
155
|
+
function printCancelled() {
|
|
156
|
+
console.log("\nSetup cancelled. No files were written.\n");
|
|
157
|
+
}
|
|
158
|
+
//#endregion
|
|
159
|
+
export { runInit };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
5
|
+
//#region config/loader.ts
|
|
6
|
+
/** Always ~/.frappe-builder/sessions/ — never the project directory (NFR9). */
|
|
7
|
+
const SESSION_LOG_DIR = join(homedir(), ".frappe-builder", "sessions");
|
|
8
|
+
const GITIGNORE_ERROR = "Site credentials file is not gitignored. Add .frappe-builder-config.json to .gitignore before proceeding.";
|
|
9
|
+
/**
|
|
10
|
+
* Validates that {projectRoot}/.gitignore lists .frappe-builder-config.json.
|
|
11
|
+
* Throws with the exact AC error message if missing or absent.
|
|
12
|
+
*/
|
|
13
|
+
function validateGitignore(projectRoot) {
|
|
14
|
+
const gitignorePath = join(projectRoot, ".gitignore");
|
|
15
|
+
if (!existsSync(gitignorePath)) throw new Error(GITIGNORE_ERROR);
|
|
16
|
+
if (!readFileSync(gitignorePath, "utf8").split("\n").map((l) => l.trim()).includes(".frappe-builder-config.json")) throw new Error(GITIGNORE_ERROR);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Reads LLM API keys from ~/.frappe-builder/config.json (user-global, never project).
|
|
20
|
+
* Creates ~/.frappe-builder/ on first run. Returns empty defaults if file absent.
|
|
21
|
+
*/
|
|
22
|
+
function loadGlobalConfig() {
|
|
23
|
+
const configDir = join(homedir(), ".frappe-builder");
|
|
24
|
+
mkdirSync(configDir, { recursive: true });
|
|
25
|
+
const configPath = join(configDir, "config.json");
|
|
26
|
+
if (!existsSync(configPath)) return { llm_api_key: "" };
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(readFileSync(configPath, "utf8"));
|
|
29
|
+
} catch {
|
|
30
|
+
return { llm_api_key: "" };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Reads Frappe site credentials from {projectRoot}/.frappe-builder-config.json.
|
|
35
|
+
* Returns empty defaults if file absent (caller should surface a warning).
|
|
36
|
+
*/
|
|
37
|
+
function loadProjectConfig(projectRoot) {
|
|
38
|
+
const configPath = join(projectRoot, ".frappe-builder-config.json");
|
|
39
|
+
if (!existsSync(configPath)) return {
|
|
40
|
+
site_url: "",
|
|
41
|
+
api_key: "",
|
|
42
|
+
api_secret: ""
|
|
43
|
+
};
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(readFileSync(configPath, "utf8"));
|
|
46
|
+
} catch {
|
|
47
|
+
return {
|
|
48
|
+
site_url: "",
|
|
49
|
+
api_key: "",
|
|
50
|
+
api_secret: ""
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Full credential loader: validates gitignore, loads global + project configs.
|
|
56
|
+
* Throws if .frappe-builder-config.json is not gitignored.
|
|
57
|
+
* API keys are only ever read from ~/.frappe-builder/ — never from projectRoot.
|
|
58
|
+
*/
|
|
59
|
+
function loadCredentials(projectRoot) {
|
|
60
|
+
validateGitignore(projectRoot);
|
|
61
|
+
return {
|
|
62
|
+
global: loadGlobalConfig(),
|
|
63
|
+
project: loadProjectConfig(projectRoot),
|
|
64
|
+
sessionLogDir: SESSION_LOG_DIR
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
export { loadCredentials as t };
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frappe-builder",
|
|
3
|
-
"version": "1.1.0-dev.
|
|
3
|
+
"version": "1.1.0-dev.9",
|
|
4
4
|
"description": "Frappe-native AI co-pilot for building and customising Frappe/ERPNext applications",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"frappe-builder": "./dist/cli.mjs"
|
|
8
|
+
},
|
|
6
9
|
"pi": {
|
|
7
10
|
"_note": "TODO: verify 'pi' field schema against @mariozechner/pi-agent-core docs — no schema found in installed package. Reference schema below is a best-guess pending confirmation.",
|
|
8
11
|
"extensions": [
|