@getjack/jack 0.1.32 → 0.1.34

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 (196) 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/secrets.ts +3 -1
  7. package/src/commands/services.ts +11 -1
  8. package/src/commands/ship.ts +3 -1
  9. package/src/commands/tokens.ts +16 -1
  10. package/src/commands/whoami.ts +43 -8
  11. package/src/index.ts +16 -0
  12. package/src/lib/agent-files.ts +54 -4
  13. package/src/lib/agent-integration.ts +4 -166
  14. package/src/lib/claude-hooks-installer.ts +55 -0
  15. package/src/lib/control-plane.ts +78 -40
  16. package/src/lib/crypto.ts +84 -0
  17. package/src/lib/debug.ts +2 -1
  18. package/src/lib/deploy-upload.ts +13 -3
  19. package/src/lib/hooks.ts +4 -3
  20. package/src/lib/managed-deploy.ts +12 -9
  21. package/src/lib/project-link.ts +6 -0
  22. package/src/lib/project-operations.ts +92 -30
  23. package/src/lib/prompts.ts +2 -2
  24. package/src/lib/telemetry.ts +2 -0
  25. package/src/mcp/README.md +1 -1
  26. package/src/mcp/resources/index.ts +1 -16
  27. package/src/mcp/server.ts +23 -0
  28. package/src/mcp/tools/index.ts +133 -17
  29. package/src/mcp/types.ts +1 -0
  30. package/src/mcp/utils.ts +2 -1
  31. package/src/templates/index.ts +25 -73
  32. package/templates/CLAUDE.md +62 -0
  33. package/templates/ai-chat/.jack.json +10 -5
  34. package/templates/ai-chat/bun.lock +50 -1
  35. package/templates/ai-chat/package.json +5 -0
  36. package/templates/ai-chat/public/app.js +73 -0
  37. package/templates/ai-chat/public/index.html +14 -197
  38. package/templates/ai-chat/schema.sql +14 -0
  39. package/templates/ai-chat/src/index.ts +86 -102
  40. package/templates/ai-chat/wrangler.jsonc +8 -1
  41. package/templates/cron/.jack.json +66 -0
  42. package/templates/cron/bun.lock +23 -0
  43. package/templates/cron/package.json +16 -0
  44. package/templates/cron/schema.sql +24 -0
  45. package/templates/cron/src/index.ts +117 -0
  46. package/templates/cron/src/jobs.ts +139 -0
  47. package/templates/cron/src/webhooks.ts +95 -0
  48. package/templates/cron/tsconfig.json +17 -0
  49. package/templates/cron/wrangler.jsonc +11 -0
  50. package/templates/miniapp/.jack.json +1 -1
  51. package/templates/nextjs/.jack.json +1 -1
  52. package/templates/nextjs-auth/.jack.json +44 -0
  53. package/templates/nextjs-auth/app/api/auth/[...all]/route.ts +11 -0
  54. package/templates/nextjs-auth/app/dashboard/loading.tsx +53 -0
  55. package/templates/nextjs-auth/app/dashboard/page.tsx +73 -0
  56. package/templates/nextjs-auth/app/error.tsx +44 -0
  57. package/templates/nextjs-auth/app/globals.css +1 -0
  58. package/templates/nextjs-auth/app/health/route.ts +3 -0
  59. package/templates/nextjs-auth/app/layout.tsx +24 -0
  60. package/templates/nextjs-auth/app/login/page.tsx +10 -0
  61. package/templates/nextjs-auth/app/page.tsx +86 -0
  62. package/templates/nextjs-auth/app/signup/page.tsx +10 -0
  63. package/templates/nextjs-auth/bun.lock +1065 -0
  64. package/templates/nextjs-auth/cloudflare-env.d.ts +8 -0
  65. package/templates/nextjs-auth/components/auth-form.tsx +191 -0
  66. package/templates/nextjs-auth/components/header.tsx +50 -0
  67. package/templates/nextjs-auth/components/user-menu.tsx +23 -0
  68. package/templates/nextjs-auth/lib/auth-client.ts +3 -0
  69. package/templates/nextjs-auth/lib/auth.ts +43 -0
  70. package/templates/nextjs-auth/lib/utils.ts +6 -0
  71. package/templates/nextjs-auth/middleware.ts +33 -0
  72. package/templates/nextjs-auth/next.config.ts +8 -0
  73. package/templates/nextjs-auth/open-next.config.ts +6 -0
  74. package/templates/nextjs-auth/package.json +33 -0
  75. package/templates/nextjs-auth/postcss.config.mjs +8 -0
  76. package/templates/nextjs-auth/schema.sql +49 -0
  77. package/templates/nextjs-auth/tsconfig.json +28 -0
  78. package/templates/nextjs-auth/wrangler.jsonc +23 -0
  79. package/templates/nextjs-clerk/.jack.json +54 -0
  80. package/templates/nextjs-clerk/app/dashboard/page.tsx +69 -0
  81. package/templates/nextjs-clerk/app/globals.css +1 -0
  82. package/templates/nextjs-clerk/app/health/route.ts +3 -0
  83. package/templates/nextjs-clerk/app/layout.tsx +28 -0
  84. package/templates/nextjs-clerk/app/page.tsx +86 -0
  85. package/templates/nextjs-clerk/app/sign-in/[[...sign-in]]/page.tsx +9 -0
  86. package/templates/nextjs-clerk/app/sign-up/[[...sign-up]]/page.tsx +9 -0
  87. package/templates/nextjs-clerk/bun.lock +1055 -0
  88. package/templates/nextjs-clerk/cloudflare-env.d.ts +3 -0
  89. package/templates/nextjs-clerk/components/header.tsx +40 -0
  90. package/templates/nextjs-clerk/lib/utils.ts +6 -0
  91. package/templates/nextjs-clerk/middleware.ts +18 -0
  92. package/templates/nextjs-clerk/next.config.ts +8 -0
  93. package/templates/nextjs-clerk/open-next.config.ts +6 -0
  94. package/templates/nextjs-clerk/package.json +31 -0
  95. package/templates/nextjs-clerk/postcss.config.mjs +8 -0
  96. package/templates/nextjs-clerk/tsconfig.json +28 -0
  97. package/templates/nextjs-clerk/wrangler.jsonc +17 -0
  98. package/templates/nextjs-shadcn/.jack.json +34 -0
  99. package/templates/nextjs-shadcn/app/dashboard/data.json +614 -0
  100. package/templates/nextjs-shadcn/app/dashboard/page.tsx +55 -0
  101. package/templates/nextjs-shadcn/app/globals.css +126 -0
  102. package/templates/nextjs-shadcn/app/health/route.ts +3 -0
  103. package/templates/nextjs-shadcn/app/layout.tsx +24 -0
  104. package/templates/nextjs-shadcn/app/login/page.tsx +19 -0
  105. package/templates/nextjs-shadcn/app/page.tsx +180 -0
  106. package/templates/nextjs-shadcn/app/showcase.tsx +1262 -0
  107. package/templates/nextjs-shadcn/bun.lock +1789 -0
  108. package/templates/nextjs-shadcn/cloudflare-env.d.ts +4 -0
  109. package/templates/nextjs-shadcn/components/app-sidebar.tsx +175 -0
  110. package/templates/nextjs-shadcn/components/chart-area-interactive.tsx +291 -0
  111. package/templates/nextjs-shadcn/components/data-table.tsx +807 -0
  112. package/templates/nextjs-shadcn/components/login-form.tsx +95 -0
  113. package/templates/nextjs-shadcn/components/nav-documents.tsx +92 -0
  114. package/templates/nextjs-shadcn/components/nav-main.tsx +73 -0
  115. package/templates/nextjs-shadcn/components/nav-projects.tsx +89 -0
  116. package/templates/nextjs-shadcn/components/nav-secondary.tsx +42 -0
  117. package/templates/nextjs-shadcn/components/nav-user.tsx +114 -0
  118. package/templates/nextjs-shadcn/components/section-cards.tsx +102 -0
  119. package/templates/nextjs-shadcn/components/site-header.tsx +30 -0
  120. package/templates/nextjs-shadcn/components/team-switcher.tsx +91 -0
  121. package/templates/nextjs-shadcn/components/ui/accordion.tsx +66 -0
  122. package/templates/nextjs-shadcn/components/ui/alert-dialog.tsx +196 -0
  123. package/templates/nextjs-shadcn/components/ui/alert.tsx +66 -0
  124. package/templates/nextjs-shadcn/components/ui/aspect-ratio.tsx +11 -0
  125. package/templates/nextjs-shadcn/components/ui/avatar.tsx +109 -0
  126. package/templates/nextjs-shadcn/components/ui/badge.tsx +48 -0
  127. package/templates/nextjs-shadcn/components/ui/breadcrumb.tsx +109 -0
  128. package/templates/nextjs-shadcn/components/ui/button-group.tsx +83 -0
  129. package/templates/nextjs-shadcn/components/ui/button.tsx +64 -0
  130. package/templates/nextjs-shadcn/components/ui/calendar.tsx +220 -0
  131. package/templates/nextjs-shadcn/components/ui/card.tsx +92 -0
  132. package/templates/nextjs-shadcn/components/ui/carousel.tsx +241 -0
  133. package/templates/nextjs-shadcn/components/ui/chart.tsx +357 -0
  134. package/templates/nextjs-shadcn/components/ui/checkbox.tsx +32 -0
  135. package/templates/nextjs-shadcn/components/ui/collapsible.tsx +33 -0
  136. package/templates/nextjs-shadcn/components/ui/combobox.tsx +310 -0
  137. package/templates/nextjs-shadcn/components/ui/command.tsx +184 -0
  138. package/templates/nextjs-shadcn/components/ui/context-menu.tsx +252 -0
  139. package/templates/nextjs-shadcn/components/ui/dialog.tsx +158 -0
  140. package/templates/nextjs-shadcn/components/ui/direction.tsx +22 -0
  141. package/templates/nextjs-shadcn/components/ui/drawer.tsx +135 -0
  142. package/templates/nextjs-shadcn/components/ui/dropdown-menu.tsx +257 -0
  143. package/templates/nextjs-shadcn/components/ui/empty.tsx +104 -0
  144. package/templates/nextjs-shadcn/components/ui/field.tsx +248 -0
  145. package/templates/nextjs-shadcn/components/ui/form.tsx +167 -0
  146. package/templates/nextjs-shadcn/components/ui/hover-card.tsx +44 -0
  147. package/templates/nextjs-shadcn/components/ui/input-group.tsx +170 -0
  148. package/templates/nextjs-shadcn/components/ui/input-otp.tsx +77 -0
  149. package/templates/nextjs-shadcn/components/ui/input.tsx +21 -0
  150. package/templates/nextjs-shadcn/components/ui/item.tsx +193 -0
  151. package/templates/nextjs-shadcn/components/ui/kbd.tsx +28 -0
  152. package/templates/nextjs-shadcn/components/ui/label.tsx +24 -0
  153. package/templates/nextjs-shadcn/components/ui/menubar.tsx +276 -0
  154. package/templates/nextjs-shadcn/components/ui/native-select.tsx +53 -0
  155. package/templates/nextjs-shadcn/components/ui/navigation-menu.tsx +168 -0
  156. package/templates/nextjs-shadcn/components/ui/pagination.tsx +127 -0
  157. package/templates/nextjs-shadcn/components/ui/popover.tsx +89 -0
  158. package/templates/nextjs-shadcn/components/ui/progress.tsx +31 -0
  159. package/templates/nextjs-shadcn/components/ui/radio-group.tsx +45 -0
  160. package/templates/nextjs-shadcn/components/ui/resizable.tsx +53 -0
  161. package/templates/nextjs-shadcn/components/ui/scroll-area.tsx +58 -0
  162. package/templates/nextjs-shadcn/components/ui/select.tsx +190 -0
  163. package/templates/nextjs-shadcn/components/ui/separator.tsx +28 -0
  164. package/templates/nextjs-shadcn/components/ui/sheet.tsx +143 -0
  165. package/templates/nextjs-shadcn/components/ui/sidebar.tsx +726 -0
  166. package/templates/nextjs-shadcn/components/ui/skeleton.tsx +13 -0
  167. package/templates/nextjs-shadcn/components/ui/slider.tsx +63 -0
  168. package/templates/nextjs-shadcn/components/ui/sonner.tsx +40 -0
  169. package/templates/nextjs-shadcn/components/ui/spinner.tsx +16 -0
  170. package/templates/nextjs-shadcn/components/ui/switch.tsx +35 -0
  171. package/templates/nextjs-shadcn/components/ui/table.tsx +116 -0
  172. package/templates/nextjs-shadcn/components/ui/tabs.tsx +91 -0
  173. package/templates/nextjs-shadcn/components/ui/textarea.tsx +18 -0
  174. package/templates/nextjs-shadcn/components/ui/toggle-group.tsx +83 -0
  175. package/templates/nextjs-shadcn/components/ui/toggle.tsx +47 -0
  176. package/templates/nextjs-shadcn/components/ui/tooltip.tsx +57 -0
  177. package/templates/nextjs-shadcn/components.json +23 -0
  178. package/templates/nextjs-shadcn/hooks/use-mobile.ts +19 -0
  179. package/templates/nextjs-shadcn/lib/utils.ts +6 -0
  180. package/templates/nextjs-shadcn/next-env.d.ts +6 -0
  181. package/templates/nextjs-shadcn/next.config.ts +8 -0
  182. package/templates/nextjs-shadcn/open-next.config.ts +6 -0
  183. package/templates/nextjs-shadcn/package.json +55 -0
  184. package/templates/nextjs-shadcn/postcss.config.mjs +8 -0
  185. package/templates/nextjs-shadcn/tsconfig.json +28 -0
  186. package/templates/nextjs-shadcn/wrangler.jsonc +23 -0
  187. package/templates/resend/.jack.json +64 -0
  188. package/templates/resend/bun.lock +23 -0
  189. package/templates/resend/package.json +16 -0
  190. package/templates/resend/schema.sql +13 -0
  191. package/templates/resend/src/email.ts +165 -0
  192. package/templates/resend/src/index.ts +108 -0
  193. package/templates/resend/tsconfig.json +17 -0
  194. package/templates/resend/wrangler.jsonc +11 -0
  195. package/templates/saas/.jack.json +1 -1
  196. package/templates/ai-chat/public/chat.js +0 -149
