@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.
- package/package.json +6 -2
- package/src/commands/down.ts +20 -3
- package/src/commands/mcp.ts +17 -1
- package/src/commands/publish.ts +50 -0
- package/src/commands/ship.ts +4 -3
- package/src/index.ts +7 -0
- package/src/lib/agent-files.ts +0 -2
- package/src/lib/binding-validator.ts +9 -4
- package/src/lib/build-helper.ts +67 -45
- package/src/lib/config-generator.ts +120 -0
- package/src/lib/config.ts +2 -1
- package/src/lib/control-plane.ts +61 -0
- package/src/lib/managed-deploy.ts +10 -2
- package/src/lib/mcp-config.ts +2 -1
- package/src/lib/output.ts +21 -1
- package/src/lib/project-detection.ts +431 -0
- package/src/lib/project-link.test.ts +4 -5
- package/src/lib/project-link.ts +5 -3
- package/src/lib/project-operations.ts +334 -35
- package/src/lib/project-resolver.ts +9 -2
- package/src/lib/secrets.ts +1 -2
- package/src/lib/storage/file-filter.ts +5 -0
- package/src/lib/telemetry-config.ts +3 -3
- package/src/lib/telemetry.ts +4 -0
- package/src/lib/zip-packager.ts +8 -0
- package/src/mcp/test-utils.ts +112 -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/src/lib/mcp-config.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { mkdir } from "node:fs/promises";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { platform } from "node:os";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
|
+
import { CONFIG_DIR } from "./config.ts";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* MCP server configuration structure
|
|
@@ -44,7 +45,7 @@ export const APP_MCP_CONFIGS: Record<string, AppMcpConfig> = {
|
|
|
44
45
|
/**
|
|
45
46
|
* Jack MCP configuration storage path
|
|
46
47
|
*/
|
|
47
|
-
const JACK_MCP_CONFIG_DIR = join(
|
|
48
|
+
const JACK_MCP_CONFIG_DIR = join(CONFIG_DIR, "mcp");
|
|
48
49
|
const JACK_MCP_CONFIG_PATH = join(JACK_MCP_CONFIG_DIR, "config.json");
|
|
49
50
|
|
|
50
51
|
/**
|
package/src/lib/output.ts
CHANGED
|
@@ -32,7 +32,27 @@ export const output = {
|
|
|
32
32
|
* Create a spinner for long-running operations
|
|
33
33
|
*/
|
|
34
34
|
export function spinner(text: string) {
|
|
35
|
-
|
|
35
|
+
const spin = yoctoSpinner({ text }).start();
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
success(message: string) {
|
|
39
|
+
spin.stop();
|
|
40
|
+
success(message);
|
|
41
|
+
},
|
|
42
|
+
error(message: string) {
|
|
43
|
+
spin.stop();
|
|
44
|
+
error(message);
|
|
45
|
+
},
|
|
46
|
+
stop() {
|
|
47
|
+
spin.stop();
|
|
48
|
+
},
|
|
49
|
+
get text() {
|
|
50
|
+
return spin.text;
|
|
51
|
+
},
|
|
52
|
+
set text(value: string | undefined) {
|
|
53
|
+
spin.text = value ?? "";
|
|
54
|
+
},
|
|
55
|
+
};
|
|
36
56
|
}
|
|
37
57
|
|
|
38
58
|
/**
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { readdir, stat } from "node:fs/promises";
|
|
3
|
+
import { join, relative } from "node:path";
|
|
4
|
+
import { Glob } from "bun";
|
|
5
|
+
import { DEFAULT_EXCLUDES } from "./storage/file-filter.ts";
|
|
6
|
+
|
|
7
|
+
export type ProjectType = "vite" | "sveltekit" | "hono" | "unknown";
|
|
8
|
+
|
|
9
|
+
export interface DetectionResult {
|
|
10
|
+
type: ProjectType;
|
|
11
|
+
configFile?: string;
|
|
12
|
+
entryPoint?: string;
|
|
13
|
+
error?: string;
|
|
14
|
+
unsupportedFramework?:
|
|
15
|
+
| "nextjs"
|
|
16
|
+
| "astro"
|
|
17
|
+
| "nuxt"
|
|
18
|
+
| "remix"
|
|
19
|
+
| "react-router"
|
|
20
|
+
| "tanstack-start"
|
|
21
|
+
| "tauri";
|
|
22
|
+
detectedDeps?: string[];
|
|
23
|
+
configFiles?: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ValidationResult {
|
|
27
|
+
valid: boolean;
|
|
28
|
+
error?: string;
|
|
29
|
+
fileCount?: number;
|
|
30
|
+
totalSizeKb?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface PackageJson {
|
|
34
|
+
name?: string;
|
|
35
|
+
dependencies?: Record<string, string>;
|
|
36
|
+
devDependencies?: Record<string, string>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const CONFIG_EXTENSIONS = [".ts", ".js", ".mjs"];
|
|
40
|
+
const HONO_ENTRY_CANDIDATES = [
|
|
41
|
+
"src/index.ts",
|
|
42
|
+
"src/index.js",
|
|
43
|
+
"index.ts",
|
|
44
|
+
"index.js",
|
|
45
|
+
"src/server.ts",
|
|
46
|
+
"src/server.js",
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const MAX_FILES = 1000;
|
|
50
|
+
const MAX_SIZE_KB = 50 * 1024; // 50MB in KB
|
|
51
|
+
|
|
52
|
+
function hasDep(pkg: PackageJson, name: string): boolean {
|
|
53
|
+
return !!(pkg.dependencies?.[name] || pkg.devDependencies?.[name]);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function hasConfigFile(projectPath: string, baseName: string): string | null {
|
|
57
|
+
for (const ext of CONFIG_EXTENSIONS) {
|
|
58
|
+
const configPath = join(projectPath, `${baseName}${ext}`);
|
|
59
|
+
if (existsSync(configPath)) {
|
|
60
|
+
return `${baseName}${ext}`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readPackageJson(projectPath: string): PackageJson | null {
|
|
67
|
+
const packageJsonPath = join(projectPath, "package.json");
|
|
68
|
+
if (!existsSync(packageJsonPath)) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
return JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Error messages with helpful setup instructions for each framework
|
|
79
|
+
const FRAMEWORK_SETUP_MESSAGES: Record<string, string> = {
|
|
80
|
+
nextjs: `Next.js project detected!
|
|
81
|
+
|
|
82
|
+
Next.js support requires OpenNext. For now, set up manually:
|
|
83
|
+
1. bun add @opennextjs/cloudflare
|
|
84
|
+
2. Create open-next.config.ts
|
|
85
|
+
3. Create wrangler.jsonc manually
|
|
86
|
+
|
|
87
|
+
Docs: https://opennext.js.org/cloudflare`,
|
|
88
|
+
|
|
89
|
+
astro: `Astro project detected!
|
|
90
|
+
|
|
91
|
+
Auto-deploy coming soon. For now, set up manually:
|
|
92
|
+
1. bun add @astrojs/cloudflare
|
|
93
|
+
2. Configure adapter in astro.config.mjs
|
|
94
|
+
3. Create wrangler.jsonc with main: "dist/_worker.js"
|
|
95
|
+
|
|
96
|
+
Docs: https://docs.astro.build/en/guides/deploy/cloudflare/
|
|
97
|
+
Want this supported? Run: jack feedback`,
|
|
98
|
+
|
|
99
|
+
nuxt: `Nuxt project detected!
|
|
100
|
+
|
|
101
|
+
Auto-deploy coming soon. For now, set up manually:
|
|
102
|
+
1. Set nitro.preset to 'cloudflare' in nuxt.config.ts
|
|
103
|
+
2. Run: NITRO_PRESET=cloudflare bunx nuxt build
|
|
104
|
+
3. Create wrangler.jsonc with main: ".output/server/index.mjs"
|
|
105
|
+
|
|
106
|
+
Docs: https://nuxt.com/deploy/cloudflare
|
|
107
|
+
Want this supported? Run: jack feedback`,
|
|
108
|
+
|
|
109
|
+
remix: `Remix project detected!
|
|
110
|
+
|
|
111
|
+
Remix has been superseded by React Router v7. Consider migrating:
|
|
112
|
+
https://reactrouter.com/upgrading/remix
|
|
113
|
+
|
|
114
|
+
Or set up Remix for Cloudflare manually:
|
|
115
|
+
1. Use @remix-run/cloudflare adapter
|
|
116
|
+
2. Create wrangler.jsonc manually`,
|
|
117
|
+
|
|
118
|
+
"react-router": `React Router v7 project detected!
|
|
119
|
+
|
|
120
|
+
Auto-deploy coming soon. For now, set up manually:
|
|
121
|
+
1. bun add @react-router/cloudflare @cloudflare/vite-plugin
|
|
122
|
+
2. Configure cloudflare() plugin in vite.config.ts
|
|
123
|
+
3. Create wrangler.jsonc with main: "build/server/index.js"
|
|
124
|
+
|
|
125
|
+
Docs: https://reactrouter.com/deploying/cloudflare
|
|
126
|
+
Want this supported? Run: jack feedback`,
|
|
127
|
+
|
|
128
|
+
"tanstack-start": `TanStack Start project detected!
|
|
129
|
+
|
|
130
|
+
Auto-deploy coming soon. For now, set up manually:
|
|
131
|
+
1. bun add -d @cloudflare/vite-plugin
|
|
132
|
+
2. Configure cloudflare() plugin in vite.config.ts
|
|
133
|
+
3. Set target: 'cloudflare-module' in tanstackStart() config
|
|
134
|
+
4. Create wrangler.jsonc with main: ".output/server/index.mjs"
|
|
135
|
+
|
|
136
|
+
Docs: https://tanstack.com/start/latest/docs/framework/react/hosting#cloudflare
|
|
137
|
+
Want this supported? Run: jack feedback`,
|
|
138
|
+
|
|
139
|
+
tauri: `Tauri desktop app detected!
|
|
140
|
+
|
|
141
|
+
Tauri apps are native desktop applications and cannot be deployed to Cloudflare Workers.
|
|
142
|
+
jack is for web apps that run in the browser or on the edge.
|
|
143
|
+
|
|
144
|
+
If you have a web frontend in this project, consider deploying it separately.`,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
type UnsupportedFramework =
|
|
148
|
+
| "nextjs"
|
|
149
|
+
| "astro"
|
|
150
|
+
| "nuxt"
|
|
151
|
+
| "remix"
|
|
152
|
+
| "react-router"
|
|
153
|
+
| "tanstack-start"
|
|
154
|
+
| "tauri";
|
|
155
|
+
|
|
156
|
+
function detectUnsupportedFramework(
|
|
157
|
+
projectPath: string,
|
|
158
|
+
pkg: PackageJson | null,
|
|
159
|
+
): UnsupportedFramework | null {
|
|
160
|
+
// Next.js - check config file
|
|
161
|
+
if (hasConfigFile(projectPath, "next.config")) {
|
|
162
|
+
return "nextjs";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Astro - check config file AND dependency (to avoid false positives)
|
|
166
|
+
if (hasConfigFile(projectPath, "astro.config") && pkg && hasDep(pkg, "astro")) {
|
|
167
|
+
return "astro";
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// React Router v7 - check for @react-router/dev (BEFORE checking Nuxt to avoid conflicts)
|
|
171
|
+
if (pkg && hasDep(pkg, "@react-router/dev")) {
|
|
172
|
+
return "react-router";
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// TanStack Start - check for @tanstack/react-start or @tanstack/start
|
|
176
|
+
if (pkg && (hasDep(pkg, "@tanstack/react-start") || hasDep(pkg, "@tanstack/start"))) {
|
|
177
|
+
return "tanstack-start";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Nuxt - check config file AND dependency
|
|
181
|
+
if (hasConfigFile(projectPath, "nuxt.config") && pkg && hasDep(pkg, "nuxt")) {
|
|
182
|
+
return "nuxt";
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Legacy Remix (not React Router v7)
|
|
186
|
+
if (pkg && (hasDep(pkg, "@remix-run/node") || hasDep(pkg, "@remix-run/react"))) {
|
|
187
|
+
return "remix";
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Tauri - desktop app framework (check for src-tauri dir or tauri deps)
|
|
191
|
+
if (
|
|
192
|
+
existsSync(join(projectPath, "src-tauri")) ||
|
|
193
|
+
(pkg && (hasDep(pkg, "@tauri-apps/cli") || hasDep(pkg, "@tauri-apps/api")))
|
|
194
|
+
) {
|
|
195
|
+
return "tauri";
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function findHonoEntry(projectPath: string): string | null {
|
|
202
|
+
for (const candidate of HONO_ENTRY_CANDIDATES) {
|
|
203
|
+
if (existsSync(join(projectPath, candidate))) {
|
|
204
|
+
return candidate;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function detectProjectType(projectPath: string): DetectionResult {
|
|
211
|
+
const pkg = readPackageJson(projectPath);
|
|
212
|
+
const detectedDeps: string[] = [];
|
|
213
|
+
const configFiles: string[] = [];
|
|
214
|
+
|
|
215
|
+
// Check for unsupported/coming-soon frameworks first (before reading package.json)
|
|
216
|
+
// This provides better error messages for frameworks we recognize but don't auto-deploy yet
|
|
217
|
+
const unsupported = detectUnsupportedFramework(projectPath, pkg);
|
|
218
|
+
if (unsupported) {
|
|
219
|
+
// Collect detected config files for reporting
|
|
220
|
+
const configFileMap: Record<string, string> = {
|
|
221
|
+
nextjs: "next.config",
|
|
222
|
+
astro: "astro.config",
|
|
223
|
+
nuxt: "nuxt.config",
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
if (configFileMap[unsupported]) {
|
|
227
|
+
const configFile = hasConfigFile(projectPath, configFileMap[unsupported]);
|
|
228
|
+
if (configFile) {
|
|
229
|
+
configFiles.push(configFile);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Collect detected dependencies
|
|
234
|
+
if (pkg) {
|
|
235
|
+
if (unsupported === "astro" && hasDep(pkg, "astro")) detectedDeps.push("astro");
|
|
236
|
+
if (unsupported === "nuxt" && hasDep(pkg, "nuxt")) detectedDeps.push("nuxt");
|
|
237
|
+
if (unsupported === "react-router" && hasDep(pkg, "@react-router/dev"))
|
|
238
|
+
detectedDeps.push("@react-router/dev");
|
|
239
|
+
if (
|
|
240
|
+
unsupported === "tanstack-start" &&
|
|
241
|
+
(hasDep(pkg, "@tanstack/react-start") || hasDep(pkg, "@tanstack/start"))
|
|
242
|
+
) {
|
|
243
|
+
detectedDeps.push(
|
|
244
|
+
hasDep(pkg, "@tanstack/react-start") ? "@tanstack/react-start" : "@tanstack/start",
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
if (unsupported === "remix") {
|
|
248
|
+
if (hasDep(pkg, "@remix-run/node")) detectedDeps.push("@remix-run/node");
|
|
249
|
+
if (hasDep(pkg, "@remix-run/react")) detectedDeps.push("@remix-run/react");
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
type: "unknown",
|
|
255
|
+
unsupportedFramework: unsupported,
|
|
256
|
+
error: FRAMEWORK_SETUP_MESSAGES[unsupported],
|
|
257
|
+
detectedDeps,
|
|
258
|
+
configFiles,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!pkg) {
|
|
263
|
+
return {
|
|
264
|
+
type: "unknown",
|
|
265
|
+
error: "No package.json found",
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// SvelteKit detection
|
|
270
|
+
const svelteConfig = hasConfigFile(projectPath, "svelte.config");
|
|
271
|
+
if (svelteConfig && hasDep(pkg, "@sveltejs/kit")) {
|
|
272
|
+
configFiles.push(svelteConfig);
|
|
273
|
+
detectedDeps.push("@sveltejs/kit");
|
|
274
|
+
|
|
275
|
+
if (!hasDep(pkg, "@sveltejs/adapter-cloudflare")) {
|
|
276
|
+
return {
|
|
277
|
+
type: "sveltekit",
|
|
278
|
+
configFile: svelteConfig,
|
|
279
|
+
error:
|
|
280
|
+
"Missing @sveltejs/adapter-cloudflare dependency. Install it: bun add -D @sveltejs/adapter-cloudflare",
|
|
281
|
+
detectedDeps,
|
|
282
|
+
configFiles,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
detectedDeps.push("@sveltejs/adapter-cloudflare");
|
|
287
|
+
return {
|
|
288
|
+
type: "sveltekit",
|
|
289
|
+
configFile: svelteConfig,
|
|
290
|
+
detectedDeps,
|
|
291
|
+
configFiles,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Vite detection
|
|
296
|
+
const viteConfig = hasConfigFile(projectPath, "vite.config");
|
|
297
|
+
if (viteConfig && hasDep(pkg, "vite")) {
|
|
298
|
+
configFiles.push(viteConfig);
|
|
299
|
+
detectedDeps.push("vite");
|
|
300
|
+
|
|
301
|
+
// Check for Vite + Worker hybrid pattern
|
|
302
|
+
// This is indicated by @cloudflare/vite-plugin AND a worker entry file
|
|
303
|
+
const hasCloudflarePlugin = hasDep(pkg, "@cloudflare/vite-plugin");
|
|
304
|
+
let workerEntry: string | null = null;
|
|
305
|
+
|
|
306
|
+
if (hasCloudflarePlugin) {
|
|
307
|
+
detectedDeps.push("@cloudflare/vite-plugin");
|
|
308
|
+
// Check common worker entry locations
|
|
309
|
+
const workerCandidates = ["src/worker.ts", "src/worker.js", "worker.ts", "worker.js"];
|
|
310
|
+
for (const candidate of workerCandidates) {
|
|
311
|
+
if (existsSync(join(projectPath, candidate))) {
|
|
312
|
+
workerEntry = candidate;
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
type: "vite",
|
|
320
|
+
configFile: viteConfig,
|
|
321
|
+
entryPoint: workerEntry || undefined,
|
|
322
|
+
detectedDeps,
|
|
323
|
+
configFiles,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Hono detection
|
|
328
|
+
if (hasDep(pkg, "hono")) {
|
|
329
|
+
detectedDeps.push("hono");
|
|
330
|
+
const entryPoint = findHonoEntry(projectPath);
|
|
331
|
+
if (entryPoint) {
|
|
332
|
+
return {
|
|
333
|
+
type: "hono",
|
|
334
|
+
entryPoint,
|
|
335
|
+
detectedDeps,
|
|
336
|
+
configFiles,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
type: "hono",
|
|
341
|
+
error:
|
|
342
|
+
"Hono detected but no entry file found. Expected: src/index.ts, index.ts, src/server.ts, or similar.",
|
|
343
|
+
detectedDeps,
|
|
344
|
+
configFiles,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
type: "unknown",
|
|
350
|
+
error: "Could not detect project type. Supported: Vite, SvelteKit, Hono.",
|
|
351
|
+
detectedDeps,
|
|
352
|
+
configFiles,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function shouldExclude(relativePath: string): boolean {
|
|
357
|
+
for (const pattern of DEFAULT_EXCLUDES) {
|
|
358
|
+
const glob = new Glob(pattern);
|
|
359
|
+
if (glob.match(relativePath)) {
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export async function validateProject(projectPath: string): Promise<ValidationResult> {
|
|
367
|
+
if (!existsSync(join(projectPath, "package.json"))) {
|
|
368
|
+
return {
|
|
369
|
+
valid: false,
|
|
370
|
+
error: "No package.json found in project directory",
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
let fileCount = 0;
|
|
375
|
+
let totalSizeBytes = 0;
|
|
376
|
+
|
|
377
|
+
try {
|
|
378
|
+
const entries = await readdir(projectPath, {
|
|
379
|
+
recursive: true,
|
|
380
|
+
withFileTypes: true,
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
for (const entry of entries) {
|
|
384
|
+
if (!entry.isFile()) {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const parentDir = entry.parentPath ?? projectPath;
|
|
389
|
+
const absolutePath = join(parentDir, entry.name);
|
|
390
|
+
const relativePath = relative(projectPath, absolutePath);
|
|
391
|
+
|
|
392
|
+
if (shouldExclude(relativePath)) {
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
fileCount++;
|
|
397
|
+
if (fileCount > MAX_FILES) {
|
|
398
|
+
return {
|
|
399
|
+
valid: false,
|
|
400
|
+
error: `Project has more than ${MAX_FILES} files (excluding node_modules, .git, etc.)`,
|
|
401
|
+
fileCount,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const stats = await stat(absolutePath);
|
|
406
|
+
totalSizeBytes += stats.size;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const totalSizeKb = Math.round(totalSizeBytes / 1024);
|
|
410
|
+
|
|
411
|
+
if (totalSizeKb > MAX_SIZE_KB) {
|
|
412
|
+
return {
|
|
413
|
+
valid: false,
|
|
414
|
+
error: `Project size exceeds ${MAX_SIZE_KB / 1024}MB limit (${Math.round(totalSizeKb / 1024)}MB)`,
|
|
415
|
+
fileCount,
|
|
416
|
+
totalSizeKb,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
valid: true,
|
|
422
|
+
fileCount,
|
|
423
|
+
totalSizeKb,
|
|
424
|
+
};
|
|
425
|
+
} catch (err) {
|
|
426
|
+
return {
|
|
427
|
+
valid: false,
|
|
428
|
+
error: `Failed to scan project: ${err instanceof Error ? err.message : String(err)}`,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
}
|
|
@@ -336,15 +336,14 @@ describe("project-link", () => {
|
|
|
336
336
|
expect(template).toEqual({ type: "builtin", name: "miniapp" });
|
|
337
337
|
});
|
|
338
338
|
|
|
339
|
-
it("creates template.json for
|
|
339
|
+
it("creates template.json for published template", async () => {
|
|
340
340
|
await writeTemplateMetadata(testDir, {
|
|
341
|
-
type: "
|
|
342
|
-
name: "
|
|
343
|
-
ref: "main",
|
|
341
|
+
type: "published",
|
|
342
|
+
name: "alice/my-api",
|
|
344
343
|
});
|
|
345
344
|
|
|
346
345
|
const template = await readTemplateMetadata(testDir);
|
|
347
|
-
expect(template).toEqual({ type: "
|
|
346
|
+
expect(template).toEqual({ type: "published", name: "alice/my-api" });
|
|
348
347
|
});
|
|
349
348
|
|
|
350
349
|
it("creates .jack directory if not exists", async () => {
|
package/src/lib/project-link.ts
CHANGED
|
@@ -29,15 +29,15 @@ export interface LocalProjectLink {
|
|
|
29
29
|
deploy_mode: DeployMode;
|
|
30
30
|
linked_at: string;
|
|
31
31
|
tags?: string[];
|
|
32
|
+
owner_username?: string;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
36
|
* Template metadata stored in .jack/template.json
|
|
36
37
|
*/
|
|
37
38
|
export interface TemplateMetadata {
|
|
38
|
-
type: "builtin" | "
|
|
39
|
-
name: string; // "miniapp", "api", or "
|
|
40
|
-
ref?: string; // git ref for github templates
|
|
39
|
+
type: "builtin" | "user" | "published";
|
|
40
|
+
name: string; // "miniapp", "api", or "username/slug" for published
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
const JACK_DIR = ".jack";
|
|
@@ -94,6 +94,7 @@ export async function linkProject(
|
|
|
94
94
|
projectDir: string,
|
|
95
95
|
projectId: string,
|
|
96
96
|
deployMode: DeployMode,
|
|
97
|
+
ownerUsername?: string,
|
|
97
98
|
): Promise<void> {
|
|
98
99
|
const jackDir = getJackDir(projectDir);
|
|
99
100
|
const linkPath = getProjectLinkPath(projectDir);
|
|
@@ -108,6 +109,7 @@ export async function linkProject(
|
|
|
108
109
|
project_id: projectId,
|
|
109
110
|
deploy_mode: deployMode,
|
|
110
111
|
linked_at: new Date().toISOString(),
|
|
112
|
+
owner_username: ownerUsername,
|
|
111
113
|
};
|
|
112
114
|
|
|
113
115
|
await writeFile(linkPath, JSON.stringify(link, null, 2));
|