@getjack/jack 0.1.5 → 0.1.7
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 +2 -1
- package/src/commands/login.ts +124 -1
- package/src/commands/projects.ts +5 -1
- package/src/commands/publish.ts +50 -0
- package/src/commands/ship.ts +4 -3
- package/src/index.ts +7 -0
- package/src/lib/binding-validator.ts +9 -4
- package/src/lib/build-helper.ts +61 -39
- package/src/lib/config-generator.ts +107 -0
- package/src/lib/control-plane.ts +150 -0
- package/src/lib/managed-deploy.ts +10 -2
- package/src/lib/project-detection.ts +412 -0
- package/src/lib/project-link.test.ts +4 -5
- package/src/lib/project-link.ts +5 -3
- package/src/lib/project-list.ts +5 -1
- package/src/lib/project-operations.ts +318 -21
- package/src/lib/project-resolver.ts +5 -1
- package/src/lib/storage/file-filter.ts +5 -0
- package/src/lib/telemetry.ts +24 -4
- package/src/lib/zip-packager.ts +8 -0
- package/src/templates/index.ts +137 -7
- package/templates/nextjs/.jack.json +26 -26
- package/templates/nextjs/app/globals.css +4 -4
- package/templates/nextjs/app/layout.tsx +11 -11
- package/templates/nextjs/app/page.tsx +8 -6
- package/templates/nextjs/cloudflare-env.d.ts +1 -1
- package/templates/nextjs/next.config.ts +1 -1
- package/templates/nextjs/open-next.config.ts +1 -1
- package/templates/nextjs/package.json +22 -22
- package/templates/nextjs/tsconfig.json +26 -42
- package/templates/nextjs/wrangler.jsonc +15 -15
- package/src/lib/github.ts +0 -151
- package/templates/nextjs/app/isr-test/page.tsx +0 -22
package/src/templates/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { readFile, readdir } from "node:fs/promises";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
+
import { unzipSync } from "fflate";
|
|
5
|
+
import { getControlApiUrl } from "../lib/control-plane.ts";
|
|
4
6
|
import { parseJsonc } from "../lib/jsonc.ts";
|
|
5
7
|
import type { TemplateMetadata as TemplateOrigin } from "../lib/project-link.ts";
|
|
6
8
|
import type { Template } from "./types";
|
|
@@ -97,8 +99,118 @@ async function loadTemplate(name: string): Promise<Template> {
|
|
|
97
99
|
};
|
|
98
100
|
}
|
|
99
101
|
|
|
102
|
+
// Internal files that should be excluded from templates
|
|
103
|
+
const INTERNAL_FILES = [".jack.json", ".jack/template.json"];
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Extract zip buffer to file map, excluding internal files
|
|
107
|
+
*/
|
|
108
|
+
function extractZipToFiles(zipData: ArrayBuffer): Record<string, string> {
|
|
109
|
+
const files: Record<string, string> = {};
|
|
110
|
+
const unzipped = unzipSync(new Uint8Array(zipData));
|
|
111
|
+
|
|
112
|
+
for (const [path, content] of Object.entries(unzipped)) {
|
|
113
|
+
// Skip directories (they have zero-length content or end with /)
|
|
114
|
+
if (content.length === 0 || path.endsWith("/")) continue;
|
|
115
|
+
|
|
116
|
+
// Skip internal files
|
|
117
|
+
if (path && !INTERNAL_FILES.includes(path)) {
|
|
118
|
+
files[path] = new TextDecoder().decode(content);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return files;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Read metadata from extracted files (before they're filtered)
|
|
127
|
+
*/
|
|
128
|
+
function extractMetadataFromZip(zipData: ArrayBuffer): Record<string, unknown> {
|
|
129
|
+
const unzipped = unzipSync(new Uint8Array(zipData));
|
|
130
|
+
|
|
131
|
+
for (const [path, content] of Object.entries(unzipped)) {
|
|
132
|
+
// Skip directories
|
|
133
|
+
if (content.length === 0 || path.endsWith("/")) continue;
|
|
134
|
+
|
|
135
|
+
if (path === ".jack.json") {
|
|
136
|
+
return parseJsonc(new TextDecoder().decode(content));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Fetch a published template from jack cloud (public endpoint, no auth)
|
|
145
|
+
*/
|
|
146
|
+
async function fetchPublishedTemplate(username: string, slug: string): Promise<Template> {
|
|
147
|
+
const response = await fetch(
|
|
148
|
+
`${getControlApiUrl()}/v1/projects/${encodeURIComponent(username)}/${encodeURIComponent(slug)}/source`,
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
if (!response.ok) {
|
|
152
|
+
if (response.status === 404) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`Template not found: ${username}/${slug}\n\nMake sure the project exists and is published with: jack publish`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
throw new Error(`Failed to fetch template: ${response.status}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const zipData = await response.arrayBuffer();
|
|
161
|
+
const metadata = extractMetadataFromZip(zipData);
|
|
162
|
+
const files = extractZipToFiles(zipData);
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
description: (metadata.description as string) || `Fork of ${username}/${slug}`,
|
|
166
|
+
secrets: (metadata.secrets as string[]) || [],
|
|
167
|
+
optionalSecrets: metadata.optionalSecrets as Template["optionalSecrets"],
|
|
168
|
+
capabilities: metadata.capabilities as Template["capabilities"],
|
|
169
|
+
requires: metadata.requires as Template["requires"],
|
|
170
|
+
hooks: metadata.hooks as Template["hooks"],
|
|
171
|
+
agentContext: metadata.agentContext as Template["agentContext"],
|
|
172
|
+
intent: metadata.intent as Template["intent"],
|
|
173
|
+
files,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Fetch user's own project as a template (authenticated)
|
|
179
|
+
*/
|
|
180
|
+
async function fetchUserTemplate(slug: string): Promise<Template | null> {
|
|
181
|
+
const { authFetch } = await import("../lib/auth/index.ts");
|
|
182
|
+
|
|
183
|
+
const response = await authFetch(
|
|
184
|
+
`${getControlApiUrl()}/v1/me/projects/${encodeURIComponent(slug)}/source`,
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
if (response.status === 404) {
|
|
188
|
+
return null; // Not found, will try other sources
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!response.ok) {
|
|
192
|
+
throw new Error(`Failed to fetch your project: ${response.status}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const zipData = await response.arrayBuffer();
|
|
196
|
+
const metadata = extractMetadataFromZip(zipData);
|
|
197
|
+
const files = extractZipToFiles(zipData);
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
description: (metadata.description as string) || `Your project: ${slug}`,
|
|
201
|
+
secrets: (metadata.secrets as string[]) || [],
|
|
202
|
+
optionalSecrets: metadata.optionalSecrets as Template["optionalSecrets"],
|
|
203
|
+
capabilities: metadata.capabilities as Template["capabilities"],
|
|
204
|
+
requires: metadata.requires as Template["requires"],
|
|
205
|
+
hooks: metadata.hooks as Template["hooks"],
|
|
206
|
+
agentContext: metadata.agentContext as Template["agentContext"],
|
|
207
|
+
intent: metadata.intent as Template["intent"],
|
|
208
|
+
files,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
100
212
|
/**
|
|
101
|
-
* Resolve template by name
|
|
213
|
+
* Resolve template by name
|
|
102
214
|
*/
|
|
103
215
|
export async function resolveTemplate(template?: string): Promise<Template> {
|
|
104
216
|
// No template → hello (omakase default)
|
|
@@ -111,14 +223,26 @@ export async function resolveTemplate(template?: string): Promise<Template> {
|
|
|
111
223
|
return loadTemplate(template);
|
|
112
224
|
}
|
|
113
225
|
|
|
114
|
-
//
|
|
226
|
+
// username/slug format - fetch from jack cloud
|
|
115
227
|
if (template.includes("/")) {
|
|
116
|
-
const
|
|
117
|
-
return
|
|
228
|
+
const [username, slug] = template.split("/", 2);
|
|
229
|
+
return fetchPublishedTemplate(username, slug);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Try as user's own project first
|
|
233
|
+
try {
|
|
234
|
+
const userTemplate = await fetchUserTemplate(template);
|
|
235
|
+
if (userTemplate) {
|
|
236
|
+
return userTemplate;
|
|
237
|
+
}
|
|
238
|
+
} catch (_err) {
|
|
239
|
+
// If auth fails or project not found, fall through to error
|
|
118
240
|
}
|
|
119
241
|
|
|
120
242
|
// Unknown template
|
|
121
|
-
throw new Error(
|
|
243
|
+
throw new Error(
|
|
244
|
+
`Unknown template: ${template}\n\nAvailable built-in templates: ${BUILTIN_TEMPLATES.join(", ")}\nOr use username/slug format for published projects`,
|
|
245
|
+
);
|
|
122
246
|
}
|
|
123
247
|
|
|
124
248
|
/**
|
|
@@ -131,9 +255,15 @@ export async function resolveTemplateWithOrigin(
|
|
|
131
255
|
const templateName = templateOption || "hello";
|
|
132
256
|
|
|
133
257
|
// Determine origin type
|
|
134
|
-
|
|
258
|
+
let originType: "builtin" | "user" | "published" = "builtin";
|
|
259
|
+
if (templateOption?.includes("/")) {
|
|
260
|
+
originType = "published";
|
|
261
|
+
} else if (templateOption && !BUILTIN_TEMPLATES.includes(templateOption)) {
|
|
262
|
+
originType = "user";
|
|
263
|
+
}
|
|
264
|
+
|
|
135
265
|
const origin: TemplateOrigin = {
|
|
136
|
-
type:
|
|
266
|
+
type: originType,
|
|
137
267
|
name: templateName,
|
|
138
268
|
};
|
|
139
269
|
|
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
2
|
+
"name": "nextjs",
|
|
3
|
+
"description": "Next.js App (SSR on Cloudflare)",
|
|
4
|
+
"secrets": [],
|
|
5
|
+
"capabilities": [],
|
|
6
|
+
"intent": {
|
|
7
|
+
"keywords": ["next", "nextjs", "react", "ssr", "full-stack", "server components", "rsc"],
|
|
8
|
+
"examples": ["next.js app", "server-rendered react", "react ssr"]
|
|
9
|
+
},
|
|
10
|
+
"agentContext": {
|
|
11
|
+
"summary": "A Next.js app deployed with jack. Supports SSR, SSG, and React Server Components.",
|
|
12
|
+
"full_text": "## Project Structure\n\n- `app/` - Next.js App Router pages and layouts\n- `public/` - Static assets\n- `open-next.config.ts` - OpenNext configuration\n- `wrangler.jsonc` - Worker configuration\n\n## Commands\n\n- `bun run dev` - Local development\n- `jack ship` - Deploy to production\n- `bun run preview` - Preview production build locally\n\n## Conventions\n\n- Uses App Router (not Pages Router)\n- SSR and SSG work out of the box\n- Server Components supported\n\n## Resources\n\n- [OpenNext Docs](https://opennext.js.org/cloudflare)\n- [Next.js App Router](https://nextjs.org/docs/app)"
|
|
13
|
+
},
|
|
14
|
+
"hooks": {
|
|
15
|
+
"postDeploy": [
|
|
16
|
+
{
|
|
17
|
+
"action": "clipboard",
|
|
18
|
+
"text": "{{url}}",
|
|
19
|
+
"message": "Deploy URL copied to clipboard"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"action": "box",
|
|
23
|
+
"title": "Deployed: {{name}}",
|
|
24
|
+
"lines": ["URL: {{url}}", "", "Next.js app is live!"]
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
28
|
}
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import type { Metadata } from
|
|
2
|
-
import
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import "./globals.css";
|
|
3
3
|
|
|
4
4
|
export const metadata: Metadata = {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
title: "jack-template",
|
|
6
|
+
description: "Next.js app built with jack",
|
|
7
7
|
};
|
|
8
8
|
|
|
9
9
|
export default function RootLayout({
|
|
10
|
-
|
|
10
|
+
children,
|
|
11
11
|
}: {
|
|
12
|
-
|
|
12
|
+
children: React.ReactNode;
|
|
13
13
|
}) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
return (
|
|
15
|
+
<html lang="en">
|
|
16
|
+
<body>{children}</body>
|
|
17
|
+
</html>
|
|
18
|
+
);
|
|
19
19
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export default function Home() {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
return (
|
|
3
|
+
<main style={{ padding: "2rem", fontFamily: "system-ui" }}>
|
|
4
|
+
<h1>jack-template</h1>
|
|
5
|
+
<p>
|
|
6
|
+
Ship it with <code>jack ship</code>
|
|
7
|
+
</p>
|
|
8
|
+
</main>
|
|
9
|
+
);
|
|
8
10
|
}
|
|
@@ -2,5 +2,5 @@ import { defineCloudflareConfig } from "@opennextjs/cloudflare";
|
|
|
2
2
|
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";
|
|
3
3
|
|
|
4
4
|
export default defineCloudflareConfig({
|
|
5
|
-
|
|
5
|
+
incrementalCache: r2IncrementalCache,
|
|
6
6
|
});
|
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
2
|
+
"name": "jack-template",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "next dev",
|
|
7
|
+
"build": "next build",
|
|
8
|
+
"preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
|
|
9
|
+
"deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
|
|
10
|
+
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@opennextjs/cloudflare": "^1.0.0",
|
|
14
|
+
"next": "^15.0.0",
|
|
15
|
+
"react": "^19.0.0",
|
|
16
|
+
"react-dom": "^19.0.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@cloudflare/workers-types": "^4.20241205.0",
|
|
20
|
+
"@types/react": "^19.0.0",
|
|
21
|
+
"@types/react-dom": "^19.0.0",
|
|
22
|
+
"typescript": "^5.6.0"
|
|
23
|
+
}
|
|
24
24
|
}
|
|
@@ -1,44 +1,28 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
],
|
|
29
|
-
"types": [
|
|
30
|
-
"@cloudflare/workers-types"
|
|
31
|
-
],
|
|
32
|
-
"allowJs": true
|
|
33
|
-
},
|
|
34
|
-
"include": [
|
|
35
|
-
"next-env.d.ts",
|
|
36
|
-
"**/*.ts",
|
|
37
|
-
"**/*.tsx",
|
|
38
|
-
".next/types/**/*.ts",
|
|
39
|
-
"cloudflare-env.d.ts"
|
|
40
|
-
],
|
|
41
|
-
"exclude": [
|
|
42
|
-
"node_modules"
|
|
43
|
-
]
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"jsx": "preserve",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"incremental": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"isolatedModules": true,
|
|
15
|
+
"paths": {
|
|
16
|
+
"@/*": ["./*"]
|
|
17
|
+
},
|
|
18
|
+
"plugins": [
|
|
19
|
+
{
|
|
20
|
+
"name": "next"
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"types": ["@cloudflare/workers-types"],
|
|
24
|
+
"allowJs": true
|
|
25
|
+
},
|
|
26
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "cloudflare-env.d.ts"],
|
|
27
|
+
"exclude": ["node_modules"]
|
|
44
28
|
}
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
2
|
+
"$schema": "node_modules/wrangler/config-schema.json",
|
|
3
|
+
"name": "jack-template",
|
|
4
|
+
"main": ".open-next/worker.js",
|
|
5
|
+
"compatibility_date": "2024-12-30",
|
|
6
|
+
"compatibility_flags": ["nodejs_compat"],
|
|
7
|
+
"assets": {
|
|
8
|
+
"directory": ".open-next/assets",
|
|
9
|
+
"binding": "ASSETS"
|
|
10
|
+
},
|
|
11
|
+
"r2_buckets": [
|
|
12
|
+
{
|
|
13
|
+
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
|
14
|
+
"bucket_name": "jack-template-cache"
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
17
|
}
|
package/src/lib/github.ts
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import { mkdtemp, readFile, readdir, rm } from "node:fs/promises";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { $ } from "bun";
|
|
5
|
-
import type { AgentContext, Capability, Template, TemplateHooks } from "../templates/types";
|
|
6
|
-
import { parseJsonc } from "./jsonc.ts";
|
|
7
|
-
import type { ServiceTypeKey } from "./services/index.ts";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Parse GitHub input: "user/repo" or "https://github.com/user/repo"
|
|
11
|
-
*/
|
|
12
|
-
function parseGitHubInput(input: string): { owner: string; repo: string } {
|
|
13
|
-
// Full URL: https://github.com/user/repo
|
|
14
|
-
const urlMatch = input.match(/github\.com\/([^\/]+)\/([^\/\s]+)/);
|
|
15
|
-
if (urlMatch?.[1] && urlMatch[2]) {
|
|
16
|
-
return { owner: urlMatch[1], repo: urlMatch[2].replace(/\.git$/, "") };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Shorthand: user/repo
|
|
20
|
-
const shortMatch = input.match(/^([^\/]+)\/([^\/]+)$/);
|
|
21
|
-
if (shortMatch?.[1] && shortMatch[2]) {
|
|
22
|
-
return { owner: shortMatch[1], repo: shortMatch[2] };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
throw new Error(
|
|
26
|
-
`Invalid GitHub URL: ${input}\n\nExpected: user/repo or https://github.com/user/repo`,
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Recursively read all files in a directory
|
|
32
|
-
*/
|
|
33
|
-
async function readDirRecursive(dir: string, base = ""): Promise<Record<string, string>> {
|
|
34
|
-
const files: Record<string, string> = {};
|
|
35
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
36
|
-
|
|
37
|
-
for (const entry of entries) {
|
|
38
|
-
const relativePath = base ? `${base}/${entry.name}` : entry.name;
|
|
39
|
-
const fullPath = join(dir, entry.name);
|
|
40
|
-
|
|
41
|
-
if (entry.isDirectory()) {
|
|
42
|
-
// Skip common non-source directories
|
|
43
|
-
if (["node_modules", ".git", ".wrangler"].includes(entry.name)) continue;
|
|
44
|
-
Object.assign(files, await readDirRecursive(fullPath, relativePath));
|
|
45
|
-
} else {
|
|
46
|
-
// Read file content
|
|
47
|
-
const content = await readFile(fullPath, "utf-8");
|
|
48
|
-
files[relativePath] = content;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return files;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Fetch template from GitHub tarball API
|
|
57
|
-
*/
|
|
58
|
-
export async function fetchFromGitHub(input: string): Promise<Template> {
|
|
59
|
-
const { owner, repo } = parseGitHubInput(input);
|
|
60
|
-
const tarballUrl = `https://api.github.com/repos/${owner}/${repo}/tarball`;
|
|
61
|
-
|
|
62
|
-
// Fetch tarball
|
|
63
|
-
const headers: Record<string, string> = {
|
|
64
|
-
"User-Agent": "jack-cli",
|
|
65
|
-
};
|
|
66
|
-
if (process.env.GITHUB_TOKEN) {
|
|
67
|
-
headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const response = await fetch(tarballUrl, { headers, redirect: "follow" });
|
|
71
|
-
|
|
72
|
-
if (!response.ok) {
|
|
73
|
-
if (response.status === 404) {
|
|
74
|
-
throw new Error(
|
|
75
|
-
`Repository not found: ${owner}/${repo}\n\nMake sure it exists and is public.`,
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
if (response.status === 403) {
|
|
79
|
-
throw new Error(
|
|
80
|
-
"GitHub rate limit exceeded.\n\nSet GITHUB_TOKEN to continue:\n export GITHUB_TOKEN=ghp_xxxxx\n\nGet a token at: https://github.com/settings/tokens",
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
throw new Error(`Failed to fetch template: ${response.statusText}`);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Create temp directory
|
|
87
|
-
const tempDir = await mkdtemp(join(tmpdir(), "jack-template-"));
|
|
88
|
-
const tarPath = join(tempDir, "template.tar.gz");
|
|
89
|
-
|
|
90
|
-
try {
|
|
91
|
-
// Write tarball to temp file
|
|
92
|
-
const buffer = await response.arrayBuffer();
|
|
93
|
-
await Bun.write(tarPath, buffer);
|
|
94
|
-
|
|
95
|
-
// Extract tarball
|
|
96
|
-
await $`tar -xzf ${tarPath} -C ${tempDir}`.quiet();
|
|
97
|
-
|
|
98
|
-
// Find extracted directory (GitHub tarballs have a prefix like "user-repo-sha")
|
|
99
|
-
const entries = await readdir(tempDir);
|
|
100
|
-
const extractedDir = entries.find((e) => e !== "template.tar.gz");
|
|
101
|
-
if (!extractedDir) {
|
|
102
|
-
throw new Error("Failed to extract template: no directory found");
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Read all files
|
|
106
|
-
const files = await readDirRecursive(join(tempDir, extractedDir));
|
|
107
|
-
|
|
108
|
-
// Warn if it doesn't look like a worker
|
|
109
|
-
const hasWorkerFiles = files["wrangler.toml"] || files["worker.ts"] || files["src/index.ts"];
|
|
110
|
-
if (!hasWorkerFiles) {
|
|
111
|
-
console.warn("\n⚠ This doesn't look like a Cloudflare Worker");
|
|
112
|
-
console.warn(" (no wrangler.toml or worker entry point found)\n");
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Read .jack.json metadata if it exists
|
|
116
|
-
const jackJsonContent = files[".jack.json"];
|
|
117
|
-
if (jackJsonContent) {
|
|
118
|
-
try {
|
|
119
|
-
const metadata = parseJsonc(jackJsonContent) as {
|
|
120
|
-
description?: string;
|
|
121
|
-
secrets?: string[];
|
|
122
|
-
capabilities?: Capability[];
|
|
123
|
-
requires?: ServiceTypeKey[];
|
|
124
|
-
hooks?: TemplateHooks;
|
|
125
|
-
agentContext?: AgentContext;
|
|
126
|
-
};
|
|
127
|
-
// Remove .jack.json from files (not needed in project)
|
|
128
|
-
const { ".jack.json": _, ...filesWithoutJackJson } = files;
|
|
129
|
-
return {
|
|
130
|
-
description: metadata.description || `GitHub: ${owner}/${repo}`,
|
|
131
|
-
secrets: metadata.secrets,
|
|
132
|
-
capabilities: metadata.capabilities,
|
|
133
|
-
requires: metadata.requires,
|
|
134
|
-
hooks: metadata.hooks,
|
|
135
|
-
agentContext: metadata.agentContext,
|
|
136
|
-
files: filesWithoutJackJson,
|
|
137
|
-
};
|
|
138
|
-
} catch {
|
|
139
|
-
// Invalid JSON, fall through to default
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return {
|
|
144
|
-
description: `GitHub: ${owner}/${repo}`,
|
|
145
|
-
files,
|
|
146
|
-
};
|
|
147
|
-
} finally {
|
|
148
|
-
// Cleanup
|
|
149
|
-
await rm(tempDir, { recursive: true, force: true });
|
|
150
|
-
}
|
|
151
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
// ISR Test Page - regenerates every 10 seconds
|
|
2
|
-
// After first request, page is cached in R2
|
|
3
|
-
// Subsequent requests serve cached version while revalidating in background
|
|
4
|
-
|
|
5
|
-
export const revalidate = 10; // seconds
|
|
6
|
-
|
|
7
|
-
export default async function ISRTestPage() {
|
|
8
|
-
const timestamp = new Date().toISOString();
|
|
9
|
-
|
|
10
|
-
return (
|
|
11
|
-
<main style={{ padding: '2rem', fontFamily: 'system-ui' }}>
|
|
12
|
-
<h1>ISR Test</h1>
|
|
13
|
-
<p>Generated at: <strong>{timestamp}</strong></p>
|
|
14
|
-
<p style={{ color: '#888', marginTop: '1rem' }}>
|
|
15
|
-
Refresh the page - timestamp updates every ~10 seconds (cached in R2)
|
|
16
|
-
</p>
|
|
17
|
-
<p style={{ marginTop: '2rem' }}>
|
|
18
|
-
<a href="/" style={{ color: '#0070f3' }}>← Back home</a>
|
|
19
|
-
</p>
|
|
20
|
-
</main>
|
|
21
|
-
);
|
|
22
|
-
}
|