@getjack/jack 0.1.31 → 0.1.33

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 (197) hide show
  1. package/package.json +1 -1
  2. package/src/commands/deploys.ts +95 -0
  3. package/src/commands/link.ts +8 -0
  4. package/src/commands/mcp.ts +179 -4
  5. package/src/commands/rollback.ts +53 -0
  6. package/src/commands/services.ts +100 -20
  7. package/src/commands/ship.ts +30 -3
  8. package/src/commands/tokens.ts +134 -0
  9. package/src/commands/whoami.ts +51 -12
  10. package/src/index.ts +33 -0
  11. package/src/lib/agent-files.ts +54 -4
  12. package/src/lib/agent-integration.ts +4 -166
  13. package/src/lib/auth/client.ts +11 -1
  14. package/src/lib/auth/guard.ts +1 -1
  15. package/src/lib/auth/store.ts +3 -0
  16. package/src/lib/claude-hooks-installer.ts +55 -0
  17. package/src/lib/control-plane.ts +78 -40
  18. package/src/lib/debug.ts +2 -1
  19. package/src/lib/deploy-upload.ts +6 -0
  20. package/src/lib/hooks.ts +3 -1
  21. package/src/lib/managed-deploy.ts +12 -9
  22. package/src/lib/project-link.ts +6 -0
  23. package/src/lib/project-operations.ts +68 -22
  24. package/src/lib/services/token-operations.ts +84 -0
  25. package/src/lib/telemetry.ts +6 -0
  26. package/src/mcp/README.md +1 -1
  27. package/src/mcp/resources/index.ts +174 -16
  28. package/src/mcp/server.ts +23 -0
  29. package/src/mcp/tools/index.ts +133 -17
  30. package/src/mcp/types.ts +1 -0
  31. package/src/mcp/utils.ts +2 -1
  32. package/src/templates/index.ts +25 -73
  33. package/templates/CLAUDE.md +41 -0
  34. package/templates/ai-chat/.jack.json +10 -5
  35. package/templates/ai-chat/bun.lock +50 -1
  36. package/templates/ai-chat/package.json +5 -0
  37. package/templates/ai-chat/public/app.js +73 -0
  38. package/templates/ai-chat/public/index.html +14 -197
  39. package/templates/ai-chat/schema.sql +14 -0
  40. package/templates/ai-chat/src/index.ts +86 -102
  41. package/templates/ai-chat/wrangler.jsonc +8 -1
  42. package/templates/cron/.jack.json +66 -0
  43. package/templates/cron/bun.lock +23 -0
  44. package/templates/cron/package.json +16 -0
  45. package/templates/cron/schema.sql +24 -0
  46. package/templates/cron/src/index.ts +117 -0
  47. package/templates/cron/src/jobs.ts +139 -0
  48. package/templates/cron/src/webhooks.ts +95 -0
  49. package/templates/cron/tsconfig.json +17 -0
  50. package/templates/cron/wrangler.jsonc +11 -0
  51. package/templates/miniapp/.jack.json +1 -1
  52. package/templates/nextjs/.jack.json +1 -1
  53. package/templates/nextjs-auth/.jack.json +44 -0
  54. package/templates/nextjs-auth/app/api/auth/[...all]/route.ts +11 -0
  55. package/templates/nextjs-auth/app/dashboard/loading.tsx +53 -0
  56. package/templates/nextjs-auth/app/dashboard/page.tsx +73 -0
  57. package/templates/nextjs-auth/app/error.tsx +44 -0
  58. package/templates/nextjs-auth/app/globals.css +1 -0
  59. package/templates/nextjs-auth/app/health/route.ts +3 -0
  60. package/templates/nextjs-auth/app/layout.tsx +24 -0
  61. package/templates/nextjs-auth/app/login/page.tsx +10 -0
  62. package/templates/nextjs-auth/app/page.tsx +86 -0
  63. package/templates/nextjs-auth/app/signup/page.tsx +10 -0
  64. package/templates/nextjs-auth/bun.lock +1065 -0
  65. package/templates/nextjs-auth/cloudflare-env.d.ts +8 -0
  66. package/templates/nextjs-auth/components/auth-form.tsx +191 -0
  67. package/templates/nextjs-auth/components/header.tsx +50 -0
  68. package/templates/nextjs-auth/components/user-menu.tsx +23 -0
  69. package/templates/nextjs-auth/lib/auth-client.ts +3 -0
  70. package/templates/nextjs-auth/lib/auth.ts +43 -0
  71. package/templates/nextjs-auth/lib/utils.ts +6 -0
  72. package/templates/nextjs-auth/middleware.ts +33 -0
  73. package/templates/nextjs-auth/next.config.ts +8 -0
  74. package/templates/nextjs-auth/open-next.config.ts +6 -0
  75. package/templates/nextjs-auth/package.json +33 -0
  76. package/templates/nextjs-auth/postcss.config.mjs +8 -0
  77. package/templates/nextjs-auth/schema.sql +49 -0
  78. package/templates/nextjs-auth/tsconfig.json +28 -0
  79. package/templates/nextjs-auth/wrangler.jsonc +23 -0
  80. package/templates/nextjs-clerk/.jack.json +54 -0
  81. package/templates/nextjs-clerk/app/dashboard/page.tsx +69 -0
  82. package/templates/nextjs-clerk/app/globals.css +1 -0
  83. package/templates/nextjs-clerk/app/health/route.ts +3 -0
  84. package/templates/nextjs-clerk/app/layout.tsx +26 -0
  85. package/templates/nextjs-clerk/app/page.tsx +86 -0
  86. package/templates/nextjs-clerk/app/sign-in/[[...sign-in]]/page.tsx +9 -0
  87. package/templates/nextjs-clerk/app/sign-up/[[...sign-up]]/page.tsx +9 -0
  88. package/templates/nextjs-clerk/bun.lock +1055 -0
  89. package/templates/nextjs-clerk/cloudflare-env.d.ts +3 -0
  90. package/templates/nextjs-clerk/components/header.tsx +40 -0
  91. package/templates/nextjs-clerk/lib/utils.ts +6 -0
  92. package/templates/nextjs-clerk/middleware.ts +18 -0
  93. package/templates/nextjs-clerk/next.config.ts +8 -0
  94. package/templates/nextjs-clerk/open-next.config.ts +6 -0
  95. package/templates/nextjs-clerk/package.json +31 -0
  96. package/templates/nextjs-clerk/postcss.config.mjs +8 -0
  97. package/templates/nextjs-clerk/tsconfig.json +28 -0
  98. package/templates/nextjs-clerk/wrangler.jsonc +17 -0
  99. package/templates/nextjs-shadcn/.jack.json +34 -0
  100. package/templates/nextjs-shadcn/app/dashboard/data.json +614 -0
  101. package/templates/nextjs-shadcn/app/dashboard/page.tsx +55 -0
  102. package/templates/nextjs-shadcn/app/globals.css +126 -0
  103. package/templates/nextjs-shadcn/app/health/route.ts +3 -0
  104. package/templates/nextjs-shadcn/app/layout.tsx +24 -0
  105. package/templates/nextjs-shadcn/app/login/page.tsx +19 -0
  106. package/templates/nextjs-shadcn/app/page.tsx +180 -0
  107. package/templates/nextjs-shadcn/app/showcase.tsx +1262 -0
  108. package/templates/nextjs-shadcn/bun.lock +1789 -0
  109. package/templates/nextjs-shadcn/cloudflare-env.d.ts +4 -0
  110. package/templates/nextjs-shadcn/components/app-sidebar.tsx +175 -0
  111. package/templates/nextjs-shadcn/components/chart-area-interactive.tsx +291 -0
  112. package/templates/nextjs-shadcn/components/data-table.tsx +807 -0
  113. package/templates/nextjs-shadcn/components/login-form.tsx +95 -0
  114. package/templates/nextjs-shadcn/components/nav-documents.tsx +92 -0
  115. package/templates/nextjs-shadcn/components/nav-main.tsx +73 -0
  116. package/templates/nextjs-shadcn/components/nav-projects.tsx +89 -0
  117. package/templates/nextjs-shadcn/components/nav-secondary.tsx +42 -0
  118. package/templates/nextjs-shadcn/components/nav-user.tsx +114 -0
  119. package/templates/nextjs-shadcn/components/section-cards.tsx +102 -0
  120. package/templates/nextjs-shadcn/components/site-header.tsx +30 -0
  121. package/templates/nextjs-shadcn/components/team-switcher.tsx +91 -0
  122. package/templates/nextjs-shadcn/components/ui/accordion.tsx +66 -0
  123. package/templates/nextjs-shadcn/components/ui/alert-dialog.tsx +196 -0
  124. package/templates/nextjs-shadcn/components/ui/alert.tsx +66 -0
  125. package/templates/nextjs-shadcn/components/ui/aspect-ratio.tsx +11 -0
  126. package/templates/nextjs-shadcn/components/ui/avatar.tsx +109 -0
  127. package/templates/nextjs-shadcn/components/ui/badge.tsx +48 -0
  128. package/templates/nextjs-shadcn/components/ui/breadcrumb.tsx +109 -0
  129. package/templates/nextjs-shadcn/components/ui/button-group.tsx +83 -0
  130. package/templates/nextjs-shadcn/components/ui/button.tsx +64 -0
  131. package/templates/nextjs-shadcn/components/ui/calendar.tsx +220 -0
  132. package/templates/nextjs-shadcn/components/ui/card.tsx +92 -0
  133. package/templates/nextjs-shadcn/components/ui/carousel.tsx +241 -0
  134. package/templates/nextjs-shadcn/components/ui/chart.tsx +357 -0
  135. package/templates/nextjs-shadcn/components/ui/checkbox.tsx +32 -0
  136. package/templates/nextjs-shadcn/components/ui/collapsible.tsx +33 -0
  137. package/templates/nextjs-shadcn/components/ui/combobox.tsx +310 -0
  138. package/templates/nextjs-shadcn/components/ui/command.tsx +184 -0
  139. package/templates/nextjs-shadcn/components/ui/context-menu.tsx +252 -0
  140. package/templates/nextjs-shadcn/components/ui/dialog.tsx +158 -0
  141. package/templates/nextjs-shadcn/components/ui/direction.tsx +22 -0
  142. package/templates/nextjs-shadcn/components/ui/drawer.tsx +135 -0
  143. package/templates/nextjs-shadcn/components/ui/dropdown-menu.tsx +257 -0
  144. package/templates/nextjs-shadcn/components/ui/empty.tsx +104 -0
  145. package/templates/nextjs-shadcn/components/ui/field.tsx +248 -0
  146. package/templates/nextjs-shadcn/components/ui/form.tsx +167 -0
  147. package/templates/nextjs-shadcn/components/ui/hover-card.tsx +44 -0
  148. package/templates/nextjs-shadcn/components/ui/input-group.tsx +170 -0
  149. package/templates/nextjs-shadcn/components/ui/input-otp.tsx +77 -0
  150. package/templates/nextjs-shadcn/components/ui/input.tsx +21 -0
  151. package/templates/nextjs-shadcn/components/ui/item.tsx +193 -0
  152. package/templates/nextjs-shadcn/components/ui/kbd.tsx +28 -0
  153. package/templates/nextjs-shadcn/components/ui/label.tsx +24 -0
  154. package/templates/nextjs-shadcn/components/ui/menubar.tsx +276 -0
  155. package/templates/nextjs-shadcn/components/ui/native-select.tsx +53 -0
  156. package/templates/nextjs-shadcn/components/ui/navigation-menu.tsx +168 -0
  157. package/templates/nextjs-shadcn/components/ui/pagination.tsx +127 -0
  158. package/templates/nextjs-shadcn/components/ui/popover.tsx +89 -0
  159. package/templates/nextjs-shadcn/components/ui/progress.tsx +31 -0
  160. package/templates/nextjs-shadcn/components/ui/radio-group.tsx +45 -0
  161. package/templates/nextjs-shadcn/components/ui/resizable.tsx +53 -0
  162. package/templates/nextjs-shadcn/components/ui/scroll-area.tsx +58 -0
  163. package/templates/nextjs-shadcn/components/ui/select.tsx +190 -0
  164. package/templates/nextjs-shadcn/components/ui/separator.tsx +28 -0
  165. package/templates/nextjs-shadcn/components/ui/sheet.tsx +143 -0
  166. package/templates/nextjs-shadcn/components/ui/sidebar.tsx +726 -0
  167. package/templates/nextjs-shadcn/components/ui/skeleton.tsx +13 -0
  168. package/templates/nextjs-shadcn/components/ui/slider.tsx +63 -0
  169. package/templates/nextjs-shadcn/components/ui/sonner.tsx +40 -0
  170. package/templates/nextjs-shadcn/components/ui/spinner.tsx +16 -0
  171. package/templates/nextjs-shadcn/components/ui/switch.tsx +35 -0
  172. package/templates/nextjs-shadcn/components/ui/table.tsx +116 -0
  173. package/templates/nextjs-shadcn/components/ui/tabs.tsx +91 -0
  174. package/templates/nextjs-shadcn/components/ui/textarea.tsx +18 -0
  175. package/templates/nextjs-shadcn/components/ui/toggle-group.tsx +83 -0
  176. package/templates/nextjs-shadcn/components/ui/toggle.tsx +47 -0
  177. package/templates/nextjs-shadcn/components/ui/tooltip.tsx +57 -0
  178. package/templates/nextjs-shadcn/components.json +23 -0
  179. package/templates/nextjs-shadcn/hooks/use-mobile.ts +19 -0
  180. package/templates/nextjs-shadcn/lib/utils.ts +6 -0
  181. package/templates/nextjs-shadcn/next-env.d.ts +6 -0
  182. package/templates/nextjs-shadcn/next.config.ts +8 -0
  183. package/templates/nextjs-shadcn/open-next.config.ts +6 -0
  184. package/templates/nextjs-shadcn/package.json +55 -0
  185. package/templates/nextjs-shadcn/postcss.config.mjs +8 -0
  186. package/templates/nextjs-shadcn/tsconfig.json +28 -0
  187. package/templates/nextjs-shadcn/wrangler.jsonc +23 -0
  188. package/templates/resend/.jack.json +64 -0
  189. package/templates/resend/bun.lock +23 -0
  190. package/templates/resend/package.json +16 -0
  191. package/templates/resend/schema.sql +13 -0
  192. package/templates/resend/src/email.ts +165 -0
  193. package/templates/resend/src/index.ts +108 -0
  194. package/templates/resend/tsconfig.json +17 -0
  195. package/templates/resend/wrangler.jsonc +11 -0
  196. package/templates/saas/.jack.json +1 -1
  197. package/templates/ai-chat/public/chat.js +0 -149
