@getjack/jack 0.1.16 → 0.1.19
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/README.md +1 -1
- package/package.json +1 -1
- package/src/commands/clone.ts +62 -40
- package/src/commands/community.ts +47 -0
- package/src/commands/init.ts +6 -0
- package/src/commands/services.ts +354 -9
- package/src/commands/sync.ts +9 -0
- package/src/commands/update.ts +10 -0
- package/src/index.ts +7 -0
- package/src/lib/control-plane.ts +62 -0
- package/src/lib/hooks.ts +20 -0
- package/src/lib/managed-deploy.ts +26 -2
- package/src/lib/output.ts +21 -3
- package/src/lib/progress.ts +160 -0
- package/src/lib/project-operations.ts +381 -93
- package/src/lib/services/db-create.ts +6 -3
- package/src/lib/services/db-execute.ts +485 -0
- package/src/lib/services/sql-classifier.test.ts +404 -0
- package/src/lib/services/sql-classifier.ts +346 -0
- package/src/lib/storage/file-filter.ts +4 -0
- package/src/lib/telemetry.ts +3 -0
- package/src/lib/version-check.ts +14 -0
- package/src/lib/wrangler-config.test.ts +322 -0
- package/src/lib/wrangler-config.ts +649 -0
- package/src/lib/zip-packager.ts +38 -0
- package/src/lib/zip-utils.ts +38 -0
- package/src/mcp/tools/index.ts +161 -0
- package/src/templates/index.ts +4 -0
- package/src/templates/types.ts +12 -0
- package/templates/api/AGENTS.md +33 -0
- package/templates/hello/AGENTS.md +33 -0
- package/templates/miniapp/.jack.json +4 -5
- package/templates/miniapp/AGENTS.md +33 -0
- package/templates/nextjs/AGENTS.md +33 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL Risk Classification
|
|
3
|
+
*
|
|
4
|
+
* Classifies SQL statements by risk level to enable security guardrails:
|
|
5
|
+
* - read: SELECT, EXPLAIN, PRAGMA (read-only) - safe to run
|
|
6
|
+
* - write: INSERT, UPDATE, DELETE (with WHERE) - requires --write flag
|
|
7
|
+
* - destructive: DROP, TRUNCATE, DELETE (no WHERE), ALTER - requires --write + confirmation
|
|
8
|
+
*
|
|
9
|
+
* Uses simple regex-based classification since D1 uses SQLite with a limited SQL surface.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type RiskLevel = "read" | "write" | "destructive";
|
|
13
|
+
|
|
14
|
+
export interface ClassifiedStatement {
|
|
15
|
+
sql: string;
|
|
16
|
+
risk: RiskLevel;
|
|
17
|
+
operation: string; // SELECT, INSERT, DROP, etc.
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Patterns for detecting SQL operations.
|
|
22
|
+
* Order matters for operations that might overlap.
|
|
23
|
+
*/
|
|
24
|
+
const SQL_PATTERNS = {
|
|
25
|
+
// Read operations (safe)
|
|
26
|
+
select: /^\s*SELECT\b/i,
|
|
27
|
+
explain: /^\s*EXPLAIN\b/i,
|
|
28
|
+
pragma_read: /^\s*PRAGMA\s+\w+\s*(?:;?\s*$|\()/i, // PRAGMA table_info(...) or PRAGMA journal_mode;
|
|
29
|
+
// CTE (WITH) - handled specially in classifyStatement based on actual operation
|
|
30
|
+
|
|
31
|
+
// Destructive operations (dangerous - require confirmation)
|
|
32
|
+
drop: /^\s*DROP\b/i,
|
|
33
|
+
truncate: /^\s*TRUNCATE\b/i,
|
|
34
|
+
alter: /^\s*ALTER\b/i,
|
|
35
|
+
|
|
36
|
+
// Write operations (require --write flag)
|
|
37
|
+
insert: /^\s*INSERT\b/i,
|
|
38
|
+
update: /^\s*UPDATE\b/i,
|
|
39
|
+
delete: /^\s*DELETE\b/i,
|
|
40
|
+
replace: /^\s*REPLACE\b/i,
|
|
41
|
+
create: /^\s*CREATE\b/i,
|
|
42
|
+
pragma_write: /^\s*PRAGMA\s+\w+\s*=\s*/i, // PRAGMA setting = value
|
|
43
|
+
|
|
44
|
+
// CTE pattern (just to detect WITH statements)
|
|
45
|
+
with_cte: /^\s*WITH\b/i,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Strip comments from SQL for classification purposes.
|
|
50
|
+
* Preserves the actual SQL content after comments.
|
|
51
|
+
*/
|
|
52
|
+
function stripComments(sql: string): string {
|
|
53
|
+
return sql
|
|
54
|
+
.replace(/--.*$/gm, "") // Single line comments
|
|
55
|
+
.replace(/\/\*[\s\S]*?\*\//g, "") // Multi-line comments
|
|
56
|
+
.trim();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check if a DELETE statement has a WHERE clause.
|
|
61
|
+
* DELETE without WHERE is destructive (deletes all rows).
|
|
62
|
+
*/
|
|
63
|
+
function isDeleteWithoutWhere(sql: string): boolean {
|
|
64
|
+
// Remove comments and normalize whitespace
|
|
65
|
+
const cleaned = sql
|
|
66
|
+
.replace(/--.*$/gm, "") // Single line comments
|
|
67
|
+
.replace(/\/\*[\s\S]*?\*\//g, "") // Multi-line comments
|
|
68
|
+
.replace(/\s+/g, " ")
|
|
69
|
+
.trim();
|
|
70
|
+
|
|
71
|
+
// Check if it's a DELETE statement
|
|
72
|
+
if (!SQL_PATTERNS.delete.test(cleaned)) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Check for WHERE clause (case-insensitive)
|
|
77
|
+
// Match WHERE followed by anything (column name, space, etc.)
|
|
78
|
+
const hasWhere = /\bWHERE\b/i.test(cleaned);
|
|
79
|
+
|
|
80
|
+
return !hasWhere;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Extract the primary operation from a SQL statement.
|
|
85
|
+
* For CTEs (WITH clauses), finds the actual operation after the CTE definitions.
|
|
86
|
+
*/
|
|
87
|
+
function extractOperation(sql: string): string {
|
|
88
|
+
const cleaned = sql.trim().toUpperCase();
|
|
89
|
+
|
|
90
|
+
// Handle WITH clauses (CTEs)
|
|
91
|
+
// The actual operation comes after all the CTE definitions end
|
|
92
|
+
// Pattern: WITH name AS (...), name2 AS (...) <ACTUAL_OPERATION>
|
|
93
|
+
if (cleaned.startsWith("WITH")) {
|
|
94
|
+
// Find the main operation by looking for DML keyword after CTE definitions close
|
|
95
|
+
// CTE definitions are enclosed in parentheses, so find the operation after the
|
|
96
|
+
// last `)` that matches the CTE pattern
|
|
97
|
+
// Look for: ) followed by optional whitespace then SELECT/INSERT/UPDATE/DELETE
|
|
98
|
+
const cteEndMatch = cleaned.match(/\)\s*(SELECT|INSERT|UPDATE|DELETE)\b/i);
|
|
99
|
+
if (cteEndMatch) {
|
|
100
|
+
return cteEndMatch[1].toUpperCase();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Fallback: if no match, look for any of these operations
|
|
104
|
+
// (handles malformed or edge cases)
|
|
105
|
+
if (/\bDELETE\b/.test(cleaned)) return "DELETE";
|
|
106
|
+
if (/\bUPDATE\b/.test(cleaned)) return "UPDATE";
|
|
107
|
+
if (/\bINSERT\b/.test(cleaned)) return "INSERT";
|
|
108
|
+
if (/\bSELECT\b/.test(cleaned)) return "SELECT";
|
|
109
|
+
|
|
110
|
+
return "WITH";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Extract first keyword
|
|
114
|
+
const match = cleaned.match(/^\s*(\w+)/);
|
|
115
|
+
return match?.[1] ?? "UNKNOWN";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Classify a single SQL statement by risk level.
|
|
120
|
+
*/
|
|
121
|
+
export function classifyStatement(sql: string): ClassifiedStatement {
|
|
122
|
+
const trimmed = sql.trim();
|
|
123
|
+
// Strip comments for classification but preserve original SQL
|
|
124
|
+
const cleaned = stripComments(trimmed);
|
|
125
|
+
const operation = extractOperation(cleaned);
|
|
126
|
+
|
|
127
|
+
// Handle CTE (WITH) statements based on their actual operation
|
|
128
|
+
// This must come early because CTEs don't start with the operation keyword
|
|
129
|
+
if (SQL_PATTERNS.with_cte.test(cleaned)) {
|
|
130
|
+
// Classify based on the actual operation extracted from the CTE
|
|
131
|
+
switch (operation) {
|
|
132
|
+
case "DELETE":
|
|
133
|
+
// Check if DELETE in CTE has WHERE clause
|
|
134
|
+
// For CTEs, we check if DELETE...WHERE exists anywhere after the CTE defs
|
|
135
|
+
if (!/\bDELETE\b[^;]*\bWHERE\b/i.test(cleaned)) {
|
|
136
|
+
return { sql: trimmed, risk: "destructive", operation: "DELETE" };
|
|
137
|
+
}
|
|
138
|
+
return { sql: trimmed, risk: "write", operation: "DELETE" };
|
|
139
|
+
case "INSERT":
|
|
140
|
+
return { sql: trimmed, risk: "write", operation: "INSERT" };
|
|
141
|
+
case "UPDATE":
|
|
142
|
+
return { sql: trimmed, risk: "write", operation: "UPDATE" };
|
|
143
|
+
case "SELECT":
|
|
144
|
+
return { sql: trimmed, risk: "read", operation: "SELECT" };
|
|
145
|
+
default:
|
|
146
|
+
// Unknown CTE operation - default to write for safety
|
|
147
|
+
return { sql: trimmed, risk: "write", operation };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Check destructive operations first (highest risk)
|
|
152
|
+
if (SQL_PATTERNS.drop.test(cleaned)) {
|
|
153
|
+
return { sql: trimmed, risk: "destructive", operation: "DROP" };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (SQL_PATTERNS.truncate.test(cleaned)) {
|
|
157
|
+
return { sql: trimmed, risk: "destructive", operation: "TRUNCATE" };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (SQL_PATTERNS.alter.test(cleaned)) {
|
|
161
|
+
return { sql: trimmed, risk: "destructive", operation: "ALTER" };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// DELETE without WHERE is destructive
|
|
165
|
+
if (isDeleteWithoutWhere(cleaned)) {
|
|
166
|
+
return { sql: trimmed, risk: "destructive", operation: "DELETE" };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Check write operations
|
|
170
|
+
if (SQL_PATTERNS.insert.test(cleaned)) {
|
|
171
|
+
return { sql: trimmed, risk: "write", operation: "INSERT" };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (SQL_PATTERNS.update.test(cleaned)) {
|
|
175
|
+
return { sql: trimmed, risk: "write", operation: "UPDATE" };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (SQL_PATTERNS.delete.test(cleaned)) {
|
|
179
|
+
// Has WHERE clause (checked above)
|
|
180
|
+
return { sql: trimmed, risk: "write", operation: "DELETE" };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (SQL_PATTERNS.replace.test(cleaned)) {
|
|
184
|
+
return { sql: trimmed, risk: "write", operation: "REPLACE" };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (SQL_PATTERNS.create.test(cleaned)) {
|
|
188
|
+
return { sql: trimmed, risk: "write", operation: "CREATE" };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (SQL_PATTERNS.pragma_write.test(cleaned)) {
|
|
192
|
+
return { sql: trimmed, risk: "write", operation: "PRAGMA" };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Check read operations
|
|
196
|
+
if (SQL_PATTERNS.select.test(cleaned)) {
|
|
197
|
+
return { sql: trimmed, risk: "read", operation: "SELECT" };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (SQL_PATTERNS.explain.test(cleaned)) {
|
|
201
|
+
return { sql: trimmed, risk: "read", operation: "EXPLAIN" };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (SQL_PATTERNS.pragma_read.test(cleaned)) {
|
|
205
|
+
return { sql: trimmed, risk: "read", operation: "PRAGMA" };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Unknown operations default to write for safety
|
|
209
|
+
return { sql: trimmed, risk: "write", operation };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Split SQL into individual statements.
|
|
214
|
+
* Handles semicolons inside strings and comments.
|
|
215
|
+
*/
|
|
216
|
+
export function splitStatements(sql: string): string[] {
|
|
217
|
+
const statements: string[] = [];
|
|
218
|
+
let current = "";
|
|
219
|
+
let inString: string | null = null;
|
|
220
|
+
let inComment = false;
|
|
221
|
+
let inMultilineComment = false;
|
|
222
|
+
|
|
223
|
+
for (let i = 0; i < sql.length; i++) {
|
|
224
|
+
const char = sql[i];
|
|
225
|
+
const nextChar = sql[i + 1];
|
|
226
|
+
|
|
227
|
+
// Handle multiline comments
|
|
228
|
+
if (!inString && !inComment && char === "/" && nextChar === "*") {
|
|
229
|
+
inMultilineComment = true;
|
|
230
|
+
current += char;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (inMultilineComment && char === "*" && nextChar === "/") {
|
|
235
|
+
current += "*/";
|
|
236
|
+
i++; // Skip the /
|
|
237
|
+
inMultilineComment = false;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (inMultilineComment) {
|
|
242
|
+
current += char;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Handle single-line comments
|
|
247
|
+
if (!inString && char === "-" && nextChar === "-") {
|
|
248
|
+
inComment = true;
|
|
249
|
+
current += char;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (inComment && char === "\n") {
|
|
254
|
+
inComment = false;
|
|
255
|
+
current += char;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (inComment) {
|
|
260
|
+
current += char;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Handle strings
|
|
265
|
+
if (!inString && (char === "'" || char === '"')) {
|
|
266
|
+
inString = char;
|
|
267
|
+
current += char;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (inString === char) {
|
|
272
|
+
// Check for escaped quote
|
|
273
|
+
if (nextChar === char) {
|
|
274
|
+
current += char + char;
|
|
275
|
+
i++; // Skip the escaped quote
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
inString = null;
|
|
279
|
+
current += char;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (inString) {
|
|
284
|
+
current += char;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Handle statement terminator
|
|
289
|
+
if (char === ";") {
|
|
290
|
+
const trimmed = current.trim();
|
|
291
|
+
if (trimmed) {
|
|
292
|
+
statements.push(trimmed);
|
|
293
|
+
}
|
|
294
|
+
current = "";
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
current += char;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Add final statement if present
|
|
302
|
+
const trimmed = current.trim();
|
|
303
|
+
if (trimmed) {
|
|
304
|
+
statements.push(trimmed);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return statements;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Classify multiple SQL statements and return the highest risk level.
|
|
312
|
+
*/
|
|
313
|
+
export function classifyStatements(sql: string): {
|
|
314
|
+
statements: ClassifiedStatement[];
|
|
315
|
+
highestRisk: RiskLevel;
|
|
316
|
+
} {
|
|
317
|
+
const statements = splitStatements(sql).map(classifyStatement);
|
|
318
|
+
|
|
319
|
+
// Find highest risk level
|
|
320
|
+
let highestRisk: RiskLevel = "read";
|
|
321
|
+
for (const stmt of statements) {
|
|
322
|
+
if (stmt.risk === "destructive") {
|
|
323
|
+
highestRisk = "destructive";
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
if (stmt.risk === "write" && highestRisk === "read") {
|
|
327
|
+
highestRisk = "write";
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return { statements, highestRisk };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Get a human-readable description of the risk level.
|
|
336
|
+
*/
|
|
337
|
+
export function getRiskDescription(risk: RiskLevel): string {
|
|
338
|
+
switch (risk) {
|
|
339
|
+
case "read":
|
|
340
|
+
return "Read-only query";
|
|
341
|
+
case "write":
|
|
342
|
+
return "Write operation (modifies data)";
|
|
343
|
+
case "destructive":
|
|
344
|
+
return "Destructive operation (may cause data loss)";
|
|
345
|
+
}
|
|
346
|
+
}
|
package/src/lib/telemetry.ts
CHANGED
|
@@ -33,6 +33,9 @@ export const Events = {
|
|
|
33
33
|
AUTO_DETECT_SUCCESS: "auto_detect_success",
|
|
34
34
|
AUTO_DETECT_FAILED: "auto_detect_failed",
|
|
35
35
|
AUTO_DETECT_REJECTED: "auto_detect_rejected",
|
|
36
|
+
// Service events
|
|
37
|
+
SERVICE_CREATED: "service_created",
|
|
38
|
+
SQL_EXECUTED: "sql_executed",
|
|
36
39
|
} as const;
|
|
37
40
|
|
|
38
41
|
type EventName = (typeof Events)[keyof typeof Events];
|
package/src/lib/version-check.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { join } from "node:path";
|
|
|
7
7
|
import { $ } from "bun";
|
|
8
8
|
import pkg from "../../package.json";
|
|
9
9
|
import { CONFIG_DIR } from "./config.ts";
|
|
10
|
+
import { debug } from "./debug.ts";
|
|
10
11
|
|
|
11
12
|
const VERSION_CACHE_PATH = join(CONFIG_DIR, "version-cache.json");
|
|
12
13
|
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
@@ -123,8 +124,17 @@ export async function performUpdate(): Promise<{
|
|
|
123
124
|
}> {
|
|
124
125
|
try {
|
|
125
126
|
// Run bun add -g to update
|
|
127
|
+
debug(`Executing: bun add -g ${PACKAGE_NAME}@latest`);
|
|
126
128
|
const result = await $`bun add -g ${PACKAGE_NAME}@latest`.nothrow().quiet();
|
|
127
129
|
|
|
130
|
+
debug(`Exit code: ${result.exitCode}`);
|
|
131
|
+
if (result.stdout.toString()) {
|
|
132
|
+
debug(`stdout: ${result.stdout.toString()}`);
|
|
133
|
+
}
|
|
134
|
+
if (result.stderr.toString()) {
|
|
135
|
+
debug(`stderr: ${result.stderr.toString()}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
128
138
|
if (result.exitCode !== 0) {
|
|
129
139
|
return {
|
|
130
140
|
success: false,
|
|
@@ -133,12 +143,15 @@ export async function performUpdate(): Promise<{
|
|
|
133
143
|
}
|
|
134
144
|
|
|
135
145
|
// Verify the new version
|
|
146
|
+
debug("Verifying installed version...");
|
|
136
147
|
const newVersionResult = await $`bun pm ls -g`.nothrow().quiet();
|
|
137
148
|
const output = newVersionResult.stdout.toString();
|
|
149
|
+
debug(`bun pm ls -g output: ${output.slice(0, 500)}`);
|
|
138
150
|
|
|
139
151
|
// Try to extract version from output
|
|
140
152
|
const versionMatch = output.match(/@getjack\/jack@(\d+\.\d+\.\d+)/);
|
|
141
153
|
const newVersion = versionMatch?.[1];
|
|
154
|
+
debug(`Extracted version: ${newVersion ?? "not found"}`);
|
|
142
155
|
|
|
143
156
|
// Clear version cache so next check gets fresh data
|
|
144
157
|
try {
|
|
@@ -152,6 +165,7 @@ export async function performUpdate(): Promise<{
|
|
|
152
165
|
version: newVersion,
|
|
153
166
|
};
|
|
154
167
|
} catch (err) {
|
|
168
|
+
debug(`Update error: ${err instanceof Error ? err.message : String(err)}`);
|
|
155
169
|
return {
|
|
156
170
|
success: false,
|
|
157
171
|
error: err instanceof Error ? err.message : "Unknown error",
|