@getjack/jack 0.1.17 → 0.1.20
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 -5
- package/src/commands/clone.ts +62 -40
- package/src/commands/down.ts +11 -1
- package/src/commands/init.ts +21 -2
- package/src/commands/services.ts +85 -4
- package/src/commands/sync.ts +9 -0
- package/src/commands/update.ts +10 -0
- package/src/lib/agents.ts +3 -1
- package/src/lib/auth/ensure-auth.test.ts +3 -3
- package/src/lib/control-plane.ts +77 -1
- package/src/lib/deploy-upload.ts +26 -1
- package/src/lib/hooks.ts +232 -1
- package/src/lib/managed-deploy.ts +37 -6
- package/src/lib/output.ts +21 -3
- package/src/lib/progress.ts +231 -0
- package/src/lib/project-list.ts +6 -1
- package/src/lib/project-operations.ts +117 -62
- package/src/lib/project-resolver.ts +1 -1
- package/src/lib/services/db-create.ts +6 -3
- package/src/lib/version-check.ts +14 -0
- package/src/lib/wrangler-config.ts +190 -0
- package/src/lib/zip-packager.ts +74 -7
- package/src/lib/zip-utils.ts +38 -0
- package/src/templates/index.ts +1 -1
- package/src/templates/types.ts +16 -0
- package/templates/CLAUDE.md +103 -0
- package/templates/miniapp/.jack.json +1 -3
- package/templates/saas/.jack.json +154 -0
- package/templates/saas/AGENTS.md +333 -0
- package/templates/saas/bun.lock +925 -0
- package/templates/saas/components.json +21 -0
- package/templates/saas/index.html +12 -0
- package/templates/saas/package.json +75 -0
- package/templates/saas/public/icon.png +0 -0
- package/templates/saas/public/og.png +0 -0
- package/templates/saas/schema.sql +73 -0
- package/templates/saas/src/auth.ts +77 -0
- package/templates/saas/src/client/App.tsx +63 -0
- package/templates/saas/src/client/components/ProtectedRoute.tsx +29 -0
- package/templates/saas/src/client/components/ThemeToggle.tsx +32 -0
- package/templates/saas/src/client/components/ui/accordion.tsx +62 -0
- package/templates/saas/src/client/components/ui/alert-dialog.tsx +133 -0
- package/templates/saas/src/client/components/ui/alert.tsx +60 -0
- package/templates/saas/src/client/components/ui/aspect-ratio.tsx +9 -0
- package/templates/saas/src/client/components/ui/avatar.tsx +39 -0
- package/templates/saas/src/client/components/ui/badge.tsx +39 -0
- package/templates/saas/src/client/components/ui/breadcrumb.tsx +102 -0
- package/templates/saas/src/client/components/ui/button-group.tsx +78 -0
- package/templates/saas/src/client/components/ui/button.tsx +60 -0
- package/templates/saas/src/client/components/ui/card.tsx +75 -0
- package/templates/saas/src/client/components/ui/carousel.tsx +228 -0
- package/templates/saas/src/client/components/ui/chart.tsx +326 -0
- package/templates/saas/src/client/components/ui/checkbox.tsx +29 -0
- package/templates/saas/src/client/components/ui/collapsible.tsx +19 -0
- package/templates/saas/src/client/components/ui/command.tsx +159 -0
- package/templates/saas/src/client/components/ui/context-menu.tsx +224 -0
- package/templates/saas/src/client/components/ui/dialog.tsx +127 -0
- package/templates/saas/src/client/components/ui/drawer.tsx +124 -0
- package/templates/saas/src/client/components/ui/dropdown-menu.tsx +226 -0
- package/templates/saas/src/client/components/ui/empty.tsx +94 -0
- package/templates/saas/src/client/components/ui/field.tsx +232 -0
- package/templates/saas/src/client/components/ui/form.tsx +152 -0
- package/templates/saas/src/client/components/ui/hover-card.tsx +38 -0
- package/templates/saas/src/client/components/ui/input-group.tsx +158 -0
- package/templates/saas/src/client/components/ui/input-otp.tsx +68 -0
- package/templates/saas/src/client/components/ui/input.tsx +21 -0
- package/templates/saas/src/client/components/ui/item.tsx +172 -0
- package/templates/saas/src/client/components/ui/kbd.tsx +28 -0
- package/templates/saas/src/client/components/ui/label.tsx +21 -0
- package/templates/saas/src/client/components/ui/menubar.tsx +250 -0
- package/templates/saas/src/client/components/ui/navigation-menu.tsx +161 -0
- package/templates/saas/src/client/components/ui/pagination.tsx +106 -0
- package/templates/saas/src/client/components/ui/popover.tsx +42 -0
- package/templates/saas/src/client/components/ui/progress.tsx +26 -0
- package/templates/saas/src/client/components/ui/radio-group.tsx +45 -0
- package/templates/saas/src/client/components/ui/resizable.tsx +46 -0
- package/templates/saas/src/client/components/ui/scroll-area.tsx +56 -0
- package/templates/saas/src/client/components/ui/select.tsx +173 -0
- package/templates/saas/src/client/components/ui/separator.tsx +28 -0
- package/templates/saas/src/client/components/ui/sheet.tsx +128 -0
- package/templates/saas/src/client/components/ui/sidebar.tsx +694 -0
- package/templates/saas/src/client/components/ui/skeleton.tsx +13 -0
- package/templates/saas/src/client/components/ui/slider.tsx +58 -0
- package/templates/saas/src/client/components/ui/sonner.tsx +38 -0
- package/templates/saas/src/client/components/ui/spinner.tsx +16 -0
- package/templates/saas/src/client/components/ui/switch.tsx +28 -0
- package/templates/saas/src/client/components/ui/table.tsx +90 -0
- package/templates/saas/src/client/components/ui/tabs.tsx +54 -0
- package/templates/saas/src/client/components/ui/textarea.tsx +18 -0
- package/templates/saas/src/client/components/ui/toggle-group.tsx +80 -0
- package/templates/saas/src/client/components/ui/toggle.tsx +44 -0
- package/templates/saas/src/client/components/ui/tooltip.tsx +57 -0
- package/templates/saas/src/client/hooks/use-mobile.ts +19 -0
- package/templates/saas/src/client/hooks/useAuth.ts +14 -0
- package/templates/saas/src/client/hooks/useSubscription.ts +86 -0
- package/templates/saas/src/client/index.css +165 -0
- package/templates/saas/src/client/lib/auth-client.ts +7 -0
- package/templates/saas/src/client/lib/plans.ts +82 -0
- package/templates/saas/src/client/lib/utils.ts +6 -0
- package/templates/saas/src/client/main.tsx +15 -0
- package/templates/saas/src/client/pages/DashboardPage.tsx +394 -0
- package/templates/saas/src/client/pages/ForgotPasswordPage.tsx +153 -0
- package/templates/saas/src/client/pages/HomePage.tsx +285 -0
- package/templates/saas/src/client/pages/LoginPage.tsx +169 -0
- package/templates/saas/src/client/pages/PricingPage.tsx +467 -0
- package/templates/saas/src/client/pages/ResetPasswordPage.tsx +200 -0
- package/templates/saas/src/client/pages/SignupPage.tsx +192 -0
- package/templates/saas/src/index.ts +208 -0
- package/templates/saas/tsconfig.json +18 -0
- package/templates/saas/vite.config.ts +14 -0
- package/templates/saas/wrangler.jsonc +20 -0
|
@@ -39,8 +39,7 @@ import {
|
|
|
39
39
|
slugify,
|
|
40
40
|
writeWranglerConfig,
|
|
41
41
|
} from "./config-generator.ts";
|
|
42
|
-
import {
|
|
43
|
-
import { deleteManagedProject } from "./control-plane.ts";
|
|
42
|
+
import { deleteManagedProject, listManagedProjects } from "./control-plane.ts";
|
|
44
43
|
import { debug, isDebug, printTimingSummary, timerEnd, timerStart } from "./debug.ts";
|
|
45
44
|
import { ensureWranglerInstalled, validateModeAvailability } from "./deploy-mode.ts";
|
|
46
45
|
import { detectSecrets, generateEnvFile, generateSecretsJson } from "./env-parser.ts";
|
|
@@ -67,7 +66,7 @@ import {
|
|
|
67
66
|
import { filterNewSecrets, promptSaveSecrets } from "./prompts.ts";
|
|
68
67
|
import { applySchema, getD1Bindings, getD1DatabaseName, hasD1Config } from "./schema.ts";
|
|
69
68
|
import { getSavedSecrets, saveSecrets } from "./secrets.ts";
|
|
70
|
-
import { getProjectNameFromDir, getRemoteManifest
|
|
69
|
+
import { getProjectNameFromDir, getRemoteManifest } from "./storage/index.ts";
|
|
71
70
|
import { Events, track } from "./telemetry.ts";
|
|
72
71
|
|
|
73
72
|
// ============================================================================
|
|
@@ -364,7 +363,11 @@ async function runParallelSetup(
|
|
|
364
363
|
installSuccess: boolean;
|
|
365
364
|
remoteResult: ManagedCreateResult;
|
|
366
365
|
}> {
|
|
366
|
+
const setupStart = Date.now();
|
|
367
|
+
debug("Parallel setup started", { template: options.template, usePrebuilt: options.usePrebuilt });
|
|
368
|
+
|
|
367
369
|
// Start both operations
|
|
370
|
+
const installStart = Date.now();
|
|
368
371
|
const installPromise = (async () => {
|
|
369
372
|
const install = Bun.spawn(["bun", "install", "--prefer-offline"], {
|
|
370
373
|
cwd: targetDir,
|
|
@@ -372,15 +375,22 @@ async function runParallelSetup(
|
|
|
372
375
|
stderr: "ignore",
|
|
373
376
|
});
|
|
374
377
|
await install.exited;
|
|
378
|
+
const duration = ((Date.now() - installStart) / 1000).toFixed(1);
|
|
379
|
+
debug(`bun install completed in ${duration}s (exit: ${install.exitCode})`);
|
|
375
380
|
if (install.exitCode !== 0) {
|
|
376
381
|
throw new Error("Dependency installation failed");
|
|
377
382
|
}
|
|
378
383
|
return true;
|
|
379
384
|
})();
|
|
380
385
|
|
|
386
|
+
const remoteStart = Date.now();
|
|
381
387
|
const remotePromise = createManagedProjectRemote(projectName, undefined, {
|
|
382
388
|
template: options.template || "hello",
|
|
383
389
|
usePrebuilt: options.usePrebuilt ?? true,
|
|
390
|
+
}).then((result) => {
|
|
391
|
+
const duration = ((Date.now() - remoteStart) / 1000).toFixed(1);
|
|
392
|
+
debug(`Remote project created in ${duration}s (status: ${result.status})`);
|
|
393
|
+
return result;
|
|
384
394
|
});
|
|
385
395
|
|
|
386
396
|
// Report URL as soon as remote is ready (don't wait for install)
|
|
@@ -435,6 +445,9 @@ async function runParallelSetup(
|
|
|
435
445
|
throw new Error("Unexpected state: remote result not fulfilled");
|
|
436
446
|
}
|
|
437
447
|
|
|
448
|
+
const totalDuration = ((Date.now() - setupStart) / 1000).toFixed(1);
|
|
449
|
+
debug(`Parallel setup completed in ${totalDuration}s`);
|
|
450
|
+
|
|
438
451
|
return {
|
|
439
452
|
installSuccess: true,
|
|
440
453
|
remoteResult: remoteResult.value,
|
|
@@ -973,6 +986,25 @@ export async function createProject(
|
|
|
973
986
|
const rendered = renderTemplate(template, { name: projectName });
|
|
974
987
|
timings.push({ label: "Template load", duration: timerEnd("template-load") });
|
|
975
988
|
|
|
989
|
+
// Run preCreate hooks (for interactive secret collection, auto-generation, etc.)
|
|
990
|
+
if (template.hooks?.preCreate?.length) {
|
|
991
|
+
timerStart("pre-create-hooks");
|
|
992
|
+
const hookContext = { projectName, projectDir: targetDir };
|
|
993
|
+
const hookResult = await runHook(template.hooks.preCreate, hookContext, {
|
|
994
|
+
interactive,
|
|
995
|
+
output: reporter,
|
|
996
|
+
});
|
|
997
|
+
timings.push({ label: "Pre-create hooks", duration: timerEnd("pre-create-hooks") });
|
|
998
|
+
|
|
999
|
+
if (!hookResult.success) {
|
|
1000
|
+
throw new JackError(
|
|
1001
|
+
JackErrorCode.VALIDATION_ERROR,
|
|
1002
|
+
"Project setup incomplete",
|
|
1003
|
+
"Missing required configuration",
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
976
1008
|
// Handle template-specific secrets
|
|
977
1009
|
const secretsToUse: Record<string, string> = {};
|
|
978
1010
|
if (template.secrets?.length) {
|
|
@@ -1018,41 +1050,34 @@ export async function createProject(
|
|
|
1018
1050
|
continue;
|
|
1019
1051
|
}
|
|
1020
1052
|
|
|
1021
|
-
// Prompt user
|
|
1053
|
+
// Prompt user - single text input, empty/Esc to skip
|
|
1022
1054
|
reporter.stop();
|
|
1023
|
-
const { isCancel,
|
|
1055
|
+
const { isCancel, text } = await import("@clack/prompts");
|
|
1024
1056
|
console.error("");
|
|
1025
1057
|
console.error(` ${optionalSecret.description}`);
|
|
1026
1058
|
if (optionalSecret.setupUrl) {
|
|
1027
|
-
console.error(`
|
|
1059
|
+
console.error(` Get it at: \x1b[36m${optionalSecret.setupUrl}\x1b[0m`);
|
|
1028
1060
|
}
|
|
1029
1061
|
console.error("");
|
|
1030
1062
|
|
|
1031
|
-
const
|
|
1032
|
-
message:
|
|
1033
|
-
|
|
1034
|
-
{ label: "Yes", value: "yes" },
|
|
1035
|
-
{ label: "Skip", value: "skip" },
|
|
1036
|
-
],
|
|
1063
|
+
const value = await text({
|
|
1064
|
+
message: `${optionalSecret.name}:`,
|
|
1065
|
+
placeholder: "paste value or press Esc to skip",
|
|
1037
1066
|
});
|
|
1038
1067
|
|
|
1039
|
-
if (!isCancel(
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
},
|
|
1053
|
-
]);
|
|
1054
|
-
reporter.success(`Saved ${optionalSecret.name}`);
|
|
1055
|
-
}
|
|
1068
|
+
if (!isCancel(value) && value.trim()) {
|
|
1069
|
+
secretsToUse[optionalSecret.name] = value.trim();
|
|
1070
|
+
// Save to global secrets for reuse
|
|
1071
|
+
await saveSecrets([
|
|
1072
|
+
{
|
|
1073
|
+
key: optionalSecret.name,
|
|
1074
|
+
value: value.trim(),
|
|
1075
|
+
source: "optional-template",
|
|
1076
|
+
},
|
|
1077
|
+
]);
|
|
1078
|
+
reporter.success(`Saved ${optionalSecret.name}`);
|
|
1079
|
+
} else {
|
|
1080
|
+
reporter.info(`Skipped ${optionalSecret.name}`);
|
|
1056
1081
|
}
|
|
1057
1082
|
|
|
1058
1083
|
reporter.start("Creating project...");
|
|
@@ -1309,6 +1334,23 @@ export async function createProject(
|
|
|
1309
1334
|
if (!urlShownEarly) {
|
|
1310
1335
|
reporter.success(`Deployed: ${workerUrl}`);
|
|
1311
1336
|
}
|
|
1337
|
+
|
|
1338
|
+
// Upload source snapshot for forking (prebuilt path needs this too)
|
|
1339
|
+
try {
|
|
1340
|
+
const { createSourceZip } = await import("./zip-packager.ts");
|
|
1341
|
+
const { uploadSourceSnapshot } = await import("./control-plane.ts");
|
|
1342
|
+
const { rm } = await import("node:fs/promises");
|
|
1343
|
+
|
|
1344
|
+
const sourceZipPath = await createSourceZip(targetDir);
|
|
1345
|
+
await uploadSourceSnapshot(remoteResult.projectId, sourceZipPath);
|
|
1346
|
+
await rm(sourceZipPath, { force: true });
|
|
1347
|
+
debug("Source snapshot uploaded for prebuilt project");
|
|
1348
|
+
} catch (err) {
|
|
1349
|
+
debug(
|
|
1350
|
+
"Source snapshot upload failed (prebuilt):",
|
|
1351
|
+
err instanceof Error ? err.message : String(err),
|
|
1352
|
+
);
|
|
1353
|
+
}
|
|
1312
1354
|
} else {
|
|
1313
1355
|
// Prebuilt not available - fall back to fresh build
|
|
1314
1356
|
if (remoteResult.prebuiltFailed) {
|
|
@@ -1769,28 +1811,9 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
|
|
|
1769
1811
|
}
|
|
1770
1812
|
}
|
|
1771
1813
|
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
const syncSpin = reporter.spinner("Syncing source to jack storage...");
|
|
1776
|
-
try {
|
|
1777
|
-
const syncResult = await syncToCloud(projectPath);
|
|
1778
|
-
if (syncResult.success) {
|
|
1779
|
-
if (syncResult.filesUploaded > 0 || syncResult.filesDeleted > 0) {
|
|
1780
|
-
syncSpin.success(
|
|
1781
|
-
`Synced source to jack storage (${syncResult.filesUploaded} uploaded, ${syncResult.filesDeleted} removed)`,
|
|
1782
|
-
);
|
|
1783
|
-
} else {
|
|
1784
|
-
syncSpin.success("Source already synced");
|
|
1785
|
-
}
|
|
1786
|
-
}
|
|
1787
|
-
} catch {
|
|
1788
|
-
syncSpin.stop();
|
|
1789
|
-
reporter.warn("Cloud sync failed (deploy succeeded)");
|
|
1790
|
-
reporter.info("Run: jack sync");
|
|
1791
|
-
}
|
|
1792
|
-
}
|
|
1793
|
-
}
|
|
1814
|
+
// Note: Auto-sync to User R2 was removed for managed mode.
|
|
1815
|
+
// Managed projects use control-plane source.zip for clone instead.
|
|
1816
|
+
// BYO users can run 'jack sync' manually if needed.
|
|
1794
1817
|
|
|
1795
1818
|
// Ensure project is linked locally for discovery
|
|
1796
1819
|
try {
|
|
@@ -1936,7 +1959,9 @@ export async function getProjectStatus(
|
|
|
1936
1959
|
|
|
1937
1960
|
/**
|
|
1938
1961
|
* Scan for stale project paths.
|
|
1939
|
-
* Checks for
|
|
1962
|
+
* Checks for:
|
|
1963
|
+
* 1. Paths in the index that no longer have wrangler config (dir deleted/moved)
|
|
1964
|
+
* 2. Managed projects where the cloud project no longer exists (orphaned links)
|
|
1940
1965
|
* Returns total project count and stale entries with reasons.
|
|
1941
1966
|
*/
|
|
1942
1967
|
export async function scanStaleProjects(): Promise<StaleProjectScan> {
|
|
@@ -1945,31 +1970,61 @@ export async function scanStaleProjects(): Promise<StaleProjectScan> {
|
|
|
1945
1970
|
const stale: StaleProject[] = [];
|
|
1946
1971
|
let totalPaths = 0;
|
|
1947
1972
|
|
|
1973
|
+
// Get list of valid managed project IDs (if logged in)
|
|
1974
|
+
let validManagedIds: Set<string> = new Set();
|
|
1975
|
+
try {
|
|
1976
|
+
const { isLoggedIn } = await import("./auth/store.ts");
|
|
1977
|
+
if (await isLoggedIn()) {
|
|
1978
|
+
const managedProjects = await listManagedProjects();
|
|
1979
|
+
validManagedIds = new Set(managedProjects.map((p) => p.id));
|
|
1980
|
+
}
|
|
1981
|
+
} catch {
|
|
1982
|
+
// Control plane unavailable, skip orphan detection
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1948
1985
|
for (const projectId of projectIds) {
|
|
1949
1986
|
const paths = allPaths[projectId] || [];
|
|
1950
1987
|
totalPaths += paths.length;
|
|
1951
1988
|
|
|
1952
1989
|
for (const projectPath of paths) {
|
|
1953
|
-
// Check if path exists and has valid
|
|
1990
|
+
// Check if path exists and has valid wrangler config
|
|
1954
1991
|
const hasWranglerConfig =
|
|
1955
1992
|
existsSync(join(projectPath, "wrangler.jsonc")) ||
|
|
1956
1993
|
existsSync(join(projectPath, "wrangler.toml")) ||
|
|
1957
1994
|
existsSync(join(projectPath, "wrangler.json"));
|
|
1958
1995
|
|
|
1959
1996
|
if (!hasWranglerConfig) {
|
|
1960
|
-
//
|
|
1961
|
-
|
|
1962
|
-
try {
|
|
1963
|
-
name = await getProjectNameFromDir(projectPath);
|
|
1964
|
-
} catch {
|
|
1965
|
-
// Use path basename as fallback
|
|
1966
|
-
}
|
|
1967
|
-
|
|
1997
|
+
// Type 1: No wrangler config at path (dir deleted/moved)
|
|
1998
|
+
const name = projectPath.split("/").pop() || projectId;
|
|
1968
1999
|
stale.push({
|
|
1969
2000
|
name,
|
|
1970
|
-
reason: "
|
|
2001
|
+
reason: "directory missing or no wrangler config",
|
|
1971
2002
|
workerUrl: null,
|
|
1972
2003
|
});
|
|
2004
|
+
continue;
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
// Check for Type 2: Managed project link pointing to deleted cloud project
|
|
2008
|
+
try {
|
|
2009
|
+
const link = await readProjectLink(projectPath);
|
|
2010
|
+
if (link?.deploy_mode === "managed" && validManagedIds.size > 0) {
|
|
2011
|
+
if (!validManagedIds.has(link.project_id)) {
|
|
2012
|
+
// Orphaned managed link - cloud project doesn't exist
|
|
2013
|
+
let name = projectPath.split("/").pop() || projectId;
|
|
2014
|
+
try {
|
|
2015
|
+
name = await getProjectNameFromDir(projectPath);
|
|
2016
|
+
} catch {
|
|
2017
|
+
// Use path basename as fallback
|
|
2018
|
+
}
|
|
2019
|
+
stale.push({
|
|
2020
|
+
name,
|
|
2021
|
+
reason: "cloud project deleted",
|
|
2022
|
+
workerUrl: null,
|
|
2023
|
+
});
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
} catch {
|
|
2027
|
+
// Can't read link, skip
|
|
1973
2028
|
}
|
|
1974
2029
|
}
|
|
1975
2030
|
}
|
|
@@ -382,7 +382,7 @@ export async function listAllProjects(): Promise<ResolvedProject[]> {
|
|
|
382
382
|
!project.sources.controlPlane
|
|
383
383
|
) {
|
|
384
384
|
project.status = "error";
|
|
385
|
-
project.errorMessage = "Project not found in jack cloud
|
|
385
|
+
project.errorMessage = "Project not found in jack cloud";
|
|
386
386
|
}
|
|
387
387
|
}
|
|
388
388
|
} catch {
|
|
@@ -155,6 +155,8 @@ export async function createDatabase(
|
|
|
155
155
|
let databaseId: string;
|
|
156
156
|
let created = true;
|
|
157
157
|
|
|
158
|
+
let actualDatabaseName = databaseName;
|
|
159
|
+
|
|
158
160
|
if (link.deploy_mode === "managed") {
|
|
159
161
|
// Managed mode: call control plane
|
|
160
162
|
// Note: Control plane will reuse existing DB if name matches
|
|
@@ -163,7 +165,8 @@ export async function createDatabase(
|
|
|
163
165
|
bindingName,
|
|
164
166
|
});
|
|
165
167
|
databaseId = resource.provider_id;
|
|
166
|
-
//
|
|
168
|
+
// Use the actual name from control plane (may differ from CLI-generated name)
|
|
169
|
+
actualDatabaseName = resource.resource_name;
|
|
167
170
|
} else {
|
|
168
171
|
// BYO mode: use wrangler d1 create (checks for existing first)
|
|
169
172
|
const result = await createDatabaseViaWrangler(databaseName);
|
|
@@ -174,12 +177,12 @@ export async function createDatabase(
|
|
|
174
177
|
// Update wrangler.jsonc with the new binding
|
|
175
178
|
await addD1Binding(wranglerPath, {
|
|
176
179
|
binding: bindingName,
|
|
177
|
-
database_name:
|
|
180
|
+
database_name: actualDatabaseName,
|
|
178
181
|
database_id: databaseId,
|
|
179
182
|
});
|
|
180
183
|
|
|
181
184
|
return {
|
|
182
|
-
databaseName,
|
|
185
|
+
databaseName: actualDatabaseName,
|
|
183
186
|
databaseId,
|
|
184
187
|
bindingName,
|
|
185
188
|
created,
|
package/src/lib/version-check.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { join } from "node:path";
|
|
|
7
7
|
import { $ } from "bun";
|
|
8
8
|
import pkg from "../../package.json";
|
|
9
9
|
import { CONFIG_DIR } from "./config.ts";
|
|
10
|
+
import { debug } from "./debug.ts";
|
|
10
11
|
|
|
11
12
|
const VERSION_CACHE_PATH = join(CONFIG_DIR, "version-cache.json");
|
|
12
13
|
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
@@ -123,8 +124,17 @@ export async function performUpdate(): Promise<{
|
|
|
123
124
|
}> {
|
|
124
125
|
try {
|
|
125
126
|
// Run bun add -g to update
|
|
127
|
+
debug(`Executing: bun add -g ${PACKAGE_NAME}@latest`);
|
|
126
128
|
const result = await $`bun add -g ${PACKAGE_NAME}@latest`.nothrow().quiet();
|
|
127
129
|
|
|
130
|
+
debug(`Exit code: ${result.exitCode}`);
|
|
131
|
+
if (result.stdout.toString()) {
|
|
132
|
+
debug(`stdout: ${result.stdout.toString()}`);
|
|
133
|
+
}
|
|
134
|
+
if (result.stderr.toString()) {
|
|
135
|
+
debug(`stderr: ${result.stderr.toString()}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
128
138
|
if (result.exitCode !== 0) {
|
|
129
139
|
return {
|
|
130
140
|
success: false,
|
|
@@ -133,12 +143,15 @@ export async function performUpdate(): Promise<{
|
|
|
133
143
|
}
|
|
134
144
|
|
|
135
145
|
// Verify the new version
|
|
146
|
+
debug("Verifying installed version...");
|
|
136
147
|
const newVersionResult = await $`bun pm ls -g`.nothrow().quiet();
|
|
137
148
|
const output = newVersionResult.stdout.toString();
|
|
149
|
+
debug(`bun pm ls -g output: ${output.slice(0, 500)}`);
|
|
138
150
|
|
|
139
151
|
// Try to extract version from output
|
|
140
152
|
const versionMatch = output.match(/@getjack\/jack@(\d+\.\d+\.\d+)/);
|
|
141
153
|
const newVersion = versionMatch?.[1];
|
|
154
|
+
debug(`Extracted version: ${newVersion ?? "not found"}`);
|
|
142
155
|
|
|
143
156
|
// Clear version cache so next check gets fresh data
|
|
144
157
|
try {
|
|
@@ -152,6 +165,7 @@ export async function performUpdate(): Promise<{
|
|
|
152
165
|
version: newVersion,
|
|
153
166
|
};
|
|
154
167
|
} catch (err) {
|
|
168
|
+
debug(`Update error: ${err instanceof Error ? err.message : String(err)}`);
|
|
155
169
|
return {
|
|
156
170
|
success: false,
|
|
157
171
|
error: err instanceof Error ? err.message : "Unknown error",
|
|
@@ -457,3 +457,193 @@ function findLineCommentStart(line: string): number {
|
|
|
457
457
|
|
|
458
458
|
return -1;
|
|
459
459
|
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Remove a D1 database binding from wrangler.jsonc by database_name.
|
|
463
|
+
* Preserves comments and formatting.
|
|
464
|
+
*
|
|
465
|
+
* @returns true if binding was found and removed, false if not found
|
|
466
|
+
*/
|
|
467
|
+
export async function removeD1Binding(configPath: string, databaseName: string): Promise<boolean> {
|
|
468
|
+
if (!existsSync(configPath)) {
|
|
469
|
+
throw new Error(`wrangler.jsonc not found at ${configPath}. Cannot remove binding.`);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const content = await Bun.file(configPath).text();
|
|
473
|
+
|
|
474
|
+
// Parse to understand existing structure
|
|
475
|
+
const config = parseJsonc<WranglerConfig>(content);
|
|
476
|
+
|
|
477
|
+
// Check if d1_databases exists and has entries
|
|
478
|
+
if (!config.d1_databases || !Array.isArray(config.d1_databases)) {
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Find the binding to remove
|
|
483
|
+
const bindingIndex = config.d1_databases.findIndex((db) => db.database_name === databaseName);
|
|
484
|
+
|
|
485
|
+
if (bindingIndex === -1) {
|
|
486
|
+
return false; // Binding not found
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Use text manipulation to remove the binding while preserving formatting
|
|
490
|
+
const newContent = removeD1DatabaseEntryFromContent(content, databaseName);
|
|
491
|
+
|
|
492
|
+
if (newContent === content) {
|
|
493
|
+
return false; // Nothing changed
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
await Bun.write(configPath, newContent);
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Remove a specific D1 database entry from the d1_databases array in content.
|
|
502
|
+
* Handles comma placement and preserves comments.
|
|
503
|
+
*/
|
|
504
|
+
function removeD1DatabaseEntryFromContent(content: string, databaseName: string): string {
|
|
505
|
+
// Find the d1_databases array
|
|
506
|
+
const d1Match = content.match(/"d1_databases"\s*:\s*\[/);
|
|
507
|
+
if (!d1Match || d1Match.index === undefined) {
|
|
508
|
+
return content;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const arrayStartIndex = d1Match.index + d1Match[0].length;
|
|
512
|
+
const closingBracketIndex = findMatchingBracket(content, arrayStartIndex - 1, "[", "]");
|
|
513
|
+
|
|
514
|
+
if (closingBracketIndex === -1) {
|
|
515
|
+
return content;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const arrayContent = content.slice(arrayStartIndex, closingBracketIndex);
|
|
519
|
+
|
|
520
|
+
// Find the object containing this database_name
|
|
521
|
+
const escapedName = databaseName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
522
|
+
const dbNamePattern = new RegExp(`"database_name"\\s*:\\s*"${escapedName}"`);
|
|
523
|
+
|
|
524
|
+
const match = dbNamePattern.exec(arrayContent);
|
|
525
|
+
if (!match) {
|
|
526
|
+
return content;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Find the enclosing object boundaries
|
|
530
|
+
const matchPosInArray = match.index;
|
|
531
|
+
const objectStart = findObjectStartBefore(arrayContent, matchPosInArray);
|
|
532
|
+
const objectEnd = findObjectEndAfter(arrayContent, matchPosInArray);
|
|
533
|
+
|
|
534
|
+
if (objectStart === -1 || objectEnd === -1) {
|
|
535
|
+
return content;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Determine comma handling
|
|
539
|
+
let removeStart = objectStart;
|
|
540
|
+
let removeEnd = objectEnd + 1;
|
|
541
|
+
|
|
542
|
+
// Check for trailing comma after the object
|
|
543
|
+
const afterObject = arrayContent.slice(objectEnd + 1);
|
|
544
|
+
const trailingCommaMatch = afterObject.match(/^\s*,/);
|
|
545
|
+
|
|
546
|
+
// Check for leading comma before the object
|
|
547
|
+
const beforeObject = arrayContent.slice(0, objectStart);
|
|
548
|
+
const leadingCommaMatch = beforeObject.match(/,\s*$/);
|
|
549
|
+
|
|
550
|
+
if (trailingCommaMatch) {
|
|
551
|
+
// Remove trailing comma
|
|
552
|
+
removeEnd = objectEnd + 1 + trailingCommaMatch[0].length;
|
|
553
|
+
} else if (leadingCommaMatch) {
|
|
554
|
+
// Remove leading comma
|
|
555
|
+
removeStart = objectStart - leadingCommaMatch[0].length;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Build new array content
|
|
559
|
+
const newArrayContent = arrayContent.slice(0, removeStart) + arrayContent.slice(removeEnd);
|
|
560
|
+
|
|
561
|
+
// Check if array is now effectively empty (only whitespace/comments)
|
|
562
|
+
const trimmedArray = newArrayContent.replace(/\/\/[^\n]*/g, "").trim();
|
|
563
|
+
if (trimmedArray === "" || trimmedArray === "[]") {
|
|
564
|
+
// Remove the entire d1_databases property
|
|
565
|
+
return removeD1DatabasesProperty(content, d1Match.index, closingBracketIndex);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return content.slice(0, arrayStartIndex) + newArrayContent + content.slice(closingBracketIndex);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Find the start of the object (opening brace) before the given position.
|
|
573
|
+
*/
|
|
574
|
+
function findObjectStartBefore(content: string, fromPos: number): number {
|
|
575
|
+
let depth = 0;
|
|
576
|
+
for (let i = fromPos; i >= 0; i--) {
|
|
577
|
+
const char = content[i];
|
|
578
|
+
if (char === "}") depth++;
|
|
579
|
+
if (char === "{") {
|
|
580
|
+
if (depth === 0) return i;
|
|
581
|
+
depth--;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return -1;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Find the end of the object (closing brace) after the given position.
|
|
589
|
+
*/
|
|
590
|
+
function findObjectEndAfter(content: string, fromPos: number): number {
|
|
591
|
+
let depth = 0;
|
|
592
|
+
let inString = false;
|
|
593
|
+
let escaped = false;
|
|
594
|
+
|
|
595
|
+
for (let i = fromPos; i < content.length; i++) {
|
|
596
|
+
const char = content[i];
|
|
597
|
+
|
|
598
|
+
if (inString) {
|
|
599
|
+
if (escaped) {
|
|
600
|
+
escaped = false;
|
|
601
|
+
} else if (char === "\\") {
|
|
602
|
+
escaped = true;
|
|
603
|
+
} else if (char === '"') {
|
|
604
|
+
inString = false;
|
|
605
|
+
}
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (char === '"') {
|
|
610
|
+
inString = true;
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (char === "{") depth++;
|
|
615
|
+
if (char === "}") {
|
|
616
|
+
if (depth === 0) return i;
|
|
617
|
+
depth--;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return -1;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Remove the entire d1_databases property when it becomes empty.
|
|
625
|
+
*/
|
|
626
|
+
function removeD1DatabasesProperty(
|
|
627
|
+
content: string,
|
|
628
|
+
propertyStart: number,
|
|
629
|
+
arrayEnd: number,
|
|
630
|
+
): string {
|
|
631
|
+
let removeStart = propertyStart;
|
|
632
|
+
let removeEnd = arrayEnd + 1;
|
|
633
|
+
|
|
634
|
+
// Look backward for a comma to remove
|
|
635
|
+
const beforeProperty = content.slice(0, propertyStart);
|
|
636
|
+
const leadingCommaMatch = beforeProperty.match(/,\s*$/);
|
|
637
|
+
|
|
638
|
+
// Look forward for a trailing comma
|
|
639
|
+
const afterProperty = content.slice(arrayEnd + 1);
|
|
640
|
+
const trailingCommaMatch = afterProperty.match(/^\s*,/);
|
|
641
|
+
|
|
642
|
+
if (leadingCommaMatch) {
|
|
643
|
+
removeStart = propertyStart - leadingCommaMatch[0].length;
|
|
644
|
+
} else if (trailingCommaMatch) {
|
|
645
|
+
removeEnd = arrayEnd + 1 + trailingCommaMatch[0].length;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
return content.slice(0, removeStart) + content.slice(removeEnd);
|
|
649
|
+
}
|