@getjack/jack 0.1.22 → 0.1.23
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/package.json +1 -1
- package/src/commands/clone.ts +5 -5
- package/src/commands/down.ts +44 -69
- package/src/commands/link.ts +9 -6
- package/src/commands/new.ts +54 -76
- package/src/commands/publish.ts +7 -2
- package/src/commands/secrets.ts +2 -1
- package/src/commands/services.ts +41 -15
- package/src/commands/update.ts +2 -2
- package/src/index.ts +43 -2
- package/src/lib/agent-integration.ts +217 -0
- package/src/lib/auth/login-flow.ts +2 -1
- package/src/lib/binding-validator.ts +2 -3
- package/src/lib/build-helper.ts +7 -1
- package/src/lib/hooks.ts +101 -21
- package/src/lib/managed-down.ts +32 -55
- package/src/lib/project-detection.ts +48 -21
- package/src/lib/project-operations.ts +31 -13
- package/src/lib/prompts.ts +16 -23
- package/src/lib/services/db-execute.ts +39 -0
- package/src/lib/services/sql-classifier.test.ts +2 -2
- package/src/lib/services/sql-classifier.ts +5 -4
- package/src/lib/version-check.ts +15 -10
- package/src/lib/zip-packager.ts +16 -0
- package/src/mcp/resources/index.ts +42 -2
- package/src/templates/index.ts +63 -3
- package/templates/ai-chat/.jack.json +29 -0
- package/templates/ai-chat/bun.lock +18 -0
- package/templates/ai-chat/package.json +14 -0
- package/templates/ai-chat/public/chat.js +149 -0
- package/templates/ai-chat/public/index.html +209 -0
- package/templates/ai-chat/src/index.ts +105 -0
- package/templates/ai-chat/wrangler.jsonc +12 -0
- package/templates/semantic-search/.jack.json +26 -0
- package/templates/semantic-search/bun.lock +18 -0
- package/templates/semantic-search/package.json +12 -0
- package/templates/semantic-search/public/app.js +120 -0
- package/templates/semantic-search/public/index.html +210 -0
- package/templates/semantic-search/schema.sql +5 -0
- package/templates/semantic-search/src/index.ts +144 -0
- package/templates/semantic-search/tsconfig.json +13 -0
- package/templates/semantic-search/wrangler.jsonc +27 -0
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from "../templates/index.ts";
|
|
17
17
|
import type { EnvVar, Template } from "../templates/types.ts";
|
|
18
18
|
import { generateAgentFiles } from "./agent-files.ts";
|
|
19
|
+
import { ensureAgentIntegration } from "./agent-integration.ts";
|
|
19
20
|
import {
|
|
20
21
|
getActiveAgents,
|
|
21
22
|
getAgentDefinition,
|
|
@@ -535,14 +536,17 @@ async function runAutoDetectFlow(
|
|
|
535
536
|
dryRun = false,
|
|
536
537
|
): Promise<AutoDetectResult> {
|
|
537
538
|
// Step 1: Validate project (file count, size limits)
|
|
539
|
+
const validationSpin = reporter.spinner("Scanning project...");
|
|
538
540
|
const validation = await validateProject(projectPath);
|
|
539
541
|
if (!validation.valid) {
|
|
542
|
+
validationSpin.error("Project too large or invalid");
|
|
540
543
|
track(Events.AUTO_DETECT_REJECTED, { reason: "validation_failed" });
|
|
541
544
|
throw new JackError(
|
|
542
545
|
JackErrorCode.VALIDATION_ERROR,
|
|
543
546
|
validation.error || "Project validation failed",
|
|
544
547
|
);
|
|
545
548
|
}
|
|
549
|
+
validationSpin.success(`Scanned ${validation.fileCount} files`);
|
|
546
550
|
|
|
547
551
|
// Step 2: Detect project type
|
|
548
552
|
const detection = detectProjectType(projectPath);
|
|
@@ -565,9 +569,11 @@ async function runAutoDetectFlow(
|
|
|
565
569
|
if (detection.type === "unknown") {
|
|
566
570
|
track(Events.AUTO_DETECT_FAILED, { reason: "unknown_type" });
|
|
567
571
|
|
|
572
|
+
// Use detection.error if available (e.g., monorepo message), otherwise generic
|
|
568
573
|
throw new JackError(
|
|
569
574
|
JackErrorCode.VALIDATION_ERROR,
|
|
570
|
-
|
|
575
|
+
detection.error ||
|
|
576
|
+
"Could not detect project type\n\nSupported types:\n - Vite (React, Vue, etc.)\n - Hono API\n - SvelteKit (with @sveltejs/adapter-cloudflare)\n\nTo deploy manually, create a wrangler.jsonc file.\nDocs: https://docs.getjack.org/guides/manual-setup",
|
|
571
577
|
);
|
|
572
578
|
}
|
|
573
579
|
|
|
@@ -875,17 +881,17 @@ export async function createProject(
|
|
|
875
881
|
|
|
876
882
|
// No match - prompt user to choose
|
|
877
883
|
if (interactive) {
|
|
878
|
-
const {
|
|
884
|
+
const { promptSelectValue, isCancel } = await import("./hooks.ts");
|
|
879
885
|
console.error("");
|
|
880
886
|
console.error(` No template matched for: "${intentPhrase}"`);
|
|
881
887
|
console.error("");
|
|
882
888
|
|
|
883
|
-
const choice = await
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
889
|
+
const choice = await promptSelectValue(
|
|
890
|
+
"Select a template:",
|
|
891
|
+
BUILTIN_TEMPLATES.map((t) => ({ value: t, label: t })),
|
|
892
|
+
);
|
|
887
893
|
|
|
888
|
-
if (typeof choice !== "string") {
|
|
894
|
+
if (isCancel(choice) || typeof choice !== "string") {
|
|
889
895
|
throw new JackError(JackErrorCode.VALIDATION_ERROR, "No template selected", undefined, {
|
|
890
896
|
exitCode: 0,
|
|
891
897
|
reported: true,
|
|
@@ -917,18 +923,18 @@ export async function createProject(
|
|
|
917
923
|
|
|
918
924
|
// Multiple matches
|
|
919
925
|
if (interactive) {
|
|
920
|
-
const {
|
|
926
|
+
const { promptSelectValue, isCancel } = await import("./hooks.ts");
|
|
921
927
|
console.error("");
|
|
922
928
|
console.error(` Multiple templates matched: "${intentPhrase}"`);
|
|
923
929
|
console.error("");
|
|
924
930
|
|
|
925
931
|
const matchedNames = matches.map((m) => m.template);
|
|
926
|
-
const choice = await
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
932
|
+
const choice = await promptSelectValue(
|
|
933
|
+
"Select a template:",
|
|
934
|
+
matchedNames.map((t) => ({ value: t, label: t })),
|
|
935
|
+
);
|
|
930
936
|
|
|
931
|
-
if (typeof choice !== "string") {
|
|
937
|
+
if (isCancel(choice) || typeof choice !== "string") {
|
|
932
938
|
throw new JackError(JackErrorCode.VALIDATION_ERROR, "No template selected", undefined, {
|
|
933
939
|
exitCode: 0,
|
|
934
940
|
reported: true,
|
|
@@ -1609,6 +1615,18 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
|
|
|
1609
1615
|
deployMode = link?.deploy_mode ?? "byo";
|
|
1610
1616
|
}
|
|
1611
1617
|
|
|
1618
|
+
// Ensure agent integration is set up (JACK.md, MCP config)
|
|
1619
|
+
// This is idempotent and runs silently
|
|
1620
|
+
try {
|
|
1621
|
+
await ensureAgentIntegration(projectPath, {
|
|
1622
|
+
projectName,
|
|
1623
|
+
silent: true,
|
|
1624
|
+
});
|
|
1625
|
+
} catch (err) {
|
|
1626
|
+
// Don't fail deploy if agent integration fails
|
|
1627
|
+
debug("Agent integration setup failed:", err);
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1612
1630
|
// Ensure wrangler is installed (auto-install if needed)
|
|
1613
1631
|
if (!dryRun) {
|
|
1614
1632
|
let installSpinner: OperationSpinner | null = null;
|
package/src/lib/prompts.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { text } from "@clack/prompts";
|
|
2
|
+
import { isCancel } from "./hooks.ts";
|
|
2
3
|
import type { DetectedSecret } from "./env-parser.ts";
|
|
4
|
+
import { promptSelectValue } from "./hooks.ts";
|
|
3
5
|
import { info, success, warn } from "./output.ts";
|
|
4
6
|
import { getSavedSecrets, getSecretsPath, maskSecret, saveSecrets } from "./secrets.ts";
|
|
5
7
|
|
|
@@ -23,14 +25,11 @@ export async function promptSaveSecrets(detected: DetectedSecret[]): Promise<voi
|
|
|
23
25
|
}
|
|
24
26
|
console.error("");
|
|
25
27
|
|
|
26
|
-
const action = await
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
{ value: "skip", label: "Skip for now" },
|
|
32
|
-
],
|
|
33
|
-
});
|
|
28
|
+
const action = await promptSelectValue("What would you like to do?", [
|
|
29
|
+
{ value: "save", label: "Save to jack for future projects" },
|
|
30
|
+
{ value: "paste", label: "Paste additional secrets" },
|
|
31
|
+
{ value: "skip", label: "Skip for now" },
|
|
32
|
+
]);
|
|
34
33
|
|
|
35
34
|
if (isCancel(action) || action === "skip") {
|
|
36
35
|
return;
|
|
@@ -111,13 +110,10 @@ export async function promptUseSecrets(): Promise<Record<string, string> | null>
|
|
|
111
110
|
}
|
|
112
111
|
console.error("");
|
|
113
112
|
|
|
114
|
-
const action = await
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
{ label: "No", value: "no" },
|
|
119
|
-
],
|
|
120
|
-
});
|
|
113
|
+
const action = await promptSelectValue("Use them for this project?", [
|
|
114
|
+
{ value: "yes", label: "Yes" },
|
|
115
|
+
{ value: "no", label: "No" },
|
|
116
|
+
]);
|
|
121
117
|
|
|
122
118
|
if (isCancel(action) || action !== "yes") {
|
|
123
119
|
return null;
|
|
@@ -154,13 +150,10 @@ export async function promptUseSecretsFromList(
|
|
|
154
150
|
return false;
|
|
155
151
|
}
|
|
156
152
|
|
|
157
|
-
const action = await
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
{ label: "No", value: "no" },
|
|
162
|
-
],
|
|
163
|
-
});
|
|
153
|
+
const action = await promptSelectValue("Use saved secrets for this project?", [
|
|
154
|
+
{ value: "yes", label: "Yes" },
|
|
155
|
+
{ value: "no", label: "No" },
|
|
156
|
+
]);
|
|
164
157
|
|
|
165
158
|
return !isCancel(action) && action === "yes";
|
|
166
159
|
}
|
|
@@ -147,7 +147,26 @@ async function executeViaWrangler(
|
|
|
147
147
|
.quiet();
|
|
148
148
|
|
|
149
149
|
if (result.exitCode !== 0) {
|
|
150
|
+
// Wrangler outputs errors to stdout as JSON, not stderr
|
|
151
|
+
const stdout = result.stdout.toString().trim();
|
|
150
152
|
const stderr = result.stderr.toString().trim();
|
|
153
|
+
|
|
154
|
+
// Try to parse JSON error from stdout first
|
|
155
|
+
try {
|
|
156
|
+
const data = JSON.parse(stdout);
|
|
157
|
+
if (data.error) {
|
|
158
|
+
// Wrangler error format: { error: { text: "...", notes: [{ text: "..." }] } }
|
|
159
|
+
const errorText = data.error.text || data.error.message || "Unknown error";
|
|
160
|
+
const notes = data.error.notes?.map((n: { text: string }) => n.text).join("; ");
|
|
161
|
+
return {
|
|
162
|
+
success: false,
|
|
163
|
+
error: notes ? `${errorText} (${notes})` : errorText,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
} catch {
|
|
167
|
+
// Not JSON, fall through to stderr
|
|
168
|
+
}
|
|
169
|
+
|
|
151
170
|
return {
|
|
152
171
|
success: false,
|
|
153
172
|
error: stderr || `Failed to execute SQL on ${databaseName}`,
|
|
@@ -172,6 +191,8 @@ async function executeViaWrangler(
|
|
|
172
191
|
last_row_id: firstResult.meta.last_row_id,
|
|
173
192
|
}
|
|
174
193
|
: undefined,
|
|
194
|
+
// Capture error details from wrangler response
|
|
195
|
+
error: firstResult.error || firstResult.message,
|
|
175
196
|
};
|
|
176
197
|
}
|
|
177
198
|
|
|
@@ -198,7 +219,25 @@ async function executeFileViaWrangler(
|
|
|
198
219
|
.quiet();
|
|
199
220
|
|
|
200
221
|
if (result.exitCode !== 0) {
|
|
222
|
+
// Wrangler outputs errors to stdout as JSON, not stderr
|
|
223
|
+
const stdout = result.stdout.toString().trim();
|
|
201
224
|
const stderr = result.stderr.toString().trim();
|
|
225
|
+
|
|
226
|
+
// Try to parse JSON error from stdout first
|
|
227
|
+
try {
|
|
228
|
+
const data = JSON.parse(stdout);
|
|
229
|
+
if (data.error) {
|
|
230
|
+
const errorText = data.error.text || data.error.message || "Unknown error";
|
|
231
|
+
const notes = data.error.notes?.map((n: { text: string }) => n.text).join("; ");
|
|
232
|
+
return {
|
|
233
|
+
success: false,
|
|
234
|
+
error: notes ? `${errorText} (${notes})` : errorText,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
} catch {
|
|
238
|
+
// Not JSON, fall through to stderr
|
|
239
|
+
}
|
|
240
|
+
|
|
202
241
|
return {
|
|
203
242
|
success: false,
|
|
204
243
|
error: stderr || `Failed to execute SQL file on ${databaseName}`,
|
|
@@ -229,9 +229,9 @@ describe("sql-classifier", () => {
|
|
|
229
229
|
expect(result.operation).toBe("SELECT");
|
|
230
230
|
});
|
|
231
231
|
|
|
232
|
-
it("treats unknown operations as
|
|
232
|
+
it("treats unknown operations as read (let SQLite handle errors)", () => {
|
|
233
233
|
const result = classifyStatement("VACUUM");
|
|
234
|
-
expect(result.risk).toBe("
|
|
234
|
+
expect(result.risk).toBe("read");
|
|
235
235
|
expect(result.operation).toBe("VACUUM");
|
|
236
236
|
});
|
|
237
237
|
|
|
@@ -143,8 +143,8 @@ export function classifyStatement(sql: string): ClassifiedStatement {
|
|
|
143
143
|
case "SELECT":
|
|
144
144
|
return { sql: trimmed, risk: "read", operation: "SELECT" };
|
|
145
145
|
default:
|
|
146
|
-
// Unknown CTE operation -
|
|
147
|
-
return { sql: trimmed, risk: "
|
|
146
|
+
// Unknown CTE operation - let SQLite handle it
|
|
147
|
+
return { sql: trimmed, risk: "read", operation };
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
|
|
@@ -205,8 +205,9 @@ export function classifyStatement(sql: string): ClassifiedStatement {
|
|
|
205
205
|
return { sql: trimmed, risk: "read", operation: "PRAGMA" };
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
// Unknown operations default to
|
|
209
|
-
|
|
208
|
+
// Unknown operations default to read - let SQLite handle syntax errors
|
|
209
|
+
// Invalid SQL can't modify data, so no point requiring --write for gibberish
|
|
210
|
+
return { sql: trimmed, risk: "read", operation };
|
|
210
211
|
}
|
|
211
212
|
|
|
212
213
|
/**
|
package/src/lib/version-check.ts
CHANGED
|
@@ -85,20 +85,25 @@ function isNewerVersion(latest: string, current: string): boolean {
|
|
|
85
85
|
/**
|
|
86
86
|
* Check if an update is available (uses cache, non-blocking)
|
|
87
87
|
* Returns the latest version if newer, null otherwise
|
|
88
|
+
* @param skipCache - If true, bypass the cache and always fetch from npm
|
|
88
89
|
*/
|
|
89
|
-
export async function checkForUpdate(
|
|
90
|
+
export async function checkForUpdate(
|
|
91
|
+
skipCache = false,
|
|
92
|
+
): Promise<string | null> {
|
|
90
93
|
const currentVersion = getCurrentVersion();
|
|
91
94
|
|
|
92
|
-
// Check cache first
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
// Check cache first (unless skipCache is true)
|
|
96
|
+
if (!skipCache) {
|
|
97
|
+
const cache = await readVersionCache();
|
|
98
|
+
const now = Date.now();
|
|
99
|
+
|
|
100
|
+
if (cache && now - cache.checkedAt < CACHE_TTL_MS) {
|
|
101
|
+
// Use cached value
|
|
102
|
+
if (isNewerVersion(cache.latestVersion, currentVersion)) {
|
|
103
|
+
return cache.latestVersion;
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
100
106
|
}
|
|
101
|
-
return null;
|
|
102
107
|
}
|
|
103
108
|
|
|
104
109
|
// Fetch fresh version (don't await in caller for non-blocking)
|
package/src/lib/zip-packager.ts
CHANGED
|
@@ -45,6 +45,12 @@ export interface ManifestData {
|
|
|
45
45
|
vars?: Record<string, string>;
|
|
46
46
|
r2?: Array<{ binding: string; bucket_name: string }>;
|
|
47
47
|
kv?: Array<{ binding: string }>;
|
|
48
|
+
vectorize?: Array<{
|
|
49
|
+
binding: string;
|
|
50
|
+
preset?: string;
|
|
51
|
+
dimensions?: number;
|
|
52
|
+
metric?: string;
|
|
53
|
+
}>;
|
|
48
54
|
};
|
|
49
55
|
}
|
|
50
56
|
|
|
@@ -224,6 +230,16 @@ function extractBindingsFromConfig(config?: WranglerConfig): ManifestData["bindi
|
|
|
224
230
|
}));
|
|
225
231
|
}
|
|
226
232
|
|
|
233
|
+
// Extract Vectorize bindings
|
|
234
|
+
if (config.vectorize && config.vectorize.length > 0) {
|
|
235
|
+
bindings.vectorize = config.vectorize.map((vec) => ({
|
|
236
|
+
binding: vec.binding,
|
|
237
|
+
preset: vec.preset,
|
|
238
|
+
dimensions: vec.dimensions,
|
|
239
|
+
metric: vec.metric,
|
|
240
|
+
}));
|
|
241
|
+
}
|
|
242
|
+
|
|
227
243
|
// Return undefined if no bindings were extracted
|
|
228
244
|
return Object.keys(bindings).length > 0 ? bindings : undefined;
|
|
229
245
|
}
|
|
@@ -67,15 +67,30 @@ export function registerResources(
|
|
|
67
67
|
|
|
68
68
|
if (uri === "agents://context") {
|
|
69
69
|
const projectPath = options.projectPath ?? process.cwd();
|
|
70
|
+
const jackPath = join(projectPath, "JACK.md");
|
|
70
71
|
const agentsPath = join(projectPath, "AGENTS.md");
|
|
71
72
|
const claudePath = join(projectPath, "CLAUDE.md");
|
|
72
73
|
|
|
73
74
|
const contents: string[] = [];
|
|
74
75
|
|
|
76
|
+
// Try to read JACK.md first (jack-specific context)
|
|
77
|
+
if (existsSync(jackPath)) {
|
|
78
|
+
try {
|
|
79
|
+
const jackContent = await Bun.file(jackPath).text();
|
|
80
|
+
contents.push("# JACK.md\n\n");
|
|
81
|
+
contents.push(jackContent);
|
|
82
|
+
} catch {
|
|
83
|
+
// Ignore read errors
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
75
87
|
// Try to read AGENTS.md
|
|
76
88
|
if (existsSync(agentsPath)) {
|
|
77
89
|
try {
|
|
78
90
|
const agentsContent = await Bun.file(agentsPath).text();
|
|
91
|
+
if (contents.length > 0) {
|
|
92
|
+
contents.push("\n\n---\n\n");
|
|
93
|
+
}
|
|
79
94
|
contents.push("# AGENTS.md\n\n");
|
|
80
95
|
contents.push(agentsContent);
|
|
81
96
|
} catch {
|
|
@@ -97,13 +112,38 @@ export function registerResources(
|
|
|
97
112
|
}
|
|
98
113
|
}
|
|
99
114
|
|
|
115
|
+
// If no agent files found, return jack guidance as fallback
|
|
100
116
|
if (contents.length === 0) {
|
|
117
|
+
const fallbackGuidance = `# Jack Project
|
|
118
|
+
|
|
119
|
+
This project is managed by jack.
|
|
120
|
+
|
|
121
|
+
## Commands
|
|
122
|
+
- \`jack ship\` - Deploy changes
|
|
123
|
+
- \`jack logs\` - Stream production logs
|
|
124
|
+
- \`jack services\` - Manage databases and other services
|
|
125
|
+
|
|
126
|
+
**Always use jack commands. Never use wrangler directly.**
|
|
127
|
+
|
|
128
|
+
## MCP Tools Available
|
|
129
|
+
|
|
130
|
+
If connected, prefer \`mcp__jack__*\` tools over CLI:
|
|
131
|
+
- \`mcp__jack__deploy_project\` - Deploy changes
|
|
132
|
+
- \`mcp__jack__execute_sql\` - Query databases
|
|
133
|
+
- \`mcp__jack__get_project_status\` - Check status
|
|
134
|
+
|
|
135
|
+
## Documentation
|
|
136
|
+
|
|
137
|
+
Full docs: https://docs.getjack.org/llms-full.txt
|
|
138
|
+
|
|
139
|
+
Check for JACK.md in the project root for project-specific instructions.
|
|
140
|
+
`;
|
|
101
141
|
return {
|
|
102
142
|
contents: [
|
|
103
143
|
{
|
|
104
144
|
uri,
|
|
105
|
-
mimeType: "text/
|
|
106
|
-
text:
|
|
145
|
+
mimeType: "text/markdown",
|
|
146
|
+
text: fallbackGuidance,
|
|
107
147
|
},
|
|
108
148
|
],
|
|
109
149
|
};
|
package/src/templates/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type { Template } from "./types";
|
|
|
10
10
|
// Resolve templates directory relative to this file (src/templates -> templates)
|
|
11
11
|
const TEMPLATES_DIR = join(dirname(dirname(import.meta.dir)), "templates");
|
|
12
12
|
|
|
13
|
-
export const BUILTIN_TEMPLATES = ["hello", "miniapp", "api", "nextjs", "saas"];
|
|
13
|
+
export const BUILTIN_TEMPLATES = ["hello", "miniapp", "api", "nextjs", "saas", "simple-api-starter", "ai-chat", "semantic-search"];
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Resolved template with origin tracking for lineage
|
|
@@ -104,8 +104,57 @@ async function loadTemplate(name: string): Promise<Template> {
|
|
|
104
104
|
// Internal files that should be excluded from templates
|
|
105
105
|
const INTERNAL_FILES = [".jack.json", ".jack/template.json"];
|
|
106
106
|
|
|
107
|
+
// Wrangler config files that need sanitization when forking (JSONC only, no TOML support)
|
|
108
|
+
const WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json"];
|
|
109
|
+
|
|
107
110
|
/**
|
|
108
|
-
*
|
|
111
|
+
* Strip provider-specific IDs from wrangler config bindings.
|
|
112
|
+
* When forking a template, the original author's resource IDs won't work
|
|
113
|
+
* for the new user - wrangler 4.45.0+ will auto-provision new resources.
|
|
114
|
+
*
|
|
115
|
+
* Stripped fields:
|
|
116
|
+
* - D1: database_id (author's database)
|
|
117
|
+
* - KV: id (author's namespace)
|
|
118
|
+
* - R2: nothing (bucket_name is just a name, not a provider ID)
|
|
119
|
+
*/
|
|
120
|
+
function sanitizeWranglerConfig(content: string, filename: string): string {
|
|
121
|
+
// Only handle JSON/JSONC files
|
|
122
|
+
if (!filename.endsWith(".json") && !filename.endsWith(".jsonc")) {
|
|
123
|
+
return content;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const config = parseJsonc(content);
|
|
128
|
+
|
|
129
|
+
// D1: strip database_id
|
|
130
|
+
if (Array.isArray(config.d1_databases)) {
|
|
131
|
+
for (const db of config.d1_databases) {
|
|
132
|
+
if (db && typeof db === "object" && "database_id" in db) {
|
|
133
|
+
delete db.database_id;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// KV: strip id
|
|
139
|
+
if (Array.isArray(config.kv_namespaces)) {
|
|
140
|
+
for (const kv of config.kv_namespaces) {
|
|
141
|
+
if (kv && typeof kv === "object" && "id" in kv) {
|
|
142
|
+
delete kv.id;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Re-serialize with formatting
|
|
148
|
+
return JSON.stringify(config, null, "\t");
|
|
149
|
+
} catch {
|
|
150
|
+
// If parsing fails, return original content
|
|
151
|
+
return content;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Extract zip buffer to file map, excluding internal files.
|
|
157
|
+
* Sanitizes wrangler config to remove provider IDs (D1 database_id, KV id).
|
|
109
158
|
*/
|
|
110
159
|
function extractZipToFiles(zipData: ArrayBuffer): Record<string, string> {
|
|
111
160
|
const files: Record<string, string> = {};
|
|
@@ -117,7 +166,17 @@ function extractZipToFiles(zipData: ArrayBuffer): Record<string, string> {
|
|
|
117
166
|
|
|
118
167
|
// Skip internal files
|
|
119
168
|
if (path && !INTERNAL_FILES.includes(path)) {
|
|
120
|
-
|
|
169
|
+
let fileContent = new TextDecoder().decode(content);
|
|
170
|
+
|
|
171
|
+
// Sanitize wrangler config files to strip provider IDs
|
|
172
|
+
// This ensures forked projects create new resources instead of
|
|
173
|
+
// trying to use the original author's resources
|
|
174
|
+
const filename = path.split("/").pop() || path;
|
|
175
|
+
if (filename === "wrangler.jsonc" || filename === "wrangler.json") {
|
|
176
|
+
fileContent = sanitizeWranglerConfig(fileContent, filename);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
files[path] = fileContent;
|
|
121
180
|
}
|
|
122
181
|
}
|
|
123
182
|
|
|
@@ -288,6 +347,7 @@ export function renderTemplate(template: Template, vars: { name: string }): Temp
|
|
|
288
347
|
rendered[path] = content
|
|
289
348
|
.replace(/jack-template-db/g, `${vars.name}-db`)
|
|
290
349
|
.replace(/jack-template-cache/g, `${vars.name}-cache`)
|
|
350
|
+
.replace(/jack-template-vectors/g, `${vars.name}-vectors`)
|
|
291
351
|
.replace(/jack-template/g, vars.name);
|
|
292
352
|
}
|
|
293
353
|
return { ...template, files: rendered };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ai-chat",
|
|
3
|
+
"description": "AI chat with streaming UI (free Cloudflare AI)",
|
|
4
|
+
"secrets": [],
|
|
5
|
+
"capabilities": ["ai"],
|
|
6
|
+
"intent": {
|
|
7
|
+
"keywords": ["ai", "chat", "llm", "mistral", "completion", "chatbot"],
|
|
8
|
+
"examples": ["AI chatbot", "chat interface", "LLM chat app"]
|
|
9
|
+
},
|
|
10
|
+
"hooks": {
|
|
11
|
+
"postDeploy": [
|
|
12
|
+
{
|
|
13
|
+
"action": "clipboard",
|
|
14
|
+
"text": "{{url}}",
|
|
15
|
+
"message": "URL copied to clipboard"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"action": "box",
|
|
19
|
+
"title": "AI Chat: {{name}}",
|
|
20
|
+
"lines": [
|
|
21
|
+
"{{url}}",
|
|
22
|
+
"",
|
|
23
|
+
"Open in browser to start chatting!",
|
|
24
|
+
"Rate limit: 10 requests/minute"
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"configVersion": 1,
|
|
4
|
+
"workspaces": {
|
|
5
|
+
"": {
|
|
6
|
+
"name": "jack-template",
|
|
7
|
+
"devDependencies": {
|
|
8
|
+
"@cloudflare/workers-types": "^4.20241205.0",
|
|
9
|
+
"typescript": "^5.0.0",
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
"packages": {
|
|
14
|
+
"@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260120.0", "", {}, "sha512-B8pueG+a5S+mdK3z8oKu1ShcxloZ7qWb68IEyLLaepvdryIbNC7JVPcY0bWsjS56UQVKc5fnyRge3yZIwc9bxw=="],
|
|
15
|
+
|
|
16
|
+
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jack-template",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "wrangler dev",
|
|
8
|
+
"deploy": "wrangler deploy"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@cloudflare/workers-types": "^4.20241205.0",
|
|
12
|
+
"typescript": "^5.0.0"
|
|
13
|
+
}
|
|
14
|
+
}
|