@@ -7,7 +7,7 @@ import type { AgentConfig, AgentDefinition } from "./agents.ts";
7
7
  * Template for AGENTS.md
8
8
  */
9
9
  function generateAgentsMd(projectName: string, template: Template): string {
10
- const summary = template.agentContext?.summary || "A Cloudflare Workers project";
10
+ const summary = template.agentContext?.summary || "A jack project";
11
11
  const fullText = template.agentContext?.full_text || "";
12
12
 
13
13
  return `# ${projectName}
@@ -16,16 +16,66 @@ function generateAgentsMd(projectName: string, template: Template): string {
16
16
 
17
17
  ## Deployment
18
18
 
19
- This project is deployed to Cloudflare Workers using jack:
19
+ This project is deployed and managed via jack:
20
20
 
21
21
  \`\`\`bash
22
- jack ship # Deploy to Cloudflare Workers
22
+ jack ship # Deploy to production
23
23
  jack logs # Stream production logs
24
24
  \`\`\`
25
25
 
26
26
  All deployment is handled by jack. Never run \`wrangler\` commands directly.
27
27
 
28
+ ## Quick Commands
29
+
30
+ | Command | What it does |
31
+ |---------|--------------|
32
+ | \`jack ship\` | Deploy to production |
33
+ | \`jack logs\` | Stream live logs |
34
+ | \`jack services\` | Manage databases, KV, and other bindings |
35
+ | \`jack secrets\` | Manage environment secrets |
36
+
37
+ ## Services & Bindings
38
+
39
+ Jack manages your project's services. To add a database:
40
+
41
+ \`\`\`bash
42
+ jack services db create
43
+ \`\`\`
44
+
45
+ To query it:
46
+
47
+ \`\`\`bash
48
+ jack services db query "SELECT * FROM users"
49
+ \`\`\`
50
+
28
51
  ${fullText}
52
+
53
+ ## For AI Agents
54
+
55
+ ### MCP Tools
56
+
57
+ If jack MCP is connected, **always prefer these tools over CLI commands or wrangler**:
58
+
59
+ | Tool | Use for |
60
+ |------|---------|
61
+ | \`mcp__jack__create_project\` | Create a new project (supports forking via \`template: "username/slug"\`) |
62
+ | \`mcp__jack__deploy_project\` | Deploy changes |
63
+ | \`mcp__jack__get_project_status\` | Check deployment status and URL |
64
+ | \`mcp__jack__list_projects\` | List all projects |
65
+ | \`mcp__jack__create_database\` | Create a database |
66
+ | \`mcp__jack__execute_sql\` | Query the database (read-only by default) |
67
+ | \`mcp__jack__list_databases\` | List project databases |
68
+ | \`mcp__jack__create_storage_bucket\` | Create object storage |
69
+ | \`mcp__jack__create_vectorize_index\` | Create vector search index |
70
+ | \`mcp__jack__tail_logs\` | Debug with live log samples |
71
+ | \`mcp__jack__start_log_session\` | Start real-time log stream |
72
+ | \`mcp__jack__create_cron\` | Create scheduled tasks |
73
+ | \`mcp__jack__list_domains\` | List custom domains |
74
+ | \`mcp__jack__connect_domain\` | Add a custom domain |
75
+
76
+ ### Documentation
77
+
78
+ Full jack documentation: https://docs.getjack.org/llms-full.txt
29
79
  `;
30
80
  }