@@ -51,6 +51,9 @@ export type AuthState = "logged-in" | "not-logged-in" | "session-expired";
51
51
  * - "session-expired": had credentials but refresh failed
52
52
  */
53
53
  export async function getAuthState(): Promise<AuthState> {
54
+ // API token always counts as logged in
55
+ if (process.env.JACK_API_TOKEN) return "logged-in";
56
+
54
57
  const creds = await getCredentials();
55
58
  if (!creds) return "not-logged-in";
56
59
 
@@ -0,0 +1,55 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+
5
+ const HOOK_COMMAND = "jack mcp context 2>/dev/null || true";
6
+
7
+ /**
8
+ * Install a Claude Code SessionStart hook to the project's .claude/settings.json.
9
+ * Only fires when Claude Code is opened in this project directory.
10
+ * Non-destructive: preserves existing hooks and deduplicates.
11
+ */
12
+ export async function installClaudeCodeHooks(projectPath: string): Promise<boolean> {
13
+ try {
14
+ const claudeDir = join(projectPath, ".claude");
15
+ const settingsPath = join(claudeDir, "settings.json");
16
+
17
+ // Ensure .claude directory exists
18
+ if (!existsSync(claudeDir)) {
19
+ await mkdir(claudeDir, { recursive: true });
20
+ }
21
+
22
+ let settings: Record<string, unknown> = {};
23
+ if (existsSync(settingsPath)) {
24
+ try {
25
+ settings = await Bun.file(settingsPath).json();
26
+ } catch {
27
+ settings = {};
28
+ }
29
+ }
30
+
31
+ const hooks = (settings.hooks as Record<string, unknown[]>) ?? {};
32
+ const sessionStart = (hooks.SessionStart as Array<Record<string, unknown>>) ?? [];
33
+
34
+ // Check if jack hook is already installed
35
+ for (const entry of sessionStart) {
36
+ const entryHooks = entry.hooks as Array<Record<string, string>> | undefined;
37
+ if (entryHooks?.some((h) => h.command?.includes("jack mcp context"))) {
38
+ return true;
39
+ }
40
+ }
41
+
42
+ sessionStart.push({
43
+ matcher: "",
44
+ hooks: [{ type: "command", command: HOOK_COMMAND }],
45
+ });
46
+
47
+ hooks.SessionStart = sessionStart;
48
+ settings.hooks = hooks;
49
+
50
+ await Bun.write(settingsPath, JSON.stringify(settings, null, 2));
51
+ return true;
52
+ } catch {
53
+ return false;
54
+ }
55
+ }
@@ -3,7 +3,6 @@
3
3
  */
4
4
 
5
5
  import { debug } from "./debug.ts";
6
- import { formatSize } from "./format.ts";
7
6
 
8
7
  const DEFAULT_CONTROL_API_URL = "https://control.getjack.org";
9
8
 
@@ -16,12 +15,14 @@ export interface CreateProjectRequest {
16
15
  slug?: string;
17
16
  template?: string;
18
17
  use_prebuilt?: boolean;
18
+ forked_from?: string;
19
19
  }
20
20
 
21
21
  export interface CreateManagedProjectOptions {
22
22
  slug?: string;
23
23
  template?: string;
24
24
  usePrebuilt?: boolean;
25
+ forkedFrom?: string;
25
26
  }
26
27
 
27
28
  export interface CreateProjectResponse {
@@ -111,6 +112,9 @@ export async function createManagedProject(
111
112
  if (options?.usePrebuilt !== undefined) {
112
113
  requestBody.use_prebuilt = options.usePrebuilt;
113
114
  }
115
+ if (options?.forkedFrom) {
116
+ requestBody.forked_from = options.forkedFrom;
117
+ }
114
118
 
115
119
  debug("Creating managed project", {
116
120
  name,
@@ -451,6 +455,79 @@ export async function fetchProjectResources(projectId: string): Promise<ProjectR
451
455
  return data.resources;
452
456
  }
453
457
 
458
+ export interface DeploymentInfo {
459
+ id: string;
460
+ status: "queued" | "building" | "live" | "failed";
461
+ source: string;
462
+ error_message: string | null;
463
+ message: string | null;
464
+ created_at: string;
465
+ updated_at: string;
466
+ }
467
+
468
+ export interface DeploymentListResult {
469
+ deployments: DeploymentInfo[];
470
+ total: number;
471
+ }
472
+
473
+ /**
474
+ * Fetch recent deployments for a managed project.
475
+ * Returns deployments (up to 10) and total count.
476
+ */
477
+ export async function fetchDeployments(projectId: string): Promise<DeploymentListResult> {
478
+ const { authFetch } = await import("./auth/index.ts");
479
+
480
+ const response = await authFetch(`${getControlApiUrl()}/v1/projects/${projectId}/deployments`);
481
+
482
+ if (!response.ok) {
483
+ return { deployments: [], total: 0 }; // Silent fail — deployment history is supplementary
484
+ }
485
+
486
+ const data = (await response.json()) as { deployments: DeploymentInfo[]; total: number };
487
+ return { deployments: data.deployments, total: data.total };
488
+ }
489
+
490
+ export interface RollbackResponse {
491
+ deployment: {
492
+ id: string;
493
+ status: string;
494
+ source: string;
495
+ created_at: string;
496
+ updated_at: string;
497
+ };
498
+ }
499
+
500
+ /**
501
+ * Rollback a managed project to a previous deployment.
502
+ * If no deploymentId given, rolls back to the previous successful deployment.
503
+ */
504
+ export async function rollbackDeployment(
505
+ projectId: string,
506
+ deploymentId?: string,
507
+ ): Promise<RollbackResponse> {
508
+ const { authFetch } = await import("./auth/index.ts");
509
+
510
+ const body: Record<string, string> = {};
511
+ if (deploymentId) {
512
+ body.deployment_id = deploymentId;
513
+ }
514
+
515
+ const response = await authFetch(`${getControlApiUrl()}/v1/projects/${projectId}/rollback`, {
516
+ method: "POST",
517
+ headers: { "Content-Type": "application/json" },
518
+ body: JSON.stringify(body),
519
+ });
520
+
521
+ if (!response.ok) {
522
+ const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
523
+ message?: string;
524
+ };
525
+ throw new Error(err.message || `Rollback failed: ${response.status}`);
526
+ }
527
+
528
+ return response.json() as Promise<RollbackResponse>;
529
+ }
530
+
454
531
  /**
455
532
  * Create a resource for a managed project.
456
533
  * Uses POST /v1/projects/:id/resources/:type endpoint.
@@ -715,45 +792,6 @@ export async function getCurrentUserProfile(): Promise<UserProfile | null> {
715
792
  }
716
793
  }
717
794
 
718
- export interface SourceSnapshotResponse {
719
- success: boolean;
720
- source_key: string;
721
- }
722
-
723
- /**
724
- * Upload a source snapshot for a project.
725
- * Used to enable project forking.
726
- */
727
- export async function uploadSourceSnapshot(
728
- projectId: string,
729
- sourceZipPath: string,
730
- ): Promise<SourceSnapshotResponse> {
731
- const { authFetch } = await import("./auth/index.ts");
732
-
733
- const formData = new FormData();
734
- const sourceFile = Bun.file(sourceZipPath);
735
- formData.append("source", sourceFile);
736
-
737
- const url = `${getControlApiUrl()}/v1/projects/${projectId}/source`;
738
- debug(`Source snapshot: ${formatSize(sourceFile.size)}`);
739
-
740
- const start = Date.now();
741
- const response = await authFetch(url, {
742
- method: "POST",
743
- body: formData,
744
- });
745
- debug(`Source snapshot: ${response.status} (${((Date.now() - start) / 1000).toFixed(1)}s)`);
746
-
747
- if (!response.ok) {
748
- const error = (await response.json().catch(() => ({ message: "Upload failed" }))) as {
749
- message?: string;
750
- };
751
- throw new Error(error.message || `Source upload failed: ${response.status}`);
752
- }
753
-
754
- return response.json() as Promise<SourceSnapshotResponse>;
755
- }
756
-
757
795
  /**
758
796
  * Publish a project to make it forkable by others.
759
797
  */
package/src/lib/debug.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  // Debug utilities for timing and verbose logging
2
2
 
3
- let debugEnabled = false;
3
+ let debugEnabled =
4
+ process.env.JACK_DEBUG === "1" || process.env.JACK_DEBUG === "true";
4
5
  const timers: Map<string, number> = new Map();
5
6
  const startTime = Date.now();
6
7
 
@@ -18,6 +18,7 @@ export interface DeployUploadOptions {
18
18
  secretsPath?: string;
19
19
  assetsZipPath?: string;
20
20
  assetManifest?: AssetManifest;
21
+ message?: string;
21
22
  }
22
23
 
23
24
  export interface DeployUploadResult {
@@ -25,6 +26,7 @@ export interface DeployUploadResult {
25
26
  project_id: string;
26
27
  status: "queued" | "building" | "live" | "failed";
27
28
  source: string;
29
+ error_message: string | null;
28
30
  created_at: string;
29
31
  }
30
32
 
@@ -91,6 +93,10 @@ export async function uploadDeployment(options: DeployUploadOptions): Promise<De
91
93
  totalSize += manifestJson.length;
92
94
  }
93
95
 
96
+ if (options.message) {
97
+ formData.append("message", options.message);
98
+ }
99
+
94
100
  const prepareMs = Date.now() - prepareStart;
95
101
  debug(`Payload ready: ${formatSize(totalSize)} (${prepareMs}ms)`);
96
102
 
package/src/lib/hooks.ts CHANGED
@@ -702,7 +702,9 @@ const actionHandlers: {
702
702
  const proc = Bun.spawn(["sh", "-c", command], {
703
703
  cwd,
704
704
  stdin: interactive ? "inherit" : "ignore",
705
- stdout: "inherit",
705
+ // In non-interactive mode (MCP), stdout must NOT inherit because it would
706
+ // write into the JSON-RPC stdio transport and corrupt the protocol.
707
+ stdout: interactive ? "inherit" : "pipe",
706
708
  stderr: "inherit",
707
709
  });
708
710
  await proc.exited;
@@ -7,7 +7,7 @@
7
7
  import { stat } from "node:fs/promises";
8
8
  import { validateBindings } from "./binding-validator.ts";
9
9
  import { buildProject, parseWranglerConfig } from "./build-helper.ts";
10
- import { createManagedProject, syncProjectTags, uploadSourceSnapshot } from "./control-plane.ts";
10
+ import { createManagedProject, syncProjectTags } from "./control-plane.ts";
11
11
  import { debug } from "./debug.ts";
12
12
  import { uploadDeployment } from "./deploy-upload.ts";
13
13
  import { JackError, JackErrorCode } from "./errors.ts";
@@ -31,6 +31,7 @@ export interface ManagedCreateResult {
31
31
  export interface ManagedCreateOptions {
32
32
  template?: string;
33
33
  usePrebuilt?: boolean;
34
+ forkedFrom?: string;
34
35
  }
35
36
 
36
37
  /**
@@ -50,6 +51,7 @@ export async function createManagedProjectRemote(
50
51
  const result = await createManagedProject(projectName, {
51
52
  template: options?.template,
52
53
  usePrebuilt: options?.usePrebuilt ?? true,
54
+ forkedFrom: options?.forkedFrom,
53
55
  });
54
56
 
55
57
  const runjackUrl = result.url || `https://${result.project.slug}.runjack.xyz`;
@@ -79,6 +81,7 @@ export interface ManagedCodeDeployOptions {
79
81
  projectId: string;
80
82
  projectPath: string;
81
83
  reporter?: OperationReporter;
84
+ message?: string;
82
85
  }
83
86
 
84
87
  /**
@@ -88,7 +91,7 @@ export interface ManagedCodeDeployOptions {
88
91
  */
89
92
  export async function deployCodeToManagedProject(
90
93
  options: ManagedCodeDeployOptions,
91
- ): Promise<{ deploymentId: string; status: string }> {
94
+ ): Promise<{ deploymentId: string; status: string; errorMessage: string | null }> {
92
95
  const { projectId, projectPath, reporter } = options;
93
96
 
94
97
  // Track deploy start
@@ -161,6 +164,7 @@ export async function deployCodeToManagedProject(
161
164
  secretsPath: pkg.secretsPath ?? undefined,
162
165
  assetsZipPath: pkg.assetsZipPath ?? undefined,
163
166
  assetManifest: pkg.assetManifest ?? undefined,
167
+ message: options.message,
164
168
  });
165
169
 
166
170
  uploadProgress.complete();
@@ -183,16 +187,13 @@ export async function deployCodeToManagedProject(
183
187
  })
184
188
  .catch(() => {});
185
189
 
186
- // Upload source snapshot for forking (non-fatal, but must await before cleanup)
187
- try {
188
- await uploadSourceSnapshot(projectId, pkg.sourceZipPath);
189
- } catch (err) {
190
- debug("Source snapshot upload failed:", err instanceof Error ? err.message : String(err));
191
- }
190
+ // Source snapshot for forking is now derived from deployment artifacts on the control plane.
191
+ // No separate upload needed — clone/fork reads from the latest live deployment's source.zip.
192
192
 
193
193
  return {
194
194
  deploymentId: result.id,
195
195
  status: result.status,
196
+ errorMessage: result.error_message,
196
197
  };
197
198
  } catch (error) {
198
199
  reporter?.stop();
@@ -218,10 +219,12 @@ export async function deployToManagedProject(
218
219
  projectId: string,
219
220
  projectPath: string,
220
221
  reporter?: OperationReporter,
221
- ): Promise<{ deploymentId: string; status: string }> {
222
+ message?: string,
223
+ ): Promise<{ deploymentId: string; status: string; errorMessage: string | null }> {
222
224
  return deployCodeToManagedProject({
223
225
  projectId,
224
226
  projectPath,
225
227
  reporter,
228
+ message,
226
229
  });
227
230
  }
@@ -116,6 +116,12 @@ export async function linkProject(
116
116
 
117
117
  // Auto-add .jack/ to .gitignore
118
118
  await ensureGitignored(projectDir);
119
+
120
+ // Install Claude Code SessionStart hook to project-level .claude/settings.json
121
+ // Non-blocking, fire-and-forget — never delays project linking
122
+ import("./claude-hooks-installer.ts")
123
+ .then(({ installClaudeCodeHooks }) => installClaudeCodeHooks(projectDir))
124
+ .catch(() => {});
119
125
  }
120
126
 
121
127
  /**
@@ -102,6 +102,7 @@ export interface DeployOptions {
102
102
  managed?: boolean; // Force managed deploy mode
103
103
  byo?: boolean; // Force BYO deploy mode
104
104
  dryRun?: boolean; // Stop before actual deployment
105
+ message?: string; // Deploy message describing what changed
105
106
  }
106
107
 
107
108
  export interface DeployResult {
@@ -109,6 +110,9 @@ export interface DeployResult {
109
110
  projectName: string;
110
111
  deployOutput?: string;
111
112
  deployMode: DeployMode; // The deploy mode used
113
+ deploymentId?: string;
114
+ deployStatus?: string;
115
+ errorMessage?: string | null;
112
116
  }
113
117
 
114
118
  export interface ProjectStatus {
@@ -126,6 +130,12 @@ export interface ProjectStatus {
126
130
  missing: boolean;
127
131
  backupFiles: number | null;
128
132
  backupLastSync: string | null;
133
+ // Deploy tracking (managed projects only)
134
+ lastDeployAt: string | null;
135
+ deployCount: number;
136
+ lastDeployStatus: string | null;
137
+ lastDeploySource: string | null;
138
+ lastDeployMessage: string | null;
129
139
  }
130
140
 
131
141
  export interface StaleProject {
@@ -361,6 +371,7 @@ async function runParallelSetup(
361
371
  options: {
362
372
  template?: string;
363
373
  usePrebuilt?: boolean;
374
+ forkedFrom?: string;
364
375
  onRemoteReady?: (result: ManagedCreateResult) => void;
365
376
  },
366
377
  ): Promise<{
@@ -391,6 +402,7 @@ async function runParallelSetup(
391
402
  const remotePromise = createManagedProjectRemote(projectName, undefined, {
392
403
  template: options.template || "hello",
393
404
  usePrebuilt: options.usePrebuilt ?? true,
405
+ forkedFrom: options.forkedFrom,
394
406
  }).then((result) => {
395
407
  const duration = ((Date.now() - remoteStart) / 1000).toFixed(1);
396
408
  debug(`Remote project created in ${duration}s (status: ${result.status})`);
@@ -793,11 +805,25 @@ export async function createProject(
793
805
  }
794
806
  }
795
807
 
796
- // Check if jack init was run (throws if not)
808
+ // Auto-initialize on first run (replaces "Run: jack init" error)
797
809
  const { isInitialized } = await import("../commands/init.ts");
798
810
  const initialized = await isInitialized();
799
811
  if (!initialized) {
800
- throw new JackError(JackErrorCode.VALIDATION_ERROR, "jack is not set up yet", "Run: jack init");
812
+ const { writeConfig, readConfig } = await import("./config.ts");
813
+ const existingConfig = await readConfig();
814
+ await writeConfig({
815
+ version: 1,
816
+ initialized: true,
817
+ initializedAt: existingConfig?.initializedAt || new Date().toISOString(),
818
+ ...existingConfig,
819
+ });
820
+
821
+ // Best-effort MCP config install (non-blocking)
822
+ try {
823
+ const { installMcpConfigsToAllApps, saveMcpConfig } = await import("./mcp-config.ts");
824
+ await installMcpConfigsToAllApps();
825
+ await saveMcpConfig();
826
+ } catch {}
801
827
  }
802
828
 
803
829
  // Auth gate - check/prompt for authentication before any work
@@ -1199,9 +1225,11 @@ export async function createProject(
1199
1225
  reporter.start("Setting up project...");
1200
1226
 
1201
1227
  try {
1228
+ const forkedFrom = templateOrigin.type !== "builtin" ? templateOrigin.name : undefined;
1202
1229
  const result = await runParallelSetup(targetDir, projectName, {
1203
1230
  template: resolvedTemplate || "hello",
1204
1231
  usePrebuilt: templateOrigin.type === "builtin", // Only builtin templates have prebuilt bundles
1232
+ forkedFrom,
1205
1233
  onRemoteReady: (remote) => {
1206
1234
  // Show URL immediately when prebuilt succeeds
1207
1235
  reporter.stop();
@@ -1355,22 +1383,8 @@ export async function createProject(
1355
1383
  reporter.success(`Deployed: ${workerUrl}`);
1356
1384
  }
1357
1385
 
1358
- // Upload source snapshot for forking (prebuilt path needs this too)
1359
- try {
1360
- const { createSourceZip } = await import("./zip-packager.ts");
1361
- const { uploadSourceSnapshot } = await import("./control-plane.ts");
1362
- const { rm } = await import("node:fs/promises");
1363
-
1364
- const sourceZipPath = await createSourceZip(targetDir);
1365
- await uploadSourceSnapshot(remoteResult.projectId, sourceZipPath);
1366
- await rm(sourceZipPath, { force: true });
1367
- debug("Source snapshot uploaded for prebuilt project");
1368
- } catch (err) {
1369
- debug(
1370
- "Source snapshot upload failed (prebuilt):",
1371
- err instanceof Error ? err.message : String(err),
1372
- );
1373
- }
1386
+ // source.zip is now stored in deployment artifacts by the control plane
1387
+ // during prebuilt deploy — no client-side upload needed
1374
1388
  } else {
1375
1389
  // Prebuilt not available - fall back to fresh build
1376
1390
  if (remoteResult.prebuiltFailed) {
@@ -1688,11 +1702,10 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
1688
1702
  deployMode = link?.deploy_mode ?? "byo";
1689
1703
  }
1690
1704
 
1691
- // Ensure agent integration is set up (JACK.md, MCP config)
1705
+ // Ensure agent integration is set up (MCP config)
1692
1706
  // This is idempotent and runs silently
1693
1707
  try {
1694
1708
  await ensureAgentIntegration(projectPath, {
1695
- projectName,
1696
1709
  silent: true,
1697
1710
  });
1698
1711
  } catch (err) {
@@ -1723,6 +1736,7 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
1723
1736
 
1724
1737
  let workerUrl: string | null = null;
1725
1738
  let deployOutput: string | undefined;
1739
+ let managedDeployResult: { deploymentId: string; status: string; errorMessage: string | null } | undefined;
1726
1740
 
1727
1741
  // Deploy based on mode
1728
1742
  if (deployMode === "managed") {
@@ -1781,7 +1795,7 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
1781
1795
  }
1782
1796
 
1783
1797
  // deployToManagedProject now handles both template and code deploy
1784
- await deployToManagedProject(managedProjectId as string, projectPath, reporter);
1798
+ managedDeployResult = await deployToManagedProject(managedProjectId as string, projectPath, reporter, options.message);
1785
1799
 
1786
1800
  // Construct URL with username if available
1787
1801
  workerUrl = link?.owner_username
@@ -1936,6 +1950,9 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
1936
1950
  projectName,
1937
1951
  deployOutput: workerUrl ? undefined : deployOutput,
1938
1952
  deployMode,
1953
+ deploymentId: managedDeployResult?.deploymentId,
1954
+ deployStatus: managedDeployResult?.status,
1955
+ errorMessage: managedDeployResult?.errorMessage,
1939
1956
  };
1940
1957
  }
1941
1958
 
@@ -2025,11 +2042,35 @@ export async function getProjectStatus(
2025
2042
  }
2026
2043
  }
2027
2044
 
2045
+ // Fetch real deployment data for managed projects
2046
+ let lastDeployAt: string | null = null;
2047
+ let deployCount = 0;
2048
+ let lastDeployStatus: string | null = null;
2049
+ let lastDeploySource: string | null = null;
2050
+ let lastDeployMessage: string | null = null;
2051
+
2052
+ if (link?.deploy_mode === "managed") {
2053
+ try {
2054
+ const { fetchDeployments } = await import("./control-plane.ts");
2055
+ const result = await fetchDeployments(link.project_id);
2056
+ deployCount = result.total;
2057
+ const latest = result.deployments[0];
2058
+ if (latest) {
2059
+ lastDeployAt = latest.created_at;
2060
+ lastDeployStatus = latest.status;
2061
+ lastDeploySource = latest.source;
2062
+ lastDeployMessage = latest.message;
2063
+ }
2064
+ } catch {
2065
+ // Silent fail — deploy tracking is supplementary
2066
+ }
2067
+ }
2068
+
2028
2069
  return {
2029
2070
  name: projectName,
2030
2071
  localPath,
2031
2072
  workerUrl,
2032
- lastDeployed: link?.linked_at ?? null,
2073
+ lastDeployed: lastDeployAt ?? link?.linked_at ?? null,
2033
2074
  createdAt: link?.linked_at ?? null,
2034
2075
  accountId: null, // No longer stored in registry
2035
2076
  workerId: projectName,
@@ -2040,6 +2081,11 @@ export async function getProjectStatus(
2040
2081
  missing: false,
2041
2082
  backupFiles,
2042
2083
  backupLastSync,
2084
+ lastDeployAt,
2085
+ deployCount,
2086
+ lastDeployStatus,
2087
+ lastDeploySource,
2088
+ lastDeployMessage,
2043
2089
  };
2044
2090
  }
2045
2091
 
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Token operations service layer for jack cloud
3
+ *
4
+ * Provides shared API token management functions for both CLI and MCP.
5
+ * Returns pure data - no console.log or process.exit.
6
+ */
7
+
8
+ import { authFetch } from "../auth/index.ts";
9
+ import { getControlApiUrl } from "../control-plane.ts";
10
+
11
+ // ============================================================================
12
+ // Types
13
+ // ============================================================================
14
+
15
+ export interface CreateTokenResult {
16
+ token: string;
17
+ id: string;
18
+ name: string;
19
+ created_at: string;
20
+ expires_at: string | null;
21
+ }
22
+
23
+ export interface TokenInfo {
24
+ id: string;
25
+ name: string;
26
+ id_prefix: string;
27
+ created_at: string;
28
+ last_used_at: string | null;
29
+ expires_at: string | null;
30
+ }
31
+
32
+ // ============================================================================
33
+ // Service Functions
34
+ // ============================================================================
35
+
36
+ /**
37
+ * Create a new API token for headless authentication.
38
+ */
39
+ export async function createApiToken(
40
+ name: string,
41
+ expiresInDays?: number,
42
+ ): Promise<CreateTokenResult> {
43
+ const response = await authFetch(`${getControlApiUrl()}/v1/tokens`, {
44
+ method: "POST",
45
+ headers: { "Content-Type": "application/json" },
46
+ body: JSON.stringify({ name, expires_in_days: expiresInDays }),
47
+ });
48
+
49
+ if (!response.ok) {
50
+ const err = (await response.json().catch(() => ({}))) as { message?: string };
51
+ throw new Error(err.message || `Failed to create token: ${response.status}`);
52
+ }
53
+
54
+ return response.json() as Promise<CreateTokenResult>;
55
+ }
56
+
57
+ /**
58
+ * List all active API tokens for the current user.
59
+ */
60
+ export async function listApiTokens(): Promise<TokenInfo[]> {
61
+ const response = await authFetch(`${getControlApiUrl()}/v1/tokens`);
62
+
63
+ if (!response.ok) {
64
+ const err = (await response.json().catch(() => ({}))) as { message?: string };
65
+ throw new Error(err.message || `Failed to list tokens: ${response.status}`);
66
+ }
67
+
68
+ const data = (await response.json()) as { tokens: TokenInfo[] };
69
+ return data.tokens;
70
+ }
71
+
72
+ /**
73
+ * Revoke an API token by ID.
74
+ */
75
+ export async function revokeApiToken(tokenId: string): Promise<void> {
76
+ const response = await authFetch(`${getControlApiUrl()}/v1/tokens/${tokenId}`, {
77
+ method: "DELETE",
78
+ });
79
+
80
+ if (!response.ok) {
81
+ const err = (await response.json().catch(() => ({}))) as { message?: string };
82
+ throw new Error(err.message || `Failed to revoke token: ${response.status}`);
83
+ }
84
+ }
@@ -44,6 +44,11 @@ export const Events = {
44
44
  BYO_DEPLOY_STARTED: "byo_deploy_started",
45
45
  BYO_DEPLOY_COMPLETED: "byo_deploy_completed",
46
46
  BYO_DEPLOY_FAILED: "byo_deploy_failed",
47
+ // Token management events
48
+ TOKEN_CREATED: "token_created",
49
+ TOKEN_REVOKED: "token_revoked",
50
+ // Hook events
51
+ HOOK_SESSION_CONTEXT: "hook_session_context",
47
52
  } as const;
48
53
 
49
54
  type EventName = (typeof Events)[keyof typeof Events];
@@ -127,6 +132,7 @@ export interface UserProperties {
127
132
  is_tty?: boolean;
128
133
  locale?: string;
129
134
  config_style?: "byoc" | "jack-cloud";
135
+ auth_method?: "oauth" | "token";
130
136
  }
131
137
 
132
138
  // Detect environment properties (for user profile - stable properties)
package/src/mcp/README.md CHANGED
@@ -25,7 +25,7 @@ src/mcp/
25
25
  ### `tools/index.ts`
26
26
  - Centralized tool registration and dispatch
27
27
  - Implements 4 core tools:
28
- - `create_project` - Create new Cloudflare Workers project
28
+ - `create_project` - Create new project (supports forking via template)
29
29
  - `deploy_project` - Deploy existing project
30
30
  - `get_project_status` - Get project status info
31
31
  - `list_projects` - List all projects with filters