@getjack/jack 0.1.6 → 0.1.8

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.
Files changed (38) hide show
  1. package/package.json +6 -2
  2. package/src/commands/down.ts +20 -3
  3. package/src/commands/mcp.ts +17 -1
  4. package/src/commands/publish.ts +50 -0
  5. package/src/commands/ship.ts +4 -3
  6. package/src/index.ts +7 -0
  7. package/src/lib/agent-files.ts +0 -2
  8. package/src/lib/binding-validator.ts +9 -4
  9. package/src/lib/build-helper.ts +67 -45
  10. package/src/lib/config-generator.ts +120 -0
  11. package/src/lib/config.ts +2 -1
  12. package/src/lib/control-plane.ts +61 -0
  13. package/src/lib/managed-deploy.ts +10 -2
  14. package/src/lib/mcp-config.ts +2 -1
  15. package/src/lib/output.ts +21 -1
  16. package/src/lib/project-detection.ts +431 -0
  17. package/src/lib/project-link.test.ts +4 -5
  18. package/src/lib/project-link.ts +5 -3
  19. package/src/lib/project-operations.ts +334 -35
  20. package/src/lib/project-resolver.ts +9 -2
  21. package/src/lib/secrets.ts +1 -2
  22. package/src/lib/storage/file-filter.ts +5 -0
  23. package/src/lib/telemetry-config.ts +3 -3
  24. package/src/lib/telemetry.ts +4 -0
  25. package/src/lib/zip-packager.ts +8 -0
  26. package/src/mcp/test-utils.ts +112 -0
  27. package/src/templates/index.ts +137 -7
  28. package/templates/nextjs/.jack.json +26 -26
  29. package/templates/nextjs/app/globals.css +4 -4
  30. package/templates/nextjs/app/layout.tsx +11 -11
  31. package/templates/nextjs/app/page.tsx +8 -6
  32. package/templates/nextjs/cloudflare-env.d.ts +1 -1
  33. package/templates/nextjs/next.config.ts +1 -1
  34. package/templates/nextjs/open-next.config.ts +1 -1
  35. package/templates/nextjs/package.json +22 -22
  36. package/templates/nextjs/tsconfig.json +26 -42
  37. package/templates/nextjs/wrangler.jsonc +15 -15
  38. package/src/lib/github.ts +0 -151
@@ -0,0 +1,112 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3
+
4
+ export interface McpClientOptions {
5
+ command: string;
6
+ args?: string[];
7
+ cwd?: string;
8
+ env?: NodeJS.ProcessEnv;
9
+ clientName?: string;
10
+ clientVersion?: string;
11
+ }
12
+
13
+ export interface McpTestClient {
14
+ client: Client;
15
+ transport: StdioClientTransport;
16
+ getStderr(): string;
17
+ close(): Promise<void>;
18
+ }
19
+
20
+ export async function openMcpTestClient(options: McpClientOptions): Promise<McpTestClient> {
21
+ const transport = new StdioClientTransport({
22
+ command: options.command,
23
+ args: options.args ?? [],
24
+ cwd: options.cwd,
25
+ env: options.env,
26
+ stderr: "pipe",
27
+ });
28
+
29
+ let stderr = "";
30
+ transport.stderr?.on("data", (chunk) => {
31
+ stderr += chunk.toString();
32
+ });
33
+
34
+ const client = new Client({
35
+ name: options.clientName ?? "jack-mcp-test",
36
+ version: options.clientVersion ?? "0.1.0",
37
+ });
38
+
39
+ try {
40
+ await client.connect(transport);
41
+ } catch (error) {
42
+ await transport.close();
43
+ throw error;
44
+ }
45
+
46
+ return {
47
+ client,
48
+ transport,
49
+ getStderr: () => stderr,
50
+ close: () => transport.close(),
51
+ };
52
+ }
53
+
54
+ export function parseMcpToolResult(toolResult: {
55
+ content?: Array<{ type: string; text?: string }>;
56
+ }): unknown {
57
+ const toolText = toolResult.content?.[0]?.type === "text" ? toolResult.content[0].text : null;
58
+ if (!toolText) {
59
+ throw new Error("MCP tool response missing text content");
60
+ }
61
+
62
+ const parsed = JSON.parse(toolText);
63
+ if (!parsed.success) {
64
+ const message = parsed.error?.message ?? "unknown error";
65
+ throw new Error(`MCP tool failed: ${message}`);
66
+ }
67
+
68
+ return parsed.data;
69
+ }
70
+
71
+ export async function verifyMcpToolsAndResources(client: Client): Promise<void> {
72
+ const tools = await client.listTools();
73
+ if (!tools.tools?.length) {
74
+ throw new Error("MCP server reported no tools");
75
+ }
76
+
77
+ const resources = await client.listResources();
78
+ if (!resources.resources?.length) {
79
+ throw new Error("MCP server reported no resources");
80
+ }
81
+
82
+ await client.readResource({ uri: "agents://context" });
83
+ }
84
+
85
+ export async function callMcpListProjects(
86
+ client: Client,
87
+ filter?: "all" | "local" | "deployed" | "cloud",
88
+ ): Promise<unknown[]> {
89
+ const response = await client.callTool({
90
+ name: "list_projects",
91
+ arguments: filter ? { filter } : {},
92
+ });
93
+
94
+ const data = parseMcpToolResult(response);
95
+ if (!Array.isArray(data)) {
96
+ throw new Error("MCP list_projects returned unexpected data");
97
+ }
98
+
99
+ return data;
100
+ }
101
+
102
+ export async function callMcpGetProjectStatus(
103
+ client: Client,
104
+ args: { name?: string; project_path?: string },
105
+ ): Promise<unknown> {
106
+ const response = await client.callTool({
107
+ name: "get_project_status",
108
+ arguments: args,
109
+ });
110
+
111
+ return parseMcpToolResult(response);
112
+ }
@@ -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 or GitHub URL
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
- // GitHub: user/repo or full URL → fetch from network
226
+ // username/slug format - fetch from jack cloud
115
227
  if (template.includes("/")) {
116
- const { fetchFromGitHub } = await import("../lib/github");
117
- return fetchFromGitHub(template);
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(`Unknown template: ${template}\n\nAvailable: ${BUILTIN_TEMPLATES.join(", ")}`);
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
- const isGitHub = templateName.includes("/");
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: isGitHub ? "github" : "builtin",
266
+ type: originType,
137
267
  name: templateName,
138
268
  };
