@getjack/jack 0.1.6 → 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/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 +61 -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-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 +4 -0
- 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
|
@@ -23,8 +23,22 @@ import {
|
|
|
23
23
|
runAgentOneShot,
|
|
24
24
|
validateAgentPaths,
|
|
25
25
|
} from "./agents.ts";
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
checkWranglerVersion,
|
|
28
|
+
getWranglerVersion,
|
|
29
|
+
needsOpenNextBuild,
|
|
30
|
+
needsViteBuild,
|
|
31
|
+
parseWranglerConfig,
|
|
32
|
+
runOpenNextBuild,
|
|
33
|
+
runViteBuild,
|
|
34
|
+
} from "./build-helper.ts";
|
|
27
35
|
import { checkWorkerExists, getAccountId, listD1Databases } from "./cloudflare-api.ts";
|
|
36
|
+
import {
|
|
37
|
+
generateWranglerConfig,
|
|
38
|
+
getDefaultProjectName,
|
|
39
|
+
slugify,
|
|
40
|
+
writeWranglerConfig,
|
|
41
|
+
} from "./config-generator.ts";
|
|
28
42
|
import { getSyncConfig } from "./config.ts";
|
|
29
43
|
import { deleteManagedProject } from "./control-plane.ts";
|
|
30
44
|
import { debug, isDebug } from "./debug.ts";
|
|
@@ -40,6 +54,7 @@ import {
|
|
|
40
54
|
} from "./managed-deploy.ts";
|
|
41
55
|
import { generateProjectName } from "./names.ts";
|
|
42
56
|
import { getAllPaths, registerPath, unregisterPath } from "./paths-index.ts";
|
|
57
|
+
import { detectProjectType, validateProject } from "./project-detection.ts";
|
|
43
58
|
import {
|
|
44
59
|
type DeployMode,
|
|
45
60
|
type TemplateMetadata as TemplateOrigin,
|
|
@@ -83,6 +98,7 @@ export interface DeployOptions {
|
|
|
83
98
|
includeSync?: boolean;
|
|
84
99
|
managed?: boolean; // Force managed deploy mode
|
|
85
100
|
byo?: boolean; // Force BYO deploy mode
|
|
101
|
+
dryRun?: boolean; // Stop before actual deployment
|
|
86
102
|
}
|
|
87
103
|
|
|
88
104
|
export interface DeployResult {
|
|
@@ -293,6 +309,213 @@ async function preflightD1Capacity(
|
|
|
293
309
|
}
|
|
294
310
|
}
|
|
295
311
|
|
|
312
|
+
// ============================================================================
|
|
313
|
+
// Auto-detect Flow for Ship Command
|
|
314
|
+
// ============================================================================
|
|
315
|
+
|
|
316
|
+
interface AutoDetectResult {
|
|
317
|
+
projectName: string;
|
|
318
|
+
projectId: string | null; // null when dry run (no cloud project created)
|
|
319
|
+
deployMode: DeployMode;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Run the auto-detect flow when no wrangler config exists.
|
|
324
|
+
* Detects project type, prompts user for confirmation, generates config,
|
|
325
|
+
* and creates managed project on jack cloud.
|
|
326
|
+
*
|
|
327
|
+
* @param dryRun - If true, skip cloud project creation and linking
|
|
328
|
+
*/
|
|
329
|
+
async function runAutoDetectFlow(
|
|
330
|
+
projectPath: string,
|
|
331
|
+
reporter: OperationReporter,
|
|
332
|
+
interactive: boolean,
|
|
333
|
+
dryRun = false,
|
|
334
|
+
): Promise<AutoDetectResult> {
|
|
335
|
+
// Step 1: Validate project (file count, size limits)
|
|
336
|
+
const validation = await validateProject(projectPath);
|
|
337
|
+
if (!validation.valid) {
|
|
338
|
+
track(Events.AUTO_DETECT_REJECTED, { reason: "validation_failed" });
|
|
339
|
+
throw new JackError(
|
|
340
|
+
JackErrorCode.VALIDATION_ERROR,
|
|
341
|
+
validation.error || "Project validation failed",
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Step 2: Detect project type
|
|
346
|
+
const detection = detectProjectType(projectPath);
|
|
347
|
+
|
|
348
|
+
// Step 3: Handle unsupported frameworks
|
|
349
|
+
if (detection.unsupportedFramework) {
|
|
350
|
+
track(Events.AUTO_DETECT_FAILED, {
|
|
351
|
+
reason: "unsupported_framework",
|
|
352
|
+
framework: detection.unsupportedFramework,
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// Use the detailed error message from detection (includes setup instructions)
|
|
356
|
+
throw new JackError(
|
|
357
|
+
JackErrorCode.VALIDATION_ERROR,
|
|
358
|
+
detection.error || `${detection.unsupportedFramework} is not yet supported`,
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Step 4: Handle unknown project type
|
|
363
|
+
if (detection.type === "unknown") {
|
|
364
|
+
track(Events.AUTO_DETECT_FAILED, { reason: "unknown_type" });
|
|
365
|
+
|
|
366
|
+
throw new JackError(
|
|
367
|
+
JackErrorCode.VALIDATION_ERROR,
|
|
368
|
+
"Could not detect project type\n\nSupported types:\n - Vite (React, Vue, etc.)\n - Hono API\n - SvelteKit (with @sveltejs/adapter-cloudflare)\n\nTo deploy manually, create a wrangler.jsonc file.\nDocs: https://docs.getjack.org/guides/manual-setup",
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Step 5: Handle detection errors (e.g., missing adapter)
|
|
373
|
+
if (detection.error) {
|
|
374
|
+
track(Events.AUTO_DETECT_FAILED, {
|
|
375
|
+
reason: "detection_error",
|
|
376
|
+
type: detection.type,
|
|
377
|
+
});
|
|
378
|
+
throw new JackError(JackErrorCode.VALIDATION_ERROR, detection.error);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Step 6: Detection succeeded - show what was detected
|
|
382
|
+
const typeLabels: Record<string, string> = {
|
|
383
|
+
vite: "Vite",
|
|
384
|
+
hono: "Hono API",
|
|
385
|
+
sveltekit: "SvelteKit",
|
|
386
|
+
};
|
|
387
|
+
const typeLabel = typeLabels[detection.type] || detection.type;
|
|
388
|
+
const configInfo = detection.configFile || detection.entryPoint || "";
|
|
389
|
+
reporter.info(`Detected: ${typeLabel} project${configInfo ? ` (${configInfo})` : ""}`);
|
|
390
|
+
|
|
391
|
+
// Step 7: Fetch username for URL preview (skip for dry run)
|
|
392
|
+
let ownerUsername: string | null = null;
|
|
393
|
+
if (!dryRun) {
|
|
394
|
+
const { getCurrentUserProfile } = await import("./control-plane.ts");
|
|
395
|
+
const profile = await getCurrentUserProfile();
|
|
396
|
+
ownerUsername = profile?.username ?? null;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Step 8: Get default project name and prompt user
|
|
400
|
+
const defaultName = getDefaultProjectName(projectPath);
|
|
401
|
+
|
|
402
|
+
if (!interactive) {
|
|
403
|
+
// Non-interactive mode - use defaults
|
|
404
|
+
const projectName = defaultName;
|
|
405
|
+
const runjackUrl = ownerUsername
|
|
406
|
+
? `https://${ownerUsername}-${projectName}.runjack.xyz`
|
|
407
|
+
: `https://${projectName}.runjack.xyz`;
|
|
408
|
+
|
|
409
|
+
reporter.info(`Project name: ${projectName}`);
|
|
410
|
+
reporter.info(`Will deploy to: ${runjackUrl}`);
|
|
411
|
+
|
|
412
|
+
// Generate and write wrangler config
|
|
413
|
+
const wranglerConfig = generateWranglerConfig(
|
|
414
|
+
detection.type,
|
|
415
|
+
projectName,
|
|
416
|
+
detection.entryPoint,
|
|
417
|
+
);
|
|
418
|
+
writeWranglerConfig(projectPath, wranglerConfig);
|
|
419
|
+
reporter.success("Created wrangler.jsonc");
|
|
420
|
+
|
|
421
|
+
// Skip cloud creation and linking for dry run
|
|
422
|
+
if (dryRun) {
|
|
423
|
+
track(Events.AUTO_DETECT_SUCCESS, { type: detection.type });
|
|
424
|
+
return {
|
|
425
|
+
projectName,
|
|
426
|
+
projectId: null,
|
|
427
|
+
deployMode: "managed",
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Create managed project on jack cloud
|
|
432
|
+
const remoteResult = await createManagedProjectRemote(projectName, reporter, {
|
|
433
|
+
usePrebuilt: false,
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// Link project locally (include username for correct URL display)
|
|
437
|
+
await linkProject(projectPath, remoteResult.projectId, "managed", ownerUsername ?? undefined);
|
|
438
|
+
await registerPath(remoteResult.projectId, projectPath);
|
|
439
|
+
|
|
440
|
+
track(Events.AUTO_DETECT_SUCCESS, { type: detection.type });
|
|
441
|
+
|
|
442
|
+
return {
|
|
443
|
+
projectName,
|
|
444
|
+
projectId: remoteResult.projectId,
|
|
445
|
+
deployMode: "managed",
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Interactive mode - prompt for project name
|
|
450
|
+
const { input } = await import("@inquirer/prompts");
|
|
451
|
+
|
|
452
|
+
console.error("");
|
|
453
|
+
const projectName = await input({
|
|
454
|
+
message: "Project name:",
|
|
455
|
+
default: defaultName,
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
const slugifiedName = slugify(projectName.trim());
|
|
459
|
+
const runjackUrl = ownerUsername
|
|
460
|
+
? `https://${ownerUsername}-${slugifiedName}.runjack.xyz`
|
|
461
|
+
: `https://${slugifiedName}.runjack.xyz`;
|
|
462
|
+
|
|
463
|
+
// Confirmation prompt
|
|
464
|
+
console.error("");
|
|
465
|
+
console.error(" This will:");
|
|
466
|
+
console.error(" - Create wrangler.jsonc");
|
|
467
|
+
console.error(" - Create project on jack cloud");
|
|
468
|
+
console.error(` - Deploy to ${runjackUrl}`);
|
|
469
|
+
console.error("");
|
|
470
|
+
|
|
471
|
+
const { promptSelect } = await import("./hooks.ts");
|
|
472
|
+
const choice = await promptSelect(["Continue", "Cancel"]);
|
|
473
|
+
|
|
474
|
+
if (choice !== 0) {
|
|
475
|
+
track(Events.AUTO_DETECT_REJECTED, { reason: "user_cancelled" });
|
|
476
|
+
throw new JackError(JackErrorCode.VALIDATION_ERROR, "Deployment cancelled", undefined, {
|
|
477
|
+
exitCode: 0,
|
|
478
|
+
reported: true,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Generate and write wrangler config
|
|
483
|
+
const wranglerConfig = generateWranglerConfig(
|
|
484
|
+
detection.type,
|
|
485
|
+
slugifiedName,
|
|
486
|
+
detection.entryPoint,
|
|
487
|
+
);
|
|
488
|
+
writeWranglerConfig(projectPath, wranglerConfig);
|
|
489
|
+
reporter.success("Created wrangler.jsonc");
|
|
490
|
+
|
|
491
|
+
// Skip cloud creation and linking for dry run
|
|
492
|
+
if (dryRun) {
|
|
493
|
+
track(Events.AUTO_DETECT_SUCCESS, { type: detection.type });
|
|
494
|
+
return {
|
|
495
|
+
projectName: slugifiedName,
|
|
496
|
+
projectId: null,
|
|
497
|
+
deployMode: "managed",
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Create managed project on jack cloud
|
|
502
|
+
const remoteResult = await createManagedProjectRemote(slugifiedName, reporter, {
|
|
503
|
+
usePrebuilt: false,
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
// Link project locally (include username for correct URL display)
|
|
507
|
+
await linkProject(projectPath, remoteResult.projectId, "managed", ownerUsername ?? undefined);
|
|
508
|
+
await registerPath(remoteResult.projectId, projectPath);
|
|
509
|
+
|
|
510
|
+
track(Events.AUTO_DETECT_SUCCESS, { type: detection.type });
|
|
511
|
+
|
|
512
|
+
return {
|
|
513
|
+
projectName: slugifiedName,
|
|
514
|
+
projectId: remoteResult.projectId,
|
|
515
|
+
deployMode: "managed",
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
296
519
|
// ============================================================================
|
|
297
520
|
// Create Project Operation
|
|
298
521
|
// ============================================================================
|
|
@@ -674,7 +897,7 @@ export async function createProject(
|
|
|
674
897
|
try {
|
|
675
898
|
const result = await runParallelSetup(targetDir, projectName, {
|
|
676
899
|
template: resolvedTemplate || "hello",
|
|
677
|
-
usePrebuilt:
|
|
900
|
+
usePrebuilt: templateOrigin.type === "builtin", // Only builtin templates have prebuilt bundles
|
|
678
901
|
});
|
|
679
902
|
remoteResult = result.remoteResult;
|
|
680
903
|
reporter.stop();
|
|
@@ -787,9 +1010,14 @@ export async function createProject(
|
|
|
787
1010
|
);
|
|
788
1011
|
}
|
|
789
1012
|
|
|
1013
|
+
// Fetch username for link storage
|
|
1014
|
+
const { getCurrentUserProfile } = await import("./control-plane.ts");
|
|
1015
|
+
const profile = await getCurrentUserProfile();
|
|
1016
|
+
const ownerUsername = profile?.username ?? undefined;
|
|
1017
|
+
|
|
790
1018
|
// Link project locally and register path
|
|
791
1019
|
try {
|
|
792
|
-
await linkProject(targetDir, remoteResult.projectId, "managed");
|
|
1020
|
+
await linkProject(targetDir, remoteResult.projectId, "managed", ownerUsername);
|
|
793
1021
|
await writeTemplateMetadata(targetDir, templateOrigin);
|
|
794
1022
|
await registerPath(remoteResult.projectId, targetDir);
|
|
795
1023
|
} catch (err) {
|
|
@@ -957,6 +1185,7 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
|
|
|
957
1185
|
interactive: interactiveOption,
|
|
958
1186
|
includeSecrets = false,
|
|
959
1187
|
includeSync = false,
|
|
1188
|
+
dryRun = false,
|
|
960
1189
|
} = options;
|
|
961
1190
|
const reporter = providedReporter ?? noopReporter;
|
|
962
1191
|
const hasReporter = Boolean(providedReporter);
|
|
@@ -974,7 +1203,14 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
|
|
|
974
1203
|
existsSync(join(projectPath, "wrangler.jsonc")) ||
|
|
975
1204
|
existsSync(join(projectPath, "wrangler.json"));
|
|
976
1205
|
|
|
977
|
-
|
|
1206
|
+
// Check for existing project link
|
|
1207
|
+
const hasProjectLink = existsSync(join(projectPath, ".jack", "project.json"));
|
|
1208
|
+
|
|
1209
|
+
// Auto-detect flow: no wrangler config and no project link
|
|
1210
|
+
let autoDetectResult: AutoDetectResult | null = null;
|
|
1211
|
+
if (!hasWranglerConfig && !hasProjectLink) {
|
|
1212
|
+
autoDetectResult = await runAutoDetectFlow(projectPath, reporter, interactive, dryRun);
|
|
1213
|
+
} else if (!hasWranglerConfig) {
|
|
978
1214
|
throw new JackError(
|
|
979
1215
|
JackErrorCode.PROJECT_NOT_FOUND,
|
|
980
1216
|
"No wrangler config found in current directory",
|
|
@@ -982,18 +1218,20 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
|
|
|
982
1218
|
);
|
|
983
1219
|
}
|
|
984
1220
|
|
|
985
|
-
// Get project name from directory
|
|
986
|
-
const projectName = await getProjectNameFromDir(projectPath);
|
|
1221
|
+
// Get project name from directory (or auto-detect result)
|
|
1222
|
+
const projectName = autoDetectResult?.projectName ?? (await getProjectNameFromDir(projectPath));
|
|
987
1223
|
|
|
988
1224
|
// Read local project link for stored mode and project ID
|
|
989
1225
|
const link = await readProjectLink(projectPath);
|
|
990
1226
|
|
|
991
|
-
// Determine effective mode: explicit flag > stored mode > default BYO
|
|
1227
|
+
// Determine effective mode: explicit flag > auto-detect > stored mode > default BYO
|
|
992
1228
|
let deployMode: DeployMode;
|
|
993
1229
|
if (options.managed) {
|
|
994
1230
|
deployMode = "managed";
|
|
995
1231
|
} else if (options.byo) {
|
|
996
1232
|
deployMode = "byo";
|
|
1233
|
+
} else if (autoDetectResult) {
|
|
1234
|
+
deployMode = autoDetectResult.deployMode;
|
|
997
1235
|
} else {
|
|
998
1236
|
deployMode = link?.deploy_mode ?? "byo";
|
|
999
1237
|
}
|
|
@@ -1010,7 +1248,11 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
|
|
|
1010
1248
|
// Deploy based on mode
|
|
1011
1249
|
if (deployMode === "managed") {
|
|
1012
1250
|
// Managed mode: deploy via jack cloud
|
|
1013
|
-
if
|
|
1251
|
+
// Use autoDetectResult.projectId if available, otherwise require existing link
|
|
1252
|
+
const managedProjectId = autoDetectResult?.projectId ?? link?.project_id;
|
|
1253
|
+
|
|
1254
|
+
// For dry run, skip the project ID check since we didn't create a cloud project
|
|
1255
|
+
if (!dryRun && (!managedProjectId || (!autoDetectResult && link?.deploy_mode !== "managed"))) {
|
|
1014
1256
|
throw new JackError(
|
|
1015
1257
|
JackErrorCode.VALIDATION_ERROR,
|
|
1016
1258
|
"Project not linked to jack cloud",
|
|
@@ -1018,11 +1260,43 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
|
|
|
1018
1260
|
);
|
|
1019
1261
|
}
|
|
1020
1262
|
|
|
1263
|
+
// Dry run: build for validation then stop before actual deployment
|
|
1264
|
+
// (deployToManagedProject handles its own build, so only build here for dry-run)
|
|
1265
|
+
if (dryRun) {
|
|
1266
|
+
if (await needsOpenNextBuild(projectPath)) {
|
|
1267
|
+
const buildSpin = reporter.spinner("Building...");
|
|
1268
|
+
try {
|
|
1269
|
+
await runOpenNextBuild(projectPath);
|
|
1270
|
+
buildSpin.success("Built");
|
|
1271
|
+
} catch (err) {
|
|
1272
|
+
buildSpin.error("Build failed");
|
|
1273
|
+
throw err;
|
|
1274
|
+
}
|
|
1275
|
+
} else if (await needsViteBuild(projectPath)) {
|
|
1276
|
+
const buildSpin = reporter.spinner("Building...");
|
|
1277
|
+
try {
|
|
1278
|
+
await runViteBuild(projectPath);
|
|
1279
|
+
buildSpin.success("Built");
|
|
1280
|
+
} catch (err) {
|
|
1281
|
+
buildSpin.error("Build failed");
|
|
1282
|
+
throw err;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
reporter.success("Dry run complete - config generated, build verified");
|
|
1286
|
+
return {
|
|
1287
|
+
workerUrl: null,
|
|
1288
|
+
projectName,
|
|
1289
|
+
deployMode,
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1021
1293
|
// deployToManagedProject now handles both template and code deploy
|
|
1022
|
-
await deployToManagedProject(
|
|
1294
|
+
await deployToManagedProject(managedProjectId as string, projectPath, reporter);
|
|
1023
1295
|
|
|
1024
|
-
//
|
|
1025
|
-
workerUrl =
|
|
1296
|
+
// Construct URL with username if available
|
|
1297
|
+
workerUrl = link?.owner_username
|
|
1298
|
+
? `https://${link.owner_username}-${projectName}.runjack.xyz`
|
|
1299
|
+
: `https://${projectName}.runjack.xyz`;
|
|
1026
1300
|
} else {
|
|
1027
1301
|
// BYO mode: deploy via wrangler
|
|
1028
1302
|
|
|
@@ -1047,15 +1321,32 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
|
|
|
1047
1321
|
}
|
|
1048
1322
|
}
|
|
1049
1323
|
|
|
1050
|
-
//
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1324
|
+
// Dry run: stop before actual deployment
|
|
1325
|
+
if (dryRun) {
|
|
1326
|
+
reporter.success("Dry run complete - build verified");
|
|
1327
|
+
return {
|
|
1328
|
+
workerUrl: null,
|
|
1329
|
+
projectName,
|
|
1330
|
+
deployMode,
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
// Check wrangler version for auto-provisioning (KV/R2/D1 without IDs)
|
|
1335
|
+
const config = await parseWranglerConfig(projectPath);
|
|
1336
|
+
const needsAutoProvision =
|
|
1337
|
+
config.kv_namespaces?.some((kv) => !kv.id) ||
|
|
1338
|
+
config.r2_buckets?.some((r2) => r2.bucket_name?.startsWith("jack-template-")) ||
|
|
1339
|
+
config.d1_databases?.some((d1) => !d1.database_id);
|
|
1340
|
+
|
|
1341
|
+
if (needsAutoProvision) {
|
|
1342
|
+
try {
|
|
1343
|
+
const wranglerVersion = await getWranglerVersion();
|
|
1344
|
+
checkWranglerVersion(wranglerVersion);
|
|
1345
|
+
} catch (err) {
|
|
1346
|
+
if (err instanceof JackError) {
|
|
1347
|
+
throw err;
|
|
1348
|
+
}
|
|
1055
1349
|
}
|
|
1056
|
-
} catch (err) {
|
|
1057
|
-
// Non-fatal: let wrangler deploy fail with a clearer error if bucket is missing
|
|
1058
|
-
debug("R2 preflight failed:", err);
|
|
1059
1350
|
}
|
|
1060
1351
|
|
|
1061
1352
|
const spin = reporter.spinner("Deploying...");
|
|
@@ -1134,7 +1425,11 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
|
|
|
1134
1425
|
if (!existingLink) {
|
|
1135
1426
|
// Not linked yet - create link
|
|
1136
1427
|
if (deployMode === "managed" && link?.project_id) {
|
|
1137
|
-
|
|
1428
|
+
// Fetch username for link storage
|
|
1429
|
+
const { getCurrentUserProfile } = await import("./control-plane.ts");
|
|
1430
|
+
const profile = await getCurrentUserProfile();
|
|
1431
|
+
const ownerUsername = profile?.username ?? undefined;
|
|
1432
|
+
await linkProject(projectPath, link.project_id, "managed", ownerUsername);
|
|
1138
1433
|
await registerPath(link.project_id, projectPath);
|
|
1139
1434
|
} else {
|
|
1140
1435
|
// BYO mode - generate new ID
|
|
@@ -1216,7 +1511,9 @@ export async function getProjectStatus(
|
|
|
1216
1511
|
// Determine URL based on mode
|
|
1217
1512
|
let workerUrl: string | null = null;
|
|
1218
1513
|
if (link?.deploy_mode === "managed") {
|
|
1219
|
-
workerUrl =
|
|
1514
|
+
workerUrl = link.owner_username
|
|
1515
|
+
? `https://${link.owner_username}-${projectName}.runjack.xyz`
|
|
1516
|
+
: `https://${projectName}.runjack.xyz`;
|
|
1220
1517
|
}
|
|
1221
1518
|
|
|
1222
1519
|
// Get database name on-demand
|
|
@@ -121,11 +121,15 @@ function fromManagedProject(managed: ManagedProject): ResolvedProject {
|
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
const url = managed.owner_username
|
|
125
|
+
? `https://${managed.owner_username}-${managed.slug}.runjack.xyz`
|
|
126
|
+
: `https://${managed.slug}.runjack.xyz`;
|
|
127
|
+
|
|
124
128
|
return {
|
|
125
129
|
name: managed.name,
|
|
126
130
|
slug: managed.slug,
|
|
127
131
|
status,
|
|
128
|
-
url
|
|
132
|
+
url,
|
|
129
133
|
errorMessage: managed.status === "error" ? "deployment failed" : undefined,
|
|
130
134
|
sources: {
|
|
131
135
|
controlPlane: true,
|
|
@@ -25,6 +25,7 @@ export const DEFAULT_INCLUDES: string[] = [
|
|
|
25
25
|
"*.mjs",
|
|
26
26
|
"*.cjs",
|
|
27
27
|
"*.json",
|
|
28
|
+
"*.jsonc",
|
|
28
29
|
"*.toml",
|
|
29
30
|
"*.md",
|
|
30
31
|
"*.css",
|
|
@@ -48,6 +49,10 @@ export const DEFAULT_EXCLUDES: string[] = [
|
|
|
48
49
|
".DS_Store",
|
|
49
50
|
"dist/**",
|
|
50
51
|
"build/**",
|
|
52
|
+
".next/**",
|
|
53
|
+
".nuxt/**",
|
|
54
|
+
".output/**",
|
|
55
|
+
".svelte-kit/**",
|
|
51
56
|
"coverage/**",
|
|
52
57
|
".wrangler/**",
|
|
53
58
|
"*.lock",
|
package/src/lib/telemetry.ts
CHANGED
|
@@ -28,6 +28,10 @@ export const Events = {
|
|
|
28
28
|
MANAGED_DEPLOY_STARTED: "managed_deploy_started",
|
|
29
29
|
MANAGED_DEPLOY_COMPLETED: "managed_deploy_completed",
|
|
30
30
|
MANAGED_DEPLOY_FAILED: "managed_deploy_failed",
|
|
31
|
+
// Auto-detect events
|
|
32
|
+
AUTO_DETECT_SUCCESS: "auto_detect_success",
|
|
33
|
+
AUTO_DETECT_FAILED: "auto_detect_failed",
|
|
34
|
+
AUTO_DETECT_REJECTED: "auto_detect_rejected",
|
|
31
35
|
} as const;
|
|
32
36
|
|
|
33
37
|
type EventName = (typeof Events)[keyof typeof Events];
|
package/src/lib/zip-packager.ts
CHANGED
|
@@ -42,6 +42,7 @@ export interface ManifestData {
|
|
|
42
42
|
};
|
|
43
43
|
vars?: Record<string, string>;
|
|
44
44
|
r2?: Array<{ binding: string; bucket_name: string }>;
|
|
45
|
+
kv?: Array<{ binding: string }>;
|
|
45
46
|
};
|
|
46
47
|
}
|
|
47
48
|
|
|
@@ -169,6 +170,13 @@ function extractBindingsFromConfig(config?: WranglerConfig): ManifestData["bindi
|
|
|
169
170
|
}));
|
|
170
171
|
}
|
|
171
172
|
|
|
173
|
+
// Extract KV namespace bindings
|
|
174
|
+
if (config.kv_namespaces && config.kv_namespaces.length > 0) {
|
|
175
|
+
bindings.kv = config.kv_namespaces.map((kv) => ({
|
|
176
|
+
binding: kv.binding,
|
|
177
|
+
}));
|
|
178
|
+
}
|
|
179
|
+
|
|
172
180
|
// Return undefined if no bindings were extracted
|
|
173
181
|
return Object.keys(bindings).length > 0 ? bindings : undefined;
|
|
174
182
|
}
|
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
|
|