frappe-builder 1.1.0-dev.9 → 1.2.0-dev.29

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.
Files changed (47) hide show
  1. package/.fb/state.db +0 -0
  2. package/.frappe-builder/po-approval/implementation-artifacts/sprint-status.yaml +15 -0
  3. package/AGENTS.md +59 -130
  4. package/README.md +14 -21
  5. package/agents/frappe-architect.md +29 -0
  6. package/agents/frappe-ba.md +28 -0
  7. package/agents/frappe-dev.md +25 -0
  8. package/agents/frappe-docs.md +27 -0
  9. package/agents/frappe-planner.md +28 -0
  10. package/agents/frappe-qa.md +28 -0
  11. package/config/constants.ts +45 -0
  12. package/config/defaults.ts +11 -3
  13. package/config/loader.ts +18 -84
  14. package/dist/cli.mjs +49 -36
  15. package/dist/init-DvtJrAiJ.mjs +233 -0
  16. package/extensions/agent-chain.ts +254 -0
  17. package/extensions/frappe-gates.ts +31 -7
  18. package/extensions/frappe-session.ts +11 -3
  19. package/extensions/frappe-state.ts +110 -20
  20. package/extensions/frappe-tools.ts +52 -29
  21. package/extensions/frappe-ui.ts +100 -40
  22. package/extensions/frappe-workflow.ts +82 -13
  23. package/extensions/pi-types.ts +53 -0
  24. package/package.json +2 -2
  25. package/state/artifacts.ts +85 -0
  26. package/state/db.ts +18 -4
  27. package/state/fsm.ts +33 -13
  28. package/state/schema.ts +42 -3
  29. package/tools/agent-tools.ts +71 -5
  30. package/tools/bench-tools.ts +4 -8
  31. package/tools/context-sandbox.ts +11 -7
  32. package/tools/feature-tools.ts +125 -8
  33. package/tools/frappe-context7.ts +28 -32
  34. package/tools/frappe-query-tools.ts +75 -20
  35. package/tools/project-tools.ts +14 -11
  36. package/dist/coverage-check-DLGO_qwW.mjs +0 -55
  37. package/dist/db-Cx_EyUEu.mjs +0 -58
  38. package/dist/frappe-gates-c4HHJp-4.mjs +0 -349
  39. package/dist/frappe-session-BfFveYq1.mjs +0 -5
  40. package/dist/frappe-session-BzM5oUCb.mjs +0 -5
  41. package/dist/frappe-state-k--gX3wq.mjs +0 -6
  42. package/dist/frappe-tools-Dwz0eEQ-.mjs +0 -13
  43. package/dist/frappe-ui-htmQgO8t.mjs +0 -3
  44. package/dist/frappe-workflow-VId2tr9e.mjs +0 -4
  45. package/dist/fsm-DkLob1CA.mjs +0 -3
  46. package/dist/init-ChmHonBN.mjs +0 -159
  47. package/dist/loader-DC2PlJU7.mjs +0 -68