139
269
 
@@ -1,28 +1,28 @@
1
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
- }
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,9 +1,9 @@
1
1
  * {
2
- box-sizing: border-box;
3
- margin: 0;
4
- padding: 0;
2
+ box-sizing: border-box;
3
+ margin: 0;
4
+ padding: 0;
5
5
  }
6
6
 
7
7
  body {
8
- font-family: system-ui, -apple-system, sans-serif;
8
+ font-family: system-ui, -apple-system, sans-serif;
9
9
  }
@@ -1,19 +1,19 @@
1
- import type { Metadata } from 'next';
2
- import './globals.css';
1
+ import type { Metadata } from "next";
2
+ import "./globals.css";
3
3
 
4
4
  export const metadata: Metadata = {
5
- title: 'jack-template',
6
- description: 'Next.js app built with jack',
5
+ title: "jack-template",
6
+ description: "Next.js app built with jack",
7
7
  };
8
8
 
9
9
  export default function RootLayout({
10
- children,
10
+ children,
11
11
  }: {
12
- children: React.ReactNode;
12
+ children: React.ReactNode;
13
13
  }) {
14
- return (
15
- <html lang="en">
16
- <body>{children}</body>
17
- </html>
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
- return (
3
- <main style={{ padding: '2rem', fontFamily: 'system-ui' }}>
4
- <h1>jack-template</h1>
5
- <p>Ship it with <code>jack ship</code></p>
6
- </main>
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
  }
@@ -1,3 +1,3 @@
1
1
  interface CloudflareEnv {
2
- ASSETS: Fetcher;
2
+ ASSETS: Fetcher;
3
3
  }
@@ -1,5 +1,5 @@
1
- import type { NextConfig } from "next";
2
1
  import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare";
2
+ import type { NextConfig } from "next";
3
3
 
4
4
  initOpenNextCloudflareForDev();
5
5
 
@@ -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
- incrementalCache: r2IncrementalCache,
5
+ incrementalCache: r2IncrementalCache,
6
6
  });
@@ -1,24 +1,24 @@
1
1
  {
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
- }
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
- "compilerOptions": {
3
- "target": "ES2022",
4
- "lib": [
5
- "ES2022",
6
- "DOM",
7
- "DOM.Iterable"
8
- ],
9
- "module": "ESNext",
10
- "moduleResolution": "bundler",
11
- "jsx": "preserve",
12
- "strict": true,
13
- "skipLibCheck": true,
14
- "noEmit": true,
15
- "incremental": true,
16
- "esModuleInterop": true,
17
- "resolveJsonModule": true,
18
- "isolatedModules": true,
19
- "paths": {
20
- "@/*": [
21
- "./*"
22
- ]
23
- },
24
- "plugins": [
25
- {
26
- "name": "next"
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
- "$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
- ]
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
  }