31
81
 
@@ -39,7 +89,7 @@ See [AGENTS.md](./AGENTS.md) for complete project context and deployment instruc
39
89
 
40
90
  ## Quick Commands
41
91
 
42
- - **Deploy**: \`jack ship\` - Deploy to Cloudflare Workers
92
+ - **Deploy**: \`jack ship\` - Deploy to production
43
93
  - **Logs**: \`jack logs\` - Stream production logs
44
94
 
45
95
  ## Important
@@ -1,165 +1,18 @@
1
1
  /**
2
2
  * Agent integration module
3
3
  *
4
- * Ensures AI agents have proper context for jack projects.
4
+ * Ensures AI agents have MCP configured for jack projects.
5
5
  * Called during both project creation and first BYO deploy.
6
6
  */
7
7
 
8
- import { existsSync } from "node:fs";
9
- import { join } from "node:path";
10
- import type { Template } from "../templates/types.ts";
11
8
  import { installMcpConfigsToAllApps, isAppInstalled } from "./mcp-config.ts";
12
9
 
13
10
  export interface EnsureAgentResult {
14
11
  mcpInstalled: string[];
15
- jackMdCreated: boolean;
16
- referencesAdded: string[];
17
12
  }
18
13
 
19
14
  export interface EnsureAgentOptions {
20
- template?: Template;
21
15
  silent?: boolean;
22
- projectName?: string;
23
- }
24
-
25
- /**
26
- * Generate JACK.md content
27
- * Uses template agentContext if available, otherwise generic jack instructions
28
- */
29
- export function generateJackMd(projectName?: string, template?: Template): string {
30
- const header = projectName ? `# ${projectName}\n\n` : "# Jack\n\n";
31
-
32
- const templateSummary = template?.agentContext?.summary;
33
- const templateFullText = template?.agentContext?.full_text;
34
-
35
- const summarySection = templateSummary ? `> ${templateSummary}\n\n` : "";
36
-
37
- const templateSection = templateFullText ? `${templateFullText}\n\n` : "";
38
-
39
- return `${header}${summarySection}This project is deployed and managed via jack.
40
-
41
- ## Quick Commands
42
-
43
- | Command | What it does |
44
- |---------|--------------|
45
- | \`jack ship\` | Deploy to production |
46
- | \`jack logs\` | Stream live logs |
47
- | \`jack services\` | Manage databases, KV, and other bindings |
48
- | \`jack secrets\` | Manage environment secrets |
49
-
50
- ## Important
51
-
52
- - **Never run \`wrangler\` commands directly** - jack handles all infrastructure
53
- - Use \`jack services db\` to create and query databases
54
- - Secrets sync automatically across deploys
55
-
56
- ## Services & Bindings
57
-
58
- Jack manages your project's services. To add a database:
59
-
60
- \`\`\`bash
61
- jack services db create
62
- \`\`\`
63
-
64
- To query it:
65
-
66
- \`\`\`bash
67
- jack services db query "SELECT * FROM users"
68
- \`\`\`
69
-
70
- More bindings (KV, R2, queues) coming soon.
71
-
72
- ${templateSection}## For AI Agents
73
-
74
- ### MCP Tools
75
-
76
- If jack MCP is connected, prefer these tools over CLI commands:
77
-
78
- | Tool | Use for |
79
- |------|---------|
80
- | \`mcp__jack__deploy_project\` | Deploy changes |
81
- | \`mcp__jack__create_database\` | Create a new database |
82
- | \`mcp__jack__execute_sql\` | Query the database |
83
- | \`mcp__jack__list_projects\` | List all projects |
84
- | \`mcp__jack__get_project_status\` | Check deployment status |
85
-
86
- ### Documentation
87
-
88
- Full jack documentation: https://docs.getjack.org/llms-full.txt
89
- `;
90
- }
91
-
92
- /**
93
- * Create JACK.md if it doesn't exist
94
- */
95
- async function ensureJackMd(
96
- projectPath: string,
97
- projectName?: string,
98
- template?: Template,
99
- ): Promise<boolean> {
100
- const jackMdPath = join(projectPath, "JACK.md");
101
-
102
- if (existsSync(jackMdPath)) {
103
- return false;
104
- }
105
-
106
- const content = generateJackMd(projectName, template);
107
- await Bun.write(jackMdPath, content);
108
- return true;
109
- }
110
-
111
- /**
112
- * Append JACK.md reference to existing agent files (CLAUDE.md, AGENTS.md)
113
- */
114
- async function appendJackMdReferences(projectPath: string): Promise<string[]> {
115
- const filesToCheck = ["CLAUDE.md", "AGENTS.md"];
116
- const referencesAdded: string[] = [];
117
- const jackMdPath = join(projectPath, "JACK.md");
118
-
119
- // Only add references if JACK.md exists
120
- if (!existsSync(jackMdPath)) {
121
- return referencesAdded;
122
- }
123
-
124
- const referenceBlock = `<!-- Added by jack -->
125
- > **Jack project** - See [JACK.md](./JACK.md) for deployment, services, and bindings.
126
-
127
- `;
128
-
129
- for (const filename of filesToCheck) {
130
- const filePath = join(projectPath, filename);
131
-
132
- if (!existsSync(filePath)) {
133
- continue;
134
- }
135
-
136
- try {
137
- const content = await Bun.file(filePath).text();
138
-
139
- // Skip if reference already exists
140
- if (content.includes("JACK.md") || content.includes("<!-- Added by jack -->")) {
141
- continue;
142
- }
143
-
144
- // Find position after first heading, or prepend if no heading
145
- const headingMatch = content.match(/^#[^\n]*\n/m);
146
- let newContent: string;
147
-
148
- if (headingMatch && headingMatch.index !== undefined) {
149
- const insertPos = headingMatch.index + headingMatch[0].length;
150
- newContent = content.slice(0, insertPos) + "\n" + referenceBlock + content.slice(insertPos);
151
- } else {
152
- newContent = referenceBlock + content;
153
- }
154
-
155
- await Bun.write(filePath, newContent);
156
- referencesAdded.push(filename);
157
- } catch {
158
- // Ignore errors reading/writing individual files
159
- }
160
- }
161
-
162
- return referencesAdded;
163
16
  }
164
17
 
165
18
  /**
@@ -186,31 +39,16 @@ async function ensureMcpConfigured(): Promise<string[]> {
186
39
  /**
187
40
  * Ensure agent integration is set up for a project
188
41
  *
189
- * This function:
190
- * 1. Creates JACK.md if not exists (with template context if available)
191
- * 2. Appends JACK.md reference to existing CLAUDE.md/AGENTS.md
192
- * 3. Installs MCP config to detected AI apps
193
- *
42
+ * Installs MCP config to detected AI apps.
194
43
  * Safe to call multiple times - all operations are idempotent.
195
44
  */
196
45
  export async function ensureAgentIntegration(
197
- projectPath: string,
198
- options: EnsureAgentOptions = {},
46
+ _projectPath: string,
47
+ _options: EnsureAgentOptions = {},
199
48
  ): Promise<EnsureAgentResult> {
200
- const { template, projectName } = options;
201
-
202
- // 1. Create JACK.md if not exists
203
- const jackMdCreated = await ensureJackMd(projectPath, projectName, template);
204
-
205
- // 2. Append references to existing agent files
206
- const referencesAdded = await appendJackMdReferences(projectPath);
207
-
208
- // 3. Ensure MCP is configured
209
49
  const mcpInstalled = await ensureMcpConfigured();
210
50
 
211
51
  return {
212
52
  mcpInstalled,
213
- jackMdCreated,
214
- referencesAdded,
215
53
  };
216
54
  }
@@ -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
  */
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Client-side encryption for secrets sent to the control plane.
3
+ *
4
+ * Uses hybrid RSA-OAEP + AES-GCM so secrets of any size can be encrypted.
5
+ * The control plane holds the matching RSA private key as a worker secret.
6
+ */
7
+
8
+ export interface EncryptedEnvelope {
9
+ __encrypted: true;
10
+ kid: string;
11
+ wrappedKey: string; // base64url
12
+ iv: string; // base64url
13
+ ciphertext: string; // base64url
14
+ }
15
+
16
+ const KEY_ID = "v1";
17
+
18
+ /** RSA-OAEP-256 public key for secrets encryption (generated by scripts/generate-encryption-keypair.ts) */
19
+ const PUBLIC_KEY_JWK: JsonWebKey = {
20
+ alg: "RSA-OAEP-256",
21
+ e: "AQAB",
22
+ ext: true,
23
+ key_ops: ["encrypt"],
24
+ kty: "RSA",
25
+ n: "q2Y4K6heGkv_ABFOYokNXcwHFLAG3JScxEhjnZQTi7K8JEdCM9inqcy3gGhtT4lP6YWqhF4IHRMFU4qhPuByLASNp3bMWzDDKlckyDeWyPRnJqjb6IvwPYLw0ky1WumjjypAX_OSpNKhuYHx1X1hu7KQq9oa3f6sHFM5XbofMM2f__HvcEHnBVgkJvjTL2dn94DPgnsmtTLSRUAde34DQnXAKjVJ2jDuoC_sDAUmcmsEZKt3AUaCTkLBtbfW-ZI6_4VD2yNw-ySuOEprhhsNi6UpbjPY1ncduB5nkNhb276kVsjWo8w89KvDlhNCRyyZ_c0QRYSxn-nYEIE3vtS_h9FC9keMcDnH_fE4VPn14cjPV_G-eiUAoow8q5qBnFEp9DaaOswZ8IwEhpaxN6jvgk1WikZIBd58WB4HHSFWQ-W-096_5FA4cltQE7Qgwy86AgPnhpuCLLTqwpx8XF3GLbWtt9h4QYpfjrLyGuj4gJWCI4AJSDY1bvqiZtTfO1LdhyiZteEH0XhSBvXjXb1dJHbNXIcrIa_owtfEKqb53AxxwTvPaZazkigT0MqZ-141e7x6kuDkG_gSSFyCGrESaAyGYRh2K4wcGuV4jyZlQ6dzbQd0DPn8uRW3kC_vpToyZxZVqWGXFD6TtMYQwo_zWK3IaYCYMB-TYBFJj8a41z8",
26
+ };
27
+
28
+ function base64urlEncode(bytes: ArrayBuffer | Uint8Array): string {
29
+ const u8 = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
30
+ let bin = "";
31
+ for (let i = 0; i < u8.length; i++) {
32
+ bin += String.fromCharCode(u8[i]);
33
+ }
34
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
35
+ }
36
+
37
+ let _cachedPublicKey: CryptoKey | null = null;
38
+
39
+ async function getPublicKey(): Promise<CryptoKey> {
40
+ if (_cachedPublicKey) return _cachedPublicKey;
41
+ _cachedPublicKey = await crypto.subtle.importKey(
42
+ "jwk",
43
+ PUBLIC_KEY_JWK,
44
+ { name: "RSA-OAEP", hash: "SHA-256" },
45
+ false,
46
+ ["encrypt"],
47
+ );
48
+ return _cachedPublicKey;
49
+ }
50
+
51
+ /** Encrypt a single plaintext string into an encrypted envelope. */
52
+ export async function encryptSecretValue(plaintext: string): Promise<EncryptedEnvelope> {
53
+ const rsaKey = await getPublicKey();
54
+
55
+ // Generate ephemeral AES-256-GCM key
56
+ const aesKey = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, [
57
+ "encrypt",
58
+ ]);
59
+
60
+ // Encrypt plaintext with AES-GCM
61
+ const iv = crypto.getRandomValues(new Uint8Array(12));
62
+ const plainBytes = new TextEncoder().encode(plaintext);
63
+ const ciphertext = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, aesKey, plainBytes);
64
+
65
+ // Wrap the AES key with RSA-OAEP
66
+ const rawAesKey = await crypto.subtle.exportKey("raw", aesKey);
67
+ const wrappedKey = await crypto.subtle.encrypt({ name: "RSA-OAEP" }, rsaKey, rawAesKey);
68
+
69
+ return {
70
+ __encrypted: true,
71
+ kid: KEY_ID,
72
+ wrappedKey: base64urlEncode(wrappedKey),
73
+ iv: base64urlEncode(iv),
74
+ ciphertext: base64urlEncode(ciphertext),
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Encrypt a full secrets map (Record<string, string>) as a single envelope.
80
+ * The entire JSON object is serialized then encrypted as one blob.
81
+ */
82
+ export async function encryptSecrets(secrets: Record<string, string>): Promise<EncryptedEnvelope> {
83
+ return encryptSecretValue(JSON.stringify(secrets));
84
+ }
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
 
@@ -6,6 +6,7 @@ import { readFile } from "node:fs/promises";
6
6
  import type { AssetManifest } from "./asset-hash.ts";
7
7
  import { authFetch } from "./auth/index.ts";
8
8
  import { getControlApiUrl } from "./control-plane.ts";
9
+ import { encryptSecrets } from "./crypto.ts";
9
10
  import { debug } from "./debug.ts";
10
11
  import { formatSize } from "./format.ts";
11
12
 
@@ -18,6 +19,7 @@ export interface DeployUploadOptions {
18
19
  secretsPath?: string;
19
20
  assetsZipPath?: string;
20
21
  assetManifest?: AssetManifest;
22
+ message?: string;
21
23
  }
22
24
 
23
25
  export interface DeployUploadResult {
@@ -25,6 +27,7 @@ export interface DeployUploadResult {
25
27
  project_id: string;
26
28
  status: "queued" | "building" | "live" | "failed";
27
29
  source: string;
30
+ error_message: string | null;
28
31
  created_at: string;
29
32
  }
30
33
 
@@ -65,13 +68,16 @@ export async function uploadDeployment(options: DeployUploadOptions): Promise<De
65
68
  }
66
69
 
67
70
  if (options.secretsPath) {
68
- const secretsContent = await readFile(options.secretsPath);
71
+ const secretsContent = await readFile(options.secretsPath, "utf-8");
72
+ const secretsJson = JSON.parse(secretsContent) as Record<string, string>;
73
+ const encryptedEnvelope = await encryptSecrets(secretsJson);
74
+ const encryptedPayload = JSON.stringify(encryptedEnvelope);
69
75
  formData.append(
70
76
  "secrets",
71
- new Blob([secretsContent], { type: "application/json" }),
77
+ new Blob([encryptedPayload], { type: "application/json" }),
72
78
  "secrets.json",
73
79
  );
74
- totalSize += secretsContent.length;
80
+ totalSize += encryptedPayload.length;
75
81
  }
76
82
 
77
83
  if (options.assetsZipPath) {
@@ -91,6 +97,10 @@ export async function uploadDeployment(options: DeployUploadOptions): Promise<De
91
97
  totalSize += manifestJson.length;
92
98
  }
93
99
 
100
+ if (options.message) {
101
+ formData.append("message", options.message);
102
+ }
103
+
94
104
  const prepareMs = Date.now() - prepareStart;
95
105
  debug(`Payload ready: ${formatSize(totalSize)} (${prepareMs}ms)`);
96
106
 
package/src/lib/hooks.ts CHANGED
@@ -533,8 +533,7 @@ const actionHandlers: {
533
533
  const { isCancel, text } = await import("@clack/prompts");
534
534
  const value = await text({ message: promptMsg });
535
535
 
536
- if (isCancel(value) || !value.trim()) {
537
- ui.warn(`Skipped ${action.key}`);
536
+ if (isCancel(value) || !value || !value.trim()) {
538
537
  return false;
539
538
  }
540
539
 
@@ -702,7 +701,9 @@ const actionHandlers: {
702
701
  const proc = Bun.spawn(["sh", "-c", command], {
703
702
  cwd,
704
703
  stdin: interactive ? "inherit" : "ignore",
705
- stdout: "inherit",
704
+ // In non-interactive mode (MCP), stdout must NOT inherit because it would
705
+ // write into the JSON-RPC stdio transport and corrupt the protocol.
706
+ stdout: interactive ? "inherit" : "pipe",
706
707
  stderr: "inherit",
707
708
  });
708
709
  await proc.exited;