@@ -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,89 @@ 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
+
20
+ /**
21
+ * Retries a fetch on transient server errors (5xx) and rate limiting (429).
22
+ * Uses linear backoff (1s × attempt). Passes through 4xx client errors immediately.
23
+ */
24
+ async function fetchWithRetry(url: string, options: RequestInit, maxRetries = 3): Promise<Response> {
25
+ let lastResponse: Response | undefined;
26
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
27
+ const response = await fetch(url, options);
28
+ if (response.ok || (response.status >= 400 && response.status < 500 && response.status !== 429)) {
29
+ return response;
30
+ }
31
+ lastResponse = response;
32
+ if (attempt < maxRetries) {
33
+ await new Promise((r) => setTimeout(r, 1000 * attempt));
34
+ }
35
+ }
36
+ return lastResponse!;
37
+ }
38
+
39
+ /**
40
+ * Builds and validates the Frappe REST resource URL.
41
+ * Strips trailing slash from siteUrl, uses URL constructor for validation.
42
+ * Returns null if siteUrl is not a valid URL.
43
+ */
44
+ function buildResourceUrl(siteUrl: string, doctype: string, params: URLSearchParams): string | null {
45
+ try {
46
+ const base = siteUrl.replace(/\/$/, "");
47
+ const url = new URL(`${base}/api/resource/${encodeURIComponent(doctype)}`);
48
+ url.search = params.toString();
49
+ return url.toString();
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
15
55
  /**
16
- * Dispatches a Frappe get_list query via mcp2cli subprocess.
17
- * Raw output is routed through context-mode sandbox — never returned directly.
56
+ * Queries Frappe data via direct REST API call using credentials stored in the
57
+ * active session (set via set_active_project).
58
+ * Output is truncation-guarded — never returns raw payloads over 8K tokens.
18
59
  * 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
60
  */
23
61
  export async function frappeQuery({ doctype, filters }: FrappeQueryArgs): Promise<FrappeQueryResult> {
24
- const config = loadConfig();
62
+ const session = db
63
+ .prepare("SELECT site_url, api_key, api_secret FROM sessions WHERE is_active = 1 LIMIT 1")
64
+ .get() as SessionCredentials | undefined;
25
65
 
26
- if (!config.frappeMcpUrl) {
27
- return { error: "frappeMcpUrl not configured in ~/.frappe-builder/config.json" };
66
+ if (!session?.site_url) {
67
+ return { error: "No site_url configured. Call set_active_project with siteUrl, apiKey, and apiSecret first." };
68
+ }
69
+ if (!session.api_key || !session.api_secret) {
70
+ return { error: "No API credentials configured. Call set_active_project with apiKey and apiSecret." };
28
71
  }
29
72
 
30
73
  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
- ]);
74
+ const params = new URLSearchParams();
75
+ if (filters && Object.keys(filters).length > 0) {
76
+ params.set("filters", JSON.stringify(filters));
77
+ }
78
+
79
+ const url = buildResourceUrl(session.site_url, doctype, params);
80
+ if (!url) {
81
+ return { error: `Invalid site_url: "${session.site_url}". Must be a valid URL (e.g. http://site1.localhost).` };
82
+ }
83
+
84
+ const response = await fetchWithRetry(url, {
85
+ headers: {
86
+ Authorization: `token ${session.api_key}:${session.api_secret}`,
87
+ "Content-Type": "application/json",
88
+ },
89
+ });
38
90
 
39
- if (stderr) console.warn(`[frappe_query mcp2cli stderr: ${stderr}]`);
91
+ if (!response.ok) {
92
+ return { error: `Frappe API error ${response.status}: ${response.statusText}` };
93
+ }
40
94
 
41
- const summary = await routeThroughContextMode(stdout);
95
+ const raw = await response.text();
96
+ const summary = applyTruncationFallback(raw);
42
97
  return { summary };
43
98
  } catch (err) {
44
99
  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 {
@@ -77,30 +77,33 @@ export function getProjectStatus(_args?: unknown): ProjectStatus {
77
77
  interface SetActiveProjectArgs {
78
78
  projectId: string;
79
79
  sitePath: string;
80
+ appPath?: string;
81
+ siteUrl?: string;
82
+ apiKey?: string;
83
+ apiSecret?: string;
80
84
  }
81
85
 
82
86
  /**
83
87
  * Switches the active Frappe project and site, flushes current session state,
84
88
  * creates a new session, and reloads the system prompt context.
85
- *
86
- * The state_transition JSONL entry is written inside switchProject()
87
- * 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.
88
91
  */
89
- export async function setActiveProject({ projectId, sitePath }: SetActiveProjectArgs) {
90
- // Flush current state + create new session (JSONL entry written internally)
91
- switchProject(projectId, sitePath);
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);
92
95
 
93
- // Reload system prompt with new project context
94
96
  await reloadSessionContext();
95
97
 
96
- // Read restored phase for return value
97
98
  const session = db
98
- .prepare("SELECT current_phase FROM sessions WHERE is_active = 1 LIMIT 1")
99
- .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;
100
101
 
101
102
  return {
102
103
  project_id: projectId,
103
104
  site_path: sitePath,
105
+ app_path: appPath ?? null,
106
+ site_url: session?.site_url ?? null,
104
107
  phase: session?.current_phase ?? "idle",
105
108
  context_reloaded: true,
106
109
  };
@@ -1,55 +0,0 @@
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 };
@@ -1,58 +0,0 @@
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 };
@@ -1,349 +0,0 @@
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 {};
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env node
2
- import "./db-Cx_EyUEu.mjs";
3
- import "./fsm-DkLob1CA.mjs";
4
- import "./frappe-session-BzM5oUCb.mjs";
5
- export {};
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env node
2
- import "node:os";
3
- import "node:path";
4
- import "node:fs";
5
- export {};
@@ -1,6 +0,0 @@
1
- #!/usr/bin/env node
2
- import "./loader-DC2PlJU7.mjs";
3
- import "./db-Cx_EyUEu.mjs";
4
- import "./fsm-DkLob1CA.mjs";
5
- import "./frappe-session-BzM5oUCb.mjs";
6
- export {};
@@ -1,13 +0,0 @@
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 {};
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env node
2
- import "./db-Cx_EyUEu.mjs";
3
- export {};
@@ -1,4 +0,0 @@
1
- #!/usr/bin/env node
2
- import "./db-Cx_EyUEu.mjs";
3
- import "./fsm-DkLob1CA.mjs";
4
- export {};
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env node
2
- import "robot3";
3
- export {};