@getjack/jack 0.1.4 → 0.1.6
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 -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/login.ts +124 -1
- package/src/commands/logs.ts +8 -18
- package/src/commands/new.ts +7 -1
- package/src/commands/projects.ts +166 -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 +137 -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 +449 -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 +86 -157
- 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/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,63 @@ 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> = {};
|
|
45
43
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
+
}
|
|
65
|
+
|
|
66
|
+
// ============================================
|
|
67
|
+
// HELPERS
|
|
68
|
+
// ============================================
|
|
55
69
|
async function isEnabled(): Promise<boolean> {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (
|
|
59
|
-
|
|
70
|
+
if (enabledCache !== null) return enabledCache;
|
|
71
|
+
|
|
72
|
+
if (
|
|
73
|
+
process.env.DO_NOT_TRACK === "1" ||
|
|
74
|
+
process.env.CI === "true" ||
|
|
75
|
+
process.env.JACK_TELEMETRY_DISABLED === "1"
|
|
76
|
+
) {
|
|
77
|
+
enabledCache = false;
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
60
80
|
|
|
61
|
-
// Check config file
|
|
62
81
|
try {
|
|
63
82
|
const config = await getTelemetryConfig();
|
|
64
83
|
telemetryConfig = config;
|
|
84
|
+
enabledCache = config.enabled;
|
|
65
85
|
return config.enabled;
|
|
66
86
|
} catch {
|
|
67
|
-
|
|
87
|
+
enabledCache = true;
|
|
68
88
|
return true;
|
|
69
89
|
}
|
|
70
90
|
}
|
|
71
91
|
|
|
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
92
|
async function getAnonymousId(): Promise<string> {
|
|
104
93
|
if (!telemetryConfig) {
|
|
105
94
|
telemetryConfig = await getTelemetryConfig();
|
|
@@ -108,7 +97,7 @@ async function getAnonymousId(): Promise<string> {
|
|
|
108
97
|
}
|
|
109
98
|
|
|
110
99
|
// ============================================
|
|
111
|
-
// USER PROPERTIES
|
|
100
|
+
// USER PROPERTIES
|
|
112
101
|
// ============================================
|
|
113
102
|
export interface UserProperties {
|
|
114
103
|
jack_version: string;
|
|
@@ -116,71 +105,74 @@ export interface UserProperties {
|
|
|
116
105
|
arch: string;
|
|
117
106
|
node_version: string;
|
|
118
107
|
is_ci: boolean;
|
|
108
|
+
shell?: string;
|
|
109
|
+
terminal?: string;
|
|
110
|
+
terminal_width?: number;
|
|
111
|
+
is_tty?: boolean;
|
|
112
|
+
locale?: string;
|
|
119
113
|
config_style?: "byoc" | "jack-cloud";
|
|
120
114
|
}
|
|
121
115
|
|
|
122
|
-
|
|
116
|
+
// Detect environment properties
|
|
117
|
+
export function getEnvironmentProps(): Pick<
|
|
118
|
+
UserProperties,
|
|
119
|
+
"shell" | "terminal" | "terminal_width" | "is_tty" | "locale"
|
|
120
|
+
> {
|
|
121
|
+
return {
|
|
122
|
+
shell: process.env.SHELL?.split("/").pop(), // e.g., /bin/zsh -> zsh
|
|
123
|
+
terminal: process.env.TERM_PROGRAM, // e.g., iTerm.app, vscode, Apple_Terminal
|
|
124
|
+
terminal_width: process.stdout.columns,
|
|
125
|
+
is_tty: process.stdout.isTTY ?? false,
|
|
126
|
+
locale: Intl.DateTimeFormat().resolvedOptions().locale,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
123
129
|
|
|
124
|
-
/**
|
|
125
|
-
* Set user properties (sent with all events)
|
|
126
|
-
* Safe to call multiple times - properties are merged
|
|
127
|
-
*/
|
|
128
130
|
export async function identify(properties: Partial<UserProperties>): Promise<void> {
|
|
129
131
|
userProps = { ...userProps, ...properties };
|
|
130
|
-
|
|
131
|
-
const ph = await getClient();
|
|
132
|
-
if (!ph) return;
|
|
132
|
+
if (!(await isEnabled())) return;
|
|
133
133
|
|
|
134
134
|
try {
|
|
135
135
|
const distinctId = await getAnonymousId();
|
|
136
|
-
|
|
136
|
+
send(`${TELEMETRY_PROXY}/identify`, {
|
|
137
137
|
distinctId,
|
|
138
138
|
properties: userProps,
|
|
139
|
+
setOnce: { first_seen: new Date().toISOString() }, // Only sets on first identify
|
|
139
140
|
});
|
|
140
141
|
} catch {
|
|
141
|
-
// Silent
|
|
142
|
+
// Silent
|
|
142
143
|
}
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
// ============================================
|
|
146
|
-
// TRACK
|
|
147
|
+
// TRACK
|
|
147
148
|
// ============================================
|
|
148
|
-
/**
|
|
149
|
-
* Track an event with optional properties
|
|
150
|
-
* This is fire-and-forget and will never throw or block
|
|
151
|
-
*/
|
|
152
149
|
export async function track(event: EventName, properties?: Record<string, unknown>): Promise<void> {
|
|
153
|
-
|
|
154
|
-
if (!ph) return;
|
|
150
|
+
if (!(await isEnabled())) return;
|
|
155
151
|
|
|
156
152
|
try {
|
|
157
153
|
const distinctId = await getAnonymousId();
|
|
158
|
-
|
|
154
|
+
send(`${TELEMETRY_PROXY}/t`, {
|
|
159
155
|
distinctId,
|
|
160
156
|
event,
|
|
161
157
|
properties: {
|
|
162
158
|
...properties,
|
|
163
159
|
...userProps,
|
|
164
|
-
|
|
160
|
+
$session_id: SESSION_ID, // Groups events from same CLI invocation
|
|
165
161
|
},
|
|
162
|
+
timestamp: Date.now(),
|
|
166
163
|
});
|
|
167
164
|
} catch {
|
|
168
|
-
// Silent
|
|
165
|
+
// Silent
|
|
169
166
|
}
|
|
170
167
|
}
|
|
171
168
|
|
|
172
169
|
// ============================================
|
|
173
|
-
//
|
|
174
|
-
// Commands wrapped with this get automatic tracking
|
|
170
|
+
// WRAPPER
|
|
175
171
|
// ============================================
|
|
176
172
|
export interface TelemetryOptions {
|
|
177
173
|
platform?: "cli" | "mcp";
|
|
178
174
|
}
|
|
179
175
|
|
|
180
|
-
/**
|
|
181
|
-
* Wrap a command function with automatic telemetry tracking
|
|
182
|
-
* Tracks command_invoked, command_completed, and command_failed events
|
|
183
|
-
*/
|
|
184
176
|
// biome-ignore lint/suspicious/noExplicitAny: Required for flexible command wrapping
|
|
185
177
|
export function withTelemetry<T extends (...args: any[]) => Promise<any>>(
|
|
186
178
|
commandName: string,
|
|
@@ -190,7 +182,6 @@ export function withTelemetry<T extends (...args: any[]) => Promise<any>>(
|
|
|
190
182
|
const platform = options?.platform ?? "cli";
|
|
191
183
|
|
|
192
184
|
return (async (...args: Parameters<T>) => {
|
|
193
|
-
// Fire-and-forget: don't await track() to avoid blocking command execution
|
|
194
185
|
track(Events.COMMAND_INVOKED, { command: commandName, platform });
|
|
195
186
|
const start = Date.now();
|
|
196
187
|
|
|
@@ -215,84 +206,22 @@ export function withTelemetry<T extends (...args: any[]) => Promise<any>>(
|
|
|
215
206
|
}
|
|
216
207
|
|
|
217
208
|
// ============================================
|
|
218
|
-
// SHUTDOWN -
|
|
209
|
+
// SHUTDOWN - No-op (detached processes handle themselves)
|
|
219
210
|
// ============================================
|
|
220
|
-
/**
|
|
221
|
-
* Gracefully shutdown telemetry client
|
|
222
|
-
* Times out after 500ms to never block CLI exit
|
|
223
|
-
*/
|
|
224
211
|
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
|
-
}
|
|
212
|
+
// No-op - detached subprocesses send telemetry independently
|
|
237
213
|
}
|
|
238
214
|
|
|
239
215
|
// ============================================
|
|
240
216
|
// ERROR CLASSIFICATION
|
|
241
217
|
// ============================================
|
|
242
|
-
/**
|
|
243
|
-
* Classify error into broad categories for analytics
|
|
244
|
-
* Returns: 'validation' | 'network' | 'build' | 'deploy' | 'config' | 'unknown'
|
|
245
|
-
*/
|
|
246
218
|
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
|
-
}
|
|
219
|
+
const combined = `${(error as Error)?.message || ""} ${String(error)}`.toLowerCase();
|
|
296
220
|
|
|
221
|
+
if (/validation|invalid|required|missing/.test(combined)) return "validation";
|
|
222
|
+
if (/enotfound|etimedout|econnrefused|network|fetch/.test(combined)) return "network";
|
|
223
|
+
if (/vite|build|compile|bundle/.test(combined)) return "build";
|
|
224
|
+
if (/deploy|publish|upload/.test(combined)) return "deploy";
|
|
225
|
+
if (/wrangler|config|toml|json/.test(combined)) return "config";
|
|
297
226
|
return "unknown";
|
|
298
227
|
}
|
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,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
|
+
}
|