@getjack/jack 0.1.3 → 0.1.5
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 +103 -0
- package/package.json +2 -6
- package/src/commands/agents.ts +9 -24
- package/src/commands/clone.ts +27 -0
- package/src/commands/down.ts +31 -57
- package/src/commands/feedback.ts +4 -5
- package/src/commands/link.ts +147 -0
- package/src/commands/logs.ts +8 -18
- package/src/commands/new.ts +7 -1
- package/src/commands/projects.ts +162 -105
- package/src/commands/secrets.ts +7 -6
- package/src/commands/services.ts +5 -4
- package/src/commands/tag.ts +282 -0
- package/src/commands/unlink.ts +30 -0
- package/src/index.ts +46 -1
- package/src/lib/auth/index.ts +2 -0
- package/src/lib/auth/store.ts +26 -2
- package/src/lib/binding-validator.ts +4 -13
- package/src/lib/build-helper.ts +93 -5
- package/src/lib/control-plane.ts +48 -0
- package/src/lib/deploy-mode.ts +1 -1
- package/src/lib/managed-deploy.ts +11 -1
- package/src/lib/managed-down.ts +7 -20
- package/src/lib/paths-index.test.ts +546 -0
- package/src/lib/paths-index.ts +310 -0
- package/src/lib/project-link.test.ts +459 -0
- package/src/lib/project-link.ts +279 -0
- package/src/lib/project-list.test.ts +581 -0
- package/src/lib/project-list.ts +445 -0
- package/src/lib/project-operations.ts +304 -183
- package/src/lib/project-resolver.ts +191 -211
- package/src/lib/tags.ts +389 -0
- package/src/lib/telemetry.ts +81 -168
- package/src/lib/zip-packager.ts +9 -0
- package/src/templates/index.ts +5 -3
- package/templates/api/.jack/template.json +4 -0
- package/templates/hello/.jack/template.json +4 -0
- package/templates/miniapp/.jack/template.json +4 -0
- package/templates/nextjs/.jack.json +28 -0
- package/templates/nextjs/app/globals.css +9 -0
- package/templates/nextjs/app/isr-test/page.tsx +22 -0
- package/templates/nextjs/app/layout.tsx +19 -0
- package/templates/nextjs/app/page.tsx +8 -0
- package/templates/nextjs/bun.lock +2232 -0
- package/templates/nextjs/cloudflare-env.d.ts +3 -0
- package/templates/nextjs/next-env.d.ts +6 -0
- package/templates/nextjs/next.config.ts +8 -0
- package/templates/nextjs/open-next.config.ts +6 -0
- package/templates/nextjs/package.json +24 -0
- package/templates/nextjs/public/_headers +2 -0
- package/templates/nextjs/tsconfig.json +44 -0
- package/templates/nextjs/wrangler.jsonc +17 -0
- package/src/lib/local-paths.test.ts +0 -902
- package/src/lib/local-paths.ts +0 -258
- package/src/lib/registry.ts +0 -181
package/src/lib/telemetry.ts
CHANGED
|
@@ -1,30 +1,28 @@
|
|
|
1
|
-
import { PostHog } from "posthog-node";
|
|
2
1
|
import type { TelemetryConfig } from "./telemetry-config.ts";
|
|
3
2
|
import { getTelemetryConfig, setTelemetryEnabled } from "./telemetry-config.ts";
|
|
4
3
|
|
|
4
|
+
// Telemetry proxy endpoint (keeps PostHog API key secret)
|
|
5
|
+
// Override with TELEMETRY_PROXY_URL for local testing
|
|
6
|
+
const TELEMETRY_PROXY = process.env.TELEMETRY_PROXY_URL || "https://telemetry.getjack.org";
|
|
7
|
+
|
|
8
|
+
// Session ID - unique per CLI invocation, groups related events
|
|
9
|
+
const SESSION_ID = crypto.randomUUID();
|
|
10
|
+
|
|
5
11
|
// ============================================
|
|
6
|
-
// EVENT REGISTRY
|
|
7
|
-
// Add new events here, they become type-safe
|
|
12
|
+
// EVENT REGISTRY
|
|
8
13
|
// ============================================
|
|
9
14
|
export const Events = {
|
|
10
|
-
// Automatic (via wrapper)
|
|
11
15
|
COMMAND_INVOKED: "command_invoked",
|
|
12
16
|
COMMAND_COMPLETED: "command_completed",
|
|
13
17
|
COMMAND_FAILED: "command_failed",
|
|
14
|
-
|
|
15
|
-
// Business events (for future use)
|
|
16
18
|
PROJECT_CREATED: "project_created",
|
|
17
19
|
DEPLOY_STARTED: "deploy_started",
|
|
18
20
|
CONFIG_CHANGED: "config_changed",
|
|
19
|
-
|
|
20
|
-
// Intent-driven creation events
|
|
21
21
|
INTENT_MATCHED: "intent_matched",
|
|
22
22
|
INTENT_NO_MATCH: "intent_no_match",
|
|
23
23
|
INTENT_CUSTOMIZATION_STARTED: "intent_customization_started",
|
|
24
24
|
INTENT_CUSTOMIZATION_COMPLETED: "intent_customization_completed",
|
|
25
25
|
INTENT_CUSTOMIZATION_FAILED: "intent_customization_failed",
|
|
26
|
-
|
|
27
|
-
// Deploy mode events
|
|
28
26
|
DEPLOY_MODE_SELECTED: "deploy_mode_selected",
|
|
29
27
|
MANAGED_PROJECT_CREATED: "managed_project_created",
|
|
30
28
|
MANAGED_DEPLOY_STARTED: "managed_deploy_started",
|
|
@@ -34,72 +32,59 @@ export const Events = {
|
|
|
34
32
|
|
|
35
33
|
type EventName = (typeof Events)[keyof typeof Events];
|
|
36
34
|
|
|
37
|
-
// Re-export config functions for convenience
|
|
38
35
|
export { getTelemetryConfig, setTelemetryEnabled };
|
|
39
36
|
|
|
40
37
|
// ============================================
|
|
41
|
-
//
|
|
38
|
+
// STATE
|
|
42
39
|
// ============================================
|
|
43
|
-
let client: PostHog | null = null;
|
|
44
40
|
let telemetryConfig: TelemetryConfig | null = null;
|
|
41
|
+
let enabledCache: boolean | null = null;
|
|
42
|
+
let userProps: Partial<UserProperties> = {};
|
|
43
|
+
|
|
44
|
+
// ============================================
|
|
45
|
+
// FIRE-AND-FORGET SEND (detached subprocess)
|
|
46
|
+
// ============================================
|
|
47
|
+
function send(url: string, data: object): void {
|
|
48
|
+
const payload = Buffer.from(JSON.stringify(data)).toString("base64");
|
|
49
|
+
|
|
50
|
+
// Spawn detached process that sends HTTP and exits
|
|
51
|
+
// Parent doesn't wait - child outlives parent
|
|
52
|
+
const proc = Bun.spawn(
|
|
53
|
+
[
|
|
54
|
+
"bun",
|
|
55
|
+
"-e",
|
|
56
|
+
`await fetch("${url}",{method:"POST",headers:{"Content-Type":"application/json"},body:Buffer.from("${payload}","base64").toString()}).catch(()=>{})`,
|
|
57
|
+
],
|
|
58
|
+
{
|
|
59
|
+
detached: true,
|
|
60
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
61
|
+
},
|
|
62
|
+
);
|
|
63
|
+
proc.unref();
|
|
64
|
+
}
|
|
45
65
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
* 1. DO_NOT_TRACK=1 -> disabled
|
|
50
|
-
* 2. CI=true -> disabled
|
|
51
|
-
* 3. JACK_TELEMETRY_DISABLED=1 -> disabled
|
|
52
|
-
* 4. Config file enabled: false -> disabled
|
|
53
|
-
* 5. Default -> enabled
|
|
54
|
-
*/
|
|
66
|
+
// ============================================
|
|
67
|
+
// HELPERS
|
|
68
|
+
// ============================================
|
|
55
69
|
async function isEnabled(): Promise<boolean> {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (process.env.CI === "true")
|
|
59
|
-
|
|
70
|
+
if (enabledCache !== null) return enabledCache;
|
|
71
|
+
|
|
72
|
+
if (process.env.DO_NOT_TRACK === "1" || process.env.CI === "true" || process.env.JACK_TELEMETRY_DISABLED === "1") {
|
|
73
|
+
enabledCache = false;
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
60
76
|
|
|
61
|
-
// Check config file
|
|
62
77
|
try {
|
|
63
78
|
const config = await getTelemetryConfig();
|
|
64
79
|
telemetryConfig = config;
|
|
80
|
+
enabledCache = config.enabled;
|
|
65
81
|
return config.enabled;
|
|
66
82
|
} catch {
|
|
67
|
-
|
|
83
|
+
enabledCache = true;
|
|
68
84
|
return true;
|
|
69
85
|
}
|
|
70
86
|
}
|
|
71
87
|
|
|
72
|
-
/**
|
|
73
|
-
* Get or initialize PostHog client
|
|
74
|
-
* Returns null if telemetry is disabled or API key is missing
|
|
75
|
-
*/
|
|
76
|
-
async function getClient(): Promise<PostHog | null> {
|
|
77
|
-
const enabled = await isEnabled();
|
|
78
|
-
if (!enabled) return null;
|
|
79
|
-
|
|
80
|
-
// Lazy initialization
|
|
81
|
-
if (!client) {
|
|
82
|
-
const apiKey = process.env.POSTHOG_API_KEY;
|
|
83
|
-
if (!apiKey) return null;
|
|
84
|
-
|
|
85
|
-
try {
|
|
86
|
-
client = new PostHog(apiKey, {
|
|
87
|
-
host: "https://us.i.posthog.com",
|
|
88
|
-
flushAt: 1, // Flush immediately (CLI is short-lived)
|
|
89
|
-
flushInterval: 0, // No delay
|
|
90
|
-
});
|
|
91
|
-
} catch {
|
|
92
|
-
// Silent failure - never block execution
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return client;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Get anonymous ID from config
|
|
102
|
-
*/
|
|
103
88
|
async function getAnonymousId(): Promise<string> {
|
|
104
89
|
if (!telemetryConfig) {
|
|
105
90
|
telemetryConfig = await getTelemetryConfig();
|
|
@@ -108,7 +93,7 @@ async function getAnonymousId(): Promise<string> {
|
|
|
108
93
|
}
|
|
109
94
|
|
|
110
95
|
// ============================================
|
|
111
|
-
// USER PROPERTIES
|
|
96
|
+
// USER PROPERTIES
|
|
112
97
|
// ============================================
|
|
113
98
|
export interface UserProperties {
|
|
114
99
|
jack_version: string;
|
|
@@ -116,71 +101,71 @@ export interface UserProperties {
|
|
|
116
101
|
arch: string;
|
|
117
102
|
node_version: string;
|
|
118
103
|
is_ci: boolean;
|
|
104
|
+
shell?: string;
|
|
105
|
+
terminal?: string;
|
|
106
|
+
terminal_width?: number;
|
|
107
|
+
is_tty?: boolean;
|
|
108
|
+
locale?: string;
|
|
119
109
|
config_style?: "byoc" | "jack-cloud";
|
|
120
110
|
}
|
|
121
111
|
|
|
122
|
-
|
|
112
|
+
// Detect environment properties
|
|
113
|
+
export function getEnvironmentProps(): Pick<UserProperties, "shell" | "terminal" | "terminal_width" | "is_tty" | "locale"> {
|
|
114
|
+
return {
|
|
115
|
+
shell: process.env.SHELL?.split("/").pop(), // e.g., /bin/zsh -> zsh
|
|
116
|
+
terminal: process.env.TERM_PROGRAM, // e.g., iTerm.app, vscode, Apple_Terminal
|
|
117
|
+
terminal_width: process.stdout.columns,
|
|
118
|
+
is_tty: process.stdout.isTTY ?? false,
|
|
119
|
+
locale: Intl.DateTimeFormat().resolvedOptions().locale,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
123
122
|
|
|
124
|
-
/**
|
|
125
|
-
* Set user properties (sent with all events)
|
|
126
|
-
* Safe to call multiple times - properties are merged
|
|
127
|
-
*/
|
|
128
123
|
export async function identify(properties: Partial<UserProperties>): Promise<void> {
|
|
129
124
|
userProps = { ...userProps, ...properties };
|
|
130
|
-
|
|
131
|
-
const ph = await getClient();
|
|
132
|
-
if (!ph) return;
|
|
125
|
+
if (!(await isEnabled())) return;
|
|
133
126
|
|
|
134
127
|
try {
|
|
135
128
|
const distinctId = await getAnonymousId();
|
|
136
|
-
|
|
129
|
+
send(`${TELEMETRY_PROXY}/identify`, {
|
|
137
130
|
distinctId,
|
|
138
131
|
properties: userProps,
|
|
132
|
+
setOnce: { first_seen: new Date().toISOString() }, // Only sets on first identify
|
|
139
133
|
});
|
|
140
134
|
} catch {
|
|
141
|
-
// Silent
|
|
135
|
+
// Silent
|
|
142
136
|
}
|
|
143
137
|
}
|
|
144
138
|
|
|
145
139
|
// ============================================
|
|
146
|
-
// TRACK
|
|
140
|
+
// TRACK
|
|
147
141
|
// ============================================
|
|
148
|
-
/**
|
|
149
|
-
* Track an event with optional properties
|
|
150
|
-
* This is fire-and-forget and will never throw or block
|
|
151
|
-
*/
|
|
152
142
|
export async function track(event: EventName, properties?: Record<string, unknown>): Promise<void> {
|
|
153
|
-
|
|
154
|
-
if (!ph) return;
|
|
143
|
+
if (!(await isEnabled())) return;
|
|
155
144
|
|
|
156
145
|
try {
|
|
157
146
|
const distinctId = await getAnonymousId();
|
|
158
|
-
|
|
147
|
+
send(`${TELEMETRY_PROXY}/t`, {
|
|
159
148
|
distinctId,
|
|
160
149
|
event,
|
|
161
150
|
properties: {
|
|
162
151
|
...properties,
|
|
163
152
|
...userProps,
|
|
164
|
-
|
|
153
|
+
$session_id: SESSION_ID, // Groups events from same CLI invocation
|
|
165
154
|
},
|
|
155
|
+
timestamp: Date.now(),
|
|
166
156
|
});
|
|
167
157
|
} catch {
|
|
168
|
-
// Silent
|
|
158
|
+
// Silent
|
|
169
159
|
}
|
|
170
160
|
}
|
|
171
161
|
|
|
172
162
|
// ============================================
|
|
173
|
-
//
|
|
174
|
-
// Commands wrapped with this get automatic tracking
|
|
163
|
+
// WRAPPER
|
|
175
164
|
// ============================================
|
|
176
165
|
export interface TelemetryOptions {
|
|
177
166
|
platform?: "cli" | "mcp";
|
|
178
167
|
}
|
|
179
168
|
|
|
180
|
-
/**
|
|
181
|
-
* Wrap a command function with automatic telemetry tracking
|
|
182
|
-
* Tracks command_invoked, command_completed, and command_failed events
|
|
183
|
-
*/
|
|
184
169
|
// biome-ignore lint/suspicious/noExplicitAny: Required for flexible command wrapping
|
|
185
170
|
export function withTelemetry<T extends (...args: any[]) => Promise<any>>(
|
|
186
171
|
commandName: string,
|
|
@@ -190,109 +175,37 @@ export function withTelemetry<T extends (...args: any[]) => Promise<any>>(
|
|
|
190
175
|
const platform = options?.platform ?? "cli";
|
|
191
176
|
|
|
192
177
|
return (async (...args: Parameters<T>) => {
|
|
193
|
-
// Fire-and-forget: don't await track() to avoid blocking command execution
|
|
194
178
|
track(Events.COMMAND_INVOKED, { command: commandName, platform });
|
|
195
179
|
const start = Date.now();
|
|
196
180
|
|
|
197
181
|
try {
|
|
198
182
|
const result = await fn(...args);
|
|
199
|
-
track(Events.COMMAND_COMPLETED, {
|
|
200
|
-
command: commandName,
|
|
201
|
-
platform,
|
|
202
|
-
duration_ms: Date.now() - start,
|
|
203
|
-
});
|
|
183
|
+
track(Events.COMMAND_COMPLETED, { command: commandName, platform, duration_ms: Date.now() - start });
|
|
204
184
|
return result;
|
|
205
185
|
} catch (error) {
|
|
206
|
-
track(Events.COMMAND_FAILED, {
|
|
207
|
-
command: commandName,
|
|
208
|
-
platform,
|
|
209
|
-
error_type: classifyError(error),
|
|
210
|
-
duration_ms: Date.now() - start,
|
|
211
|
-
});
|
|
186
|
+
track(Events.COMMAND_FAILED, { command: commandName, platform, error_type: classifyError(error), duration_ms: Date.now() - start });
|
|
212
187
|
throw error;
|
|
213
188
|
}
|
|
214
189
|
}) as T;
|
|
215
190
|
}
|
|
216
191
|
|
|
217
192
|
// ============================================
|
|
218
|
-
// SHUTDOWN -
|
|
193
|
+
// SHUTDOWN - No-op (detached processes handle themselves)
|
|
219
194
|
// ============================================
|
|
220
|
-
/**
|
|
221
|
-
* Gracefully shutdown telemetry client
|
|
222
|
-
* Times out after 500ms to never block CLI exit
|
|
223
|
-
*/
|
|
224
195
|
export async function shutdown(): Promise<void> {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
try {
|
|
228
|
-
await Promise.race([
|
|
229
|
-
client.shutdown(),
|
|
230
|
-
new Promise((_, reject) =>
|
|
231
|
-
setTimeout(() => reject(new Error("Telemetry shutdown timeout")), 500),
|
|
232
|
-
),
|
|
233
|
-
]);
|
|
234
|
-
} catch {
|
|
235
|
-
// Silent failure - never block exit
|
|
236
|
-
}
|
|
196
|
+
// No-op - detached subprocesses send telemetry independently
|
|
237
197
|
}
|
|
238
198
|
|
|
239
199
|
// ============================================
|
|
240
200
|
// ERROR CLASSIFICATION
|
|
241
201
|
// ============================================
|
|
242
|
-
/**
|
|
243
|
-
* Classify error into broad categories for analytics
|
|
244
|
-
* Returns: 'validation' | 'network' | 'build' | 'deploy' | 'config' | 'unknown'
|
|
245
|
-
*/
|
|
246
202
|
function classifyError(error: unknown): string {
|
|
247
|
-
const
|
|
248
|
-
const errorStr = String(error).toLowerCase();
|
|
249
|
-
const combined = `${msg} ${errorStr}`.toLowerCase();
|
|
250
|
-
|
|
251
|
-
// Check for validation errors
|
|
252
|
-
if (
|
|
253
|
-
combined.includes("validation") ||
|
|
254
|
-
combined.includes("invalid") ||
|
|
255
|
-
combined.includes("required") ||
|
|
256
|
-
combined.includes("missing")
|
|
257
|
-
) {
|
|
258
|
-
return "validation";
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Check for network errors
|
|
262
|
-
if (
|
|
263
|
-
combined.includes("enotfound") ||
|
|
264
|
-
combined.includes("etimedout") ||
|
|
265
|
-
combined.includes("econnrefused") ||
|
|
266
|
-
combined.includes("network") ||
|
|
267
|
-
combined.includes("fetch")
|
|
268
|
-
) {
|
|
269
|
-
return "network";
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// Check for build errors
|
|
273
|
-
if (
|
|
274
|
-
combined.includes("vite") ||
|
|
275
|
-
combined.includes("build") ||
|
|
276
|
-
combined.includes("compile") ||
|
|
277
|
-
combined.includes("bundle")
|
|
278
|
-
) {
|
|
279
|
-
return "build";
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// Check for deploy errors
|
|
283
|
-
if (combined.includes("deploy") || combined.includes("publish") || combined.includes("upload")) {
|
|
284
|
-
return "deploy";
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Check for config errors
|
|
288
|
-
if (
|
|
289
|
-
combined.includes("wrangler") ||
|
|
290
|
-
combined.includes("config") ||
|
|
291
|
-
combined.includes("toml") ||
|
|
292
|
-
combined.includes("json")
|
|
293
|
-
) {
|
|
294
|
-
return "config";
|
|
295
|
-
}
|
|
203
|
+
const combined = `${(error as Error)?.message || ""} ${String(error)}`.toLowerCase();
|
|
296
204
|
|
|
205
|
+
if (/validation|invalid|required|missing/.test(combined)) return "validation";
|
|
206
|
+
if (/enotfound|etimedout|econnrefused|network|fetch/.test(combined)) return "network";
|
|
207
|
+
if (/vite|build|compile|bundle/.test(combined)) return "build";
|
|
208
|
+
if (/deploy|publish|upload/.test(combined)) return "deploy";
|
|
209
|
+
if (/wrangler|config|toml|json/.test(combined)) return "config";
|
|
297
210
|
return "unknown";
|
|
298
211
|
}
|
package/src/lib/zip-packager.ts
CHANGED
|
@@ -41,6 +41,7 @@ export interface ManifestData {
|
|
|
41
41
|
| "none";
|
|
42
42
|
};
|
|
43
43
|
vars?: Record<string, string>;
|
|
44
|
+
r2?: Array<{ binding: string; bucket_name: string }>;
|
|
44
45
|
};
|
|
45
46
|
}
|
|
46
47
|
|
|
@@ -160,6 +161,14 @@ function extractBindingsFromConfig(config?: WranglerConfig): ManifestData["bindi
|
|
|
160
161
|
bindings.vars = config.vars;
|
|
161
162
|
}
|
|
162
163
|
|
|
164
|
+
// Extract R2 bucket bindings (support multiple)
|
|
165
|
+
if (config.r2_buckets && config.r2_buckets.length > 0) {
|
|
166
|
+
bindings.r2 = config.r2_buckets.map((bucket) => ({
|
|
167
|
+
binding: bucket.binding,
|
|
168
|
+
bucket_name: bucket.bucket_name,
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
171
|
+
|
|
163
172
|
// Return undefined if no bindings were extracted
|
|
164
173
|
return Object.keys(bindings).length > 0 ? bindings : undefined;
|
|
165
174
|
}
|
package/src/templates/index.ts
CHANGED
|
@@ -2,13 +2,13 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { readFile, readdir } from "node:fs/promises";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { parseJsonc } from "../lib/jsonc.ts";
|
|
5
|
-
import type { TemplateOrigin } from "../lib/
|
|
5
|
+
import type { TemplateMetadata as TemplateOrigin } from "../lib/project-link.ts";
|
|
6
6
|
import type { Template } from "./types";
|
|
7
7
|
|
|
8
8
|
// Resolve templates directory relative to this file (src/templates -> templates)
|
|
9
9
|
const TEMPLATES_DIR = join(dirname(dirname(import.meta.dir)), "templates");
|
|
10
10
|
|
|
11
|
-
export const BUILTIN_TEMPLATES = ["hello", "miniapp", "api"];
|
|
11
|
+
export const BUILTIN_TEMPLATES = ["hello", "miniapp", "api", "nextjs"];
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Resolved template with origin tracking for lineage
|
|
@@ -28,6 +28,7 @@ async function readTemplateFiles(dir: string, base = ""): Promise<Record<string,
|
|
|
28
28
|
// Skip these directories/files (but keep bun.lock for faster installs)
|
|
29
29
|
const SKIP = [
|
|
30
30
|
".jack.json",
|
|
31
|
+
".jack", // Skip .jack directory (template.json is for origin tracking, not project files)
|
|
31
32
|
"node_modules",
|
|
32
33
|
".git",
|
|
33
34
|
"package-lock.json",
|
|
@@ -149,9 +150,10 @@ export async function resolveTemplateWithOrigin(
|
|
|
149
150
|
export function renderTemplate(template: Template, vars: { name: string }): Template {
|
|
150
151
|
const rendered: Record<string, string> = {};
|
|
151
152
|
for (const [path, content] of Object.entries(template.files)) {
|
|
152
|
-
// Replace
|
|
153
|
+
// Replace suffixed variants first to avoid partial matches
|
|
153
154
|
rendered[path] = content
|
|
154
155
|
.replace(/jack-template-db/g, `${vars.name}-db`)
|
|
156
|
+
.replace(/jack-template-cache/g, `${vars.name}-cache`)
|
|
155
157
|
.replace(/jack-template/g, vars.name);
|
|
156
158
|
}
|
|
157
159
|
return { ...template, files: rendered };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
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
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
import './globals.css';
|
|
3
|
+
|
|
4
|
+
export const metadata: Metadata = {
|
|
5
|
+
title: 'jack-template',
|
|
6
|
+
description: 'Next.js app built with jack',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export default function RootLayout({
|
|
10
|
+
children,
|
|
11
|
+
}: {
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
}) {
|
|
14
|
+
return (
|
|
15
|
+
<html lang="en">
|
|
16
|
+
<body>{children}</body>
|
|
17
|
+
</html>
|
|
18
|
+
);
|
|
19
|
+
}
|