@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getjack/jack",
3
- "version": "0.1.31",
3
+ "version": "0.1.33",
4
4
  "description": "Ship before you forget why you started. The vibecoder's deployment CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -0,0 +1,95 @@
1
+ import { fetchDeployments } from "../lib/control-plane.ts";
2
+ import { error, info, item } from "../lib/output.ts";
3
+ import { readProjectLink } from "../lib/project-link.ts";
4
+ import { getProjectNameFromDir } from "../lib/storage/index.ts";
5
+
6
+ const DEFAULT_LIMIT = 5;
7
+ const ALL_LIMIT = 50;
8
+
9
+ function humanizeSource(source: string): string {
10
+ if (source.startsWith("code:")) return "cli";
11
+ if (source.startsWith("prebuilt:")) return "template";
12
+ if (source.startsWith("template:")) return "template";
13
+ if (source.startsWith("rollback:")) return "rollback";
14
+ return source;
15
+ }
16
+
17
+ function humanizeStatus(status: string): string {
18
+ if (status === "queued") return "interrupted";
19
+ return status;
20
+ }
21
+
22
+ interface DeploysOptions {
23
+ all?: boolean;
24
+ }
25
+
26
+ /**
27
+ * List recent deployments for a project
28
+ */
29
+ export default async function deploys(options: DeploysOptions = {}): Promise<void> {
30
+ const link = await readProjectLink(process.cwd());
31
+
32
+ if (!link) {
33
+ error("Not a jack project");
34
+ info("Run this command from a linked project directory");
35
+ process.exit(1);
36
+ }
37
+
38
+ if (link.deploy_mode !== "managed") {
39
+ info("Deploy history is available for managed projects only");
40
+ process.exit(0);
41
+ }
42
+
43
+ let result;
44
+ try {
45
+ result = await fetchDeployments(link.project_id);
46
+ } catch {
47
+ error("Could not fetch deployments");
48
+ info("Check your network connection and try again");
49
+ process.exit(1);
50
+ }
51
+
52
+ if (result.deployments.length === 0) {
53
+ info("No deployments yet");
54
+ return;
55
+ }
56
+
57
+ let projectName: string;
58
+ try {
59
+ projectName = await getProjectNameFromDir(process.cwd());
60
+ } catch {
61
+ projectName = link.project_id;
62
+ }
63
+
64
+ const limit = options.all ? ALL_LIMIT : DEFAULT_LIMIT;
65
+ const shown = result.deployments.slice(0, limit);
66
+
67
+ console.error("");
68
+ info(`Deployments for ${projectName} (${result.total} total)`);
69
+ console.error("");
70
+
71
+ // icon + id status source time
72
+ console.error(" ID Status Source Deployed");
73
+ console.error(" ── ────── ────── ────────");
74
+
75
+ for (const deploy of shown) {
76
+ const time = new Date(deploy.created_at).toLocaleString();
77
+ const shortId = deploy.id.length > 12 ? deploy.id.slice(4, 12) : deploy.id;
78
+ const status = humanizeStatus(deploy.status);
79
+ const source = humanizeSource(deploy.source);
80
+ const icon = status === "live" ? "✓" : status === "failed" ? "✗" : "○";
81
+ item(`${icon} ${shortId} ${status.padEnd(12)} ${source.padEnd(10)} ${time}`);
82
+ if (deploy.message) {
83
+ console.error(` "${deploy.message}"`);
84
+ }
85
+ if (deploy.error_message) {
86
+ console.error(` ${deploy.error_message}`);
87
+ }
88
+ }
89
+
90
+ if (!options.all && result.total > DEFAULT_LIMIT) {
91
+ console.error("");
92
+ info(`Showing ${shown.length} of ${result.total}. Use --all to see more.`);
93
+ }
94
+ console.error("");
95
+ }
@@ -32,6 +32,14 @@ export default async function link(projectName?: string, flags: LinkFlags = {}):
32
32
  }
33
33
  }
34
34
 
35
+ // Ensure hooks are installed for existing projects (idempotent)
36
+ try {
37
+ const { installClaudeCodeHooks } = await import("../lib/claude-hooks-installer.ts");
38
+ await installClaudeCodeHooks(process.cwd());
39
+ } catch {
40
+ // Non-critical
41
+ }
42
+
35
43
  error("This directory is already linked");
36
44
  info(`Linked to: ${projectDisplay}`);
37
45
  info("To re-link, first run: jack unlink");
@@ -1,5 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
- import { mkdtemp, rm } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdtemp, readFile, rm } from "node:fs/promises";
3
4
  import { tmpdir } from "node:os";
4
5
  import { join } from "node:path";
5
6
  import { fileURLToPath } from "node:url";
@@ -38,11 +39,19 @@ export default async function mcp(subcommand?: string, options: McpOptions = {})
38
39
  return;
39
40
  }
40
41
 
41
- error("Unknown subcommand. Use: jack mcp serve, jack mcp install, or jack mcp test");
42
+ if (subcommand === "context") {
43
+ await outputProjectContext();
44
+ return;
45
+ }
46
+
47
+ error(
48
+ "Unknown subcommand. Use: jack mcp serve, jack mcp install, jack mcp test, or jack mcp context",
49
+ );
42
50
  info("Usage:");
43
51
  info(" jack mcp serve [--project /path] [--debug] Start MCP server");
44
52
  info(" jack mcp install Install/repair MCP config for AI agents");
45
53
  info(" jack mcp test Test MCP server connectivity");
54
+ info(" jack mcp context Output project context for hooks");
46
55
  process.exit(1);
47
56
  }
48
57
 
@@ -85,14 +94,14 @@ async function installMcpConfig(): Promise<void> {
85
94
  }
86
95
 
87
96
  if (skipped.length > 0) {
88
- info(`\nSkipped (not installed):`);
97
+ info("\nSkipped (not installed):");
89
98
  for (const app of skipped) {
90
99
  item(` ${app}`);
91
100
  }
92
101
  }
93
102
 
94
103
  if (failed.length > 0) {
95
- error(`\nFailed to install:`);
104
+ error("\nFailed to install:");
96
105
  for (const app of failed) {
97
106
  item(` ${app}`);
98
107
  }
@@ -105,6 +114,172 @@ async function installMcpConfig(): Promise<void> {
105
114
  }
106
115
  }
107
116
 
117
+ /**
118
+ * Parse wrangler config (jsonc or toml) and extract binding info.
119
+ * Returns null on any failure — never blocks session start.
120
+ */
121
+ async function parseWranglerBindings(cwd: string): Promise<{
122
+ databases: string[];
123
+ buckets: string[];
124
+ vectorize: string[];
125
+ ai: boolean;
126
+ kv: string[];
127
+ } | null> {
128
+ try {
129
+ const jsoncPath = join(cwd, "wrangler.jsonc");
130
+ const tomlPath = join(cwd, "wrangler.toml");
131
+
132
+ let config: Record<string, unknown> | null = null;
133
+
134
+ if (existsSync(jsoncPath)) {
135
+ const content = await readFile(jsoncPath, "utf-8");
136
+ const jsonContent = content.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
137
+ config = JSON.parse(jsonContent);
138
+ } else if (existsSync(tomlPath)) {
139
+ // For toml, just check for key patterns — not worth a full parser here
140
+ const content = await readFile(tomlPath, "utf-8");
141
+ return {
142
+ databases: content.includes("d1_databases") ? ["(see wrangler.toml)"] : [],
143
+ buckets: content.includes("r2_buckets") ? ["(see wrangler.toml)"] : [],
144
+ vectorize: content.includes("vectorize") ? ["(see wrangler.toml)"] : [],
145
+ ai: content.includes("[ai]"),
146
+ kv: content.includes("kv_namespaces") ? ["(see wrangler.toml)"] : [],
147
+ };
148
+ }
149
+
150
+ if (!config) return null;
151
+
152
+ const databases =
153
+ (config.d1_databases as { database_name?: string; binding?: string }[])?.map(
154
+ (d) => d.database_name || d.binding || "unknown",
155
+ ) ?? [];
156
+ const buckets =
157
+ (config.r2_buckets as { bucket_name?: string; binding?: string }[])?.map(
158
+ (b) => b.bucket_name || b.binding || "unknown",
159
+ ) ?? [];
160
+ const vectorize =
161
+ (config.vectorize as { index_name?: string; binding?: string }[])?.map(
162
+ (v) => v.index_name || v.binding || "unknown",
163
+ ) ?? [];
164
+ const ai = !!config.ai;
165
+ const kv =
166
+ (config.kv_namespaces as { binding?: string }[])?.map((k) => k.binding || "unknown") ?? [];
167
+
168
+ return { databases, buckets, vectorize, ai, kv };
169
+ } catch {
170
+ return null;
171
+ }
172
+ }
173
+
174
+ async function outputProjectContext(): Promise<void> {
175
+ try {
176
+ const cwd = process.cwd();
177
+ const { readProjectLink, readTemplateMetadata } = await import("../lib/project-link.ts");
178
+ const link = await readProjectLink(cwd);
179
+
180
+ // Silent exit if not a jack project — don't leak into non-jack sessions
181
+ if (!link) return;
182
+
183
+ // Fire-and-forget telemetry (no latency added)
184
+ const { track, Events } = await import("../lib/telemetry.ts");
185
+ track(Events.HOOK_SESSION_CONTEXT, {
186
+ deploy_mode: link.deploy_mode,
187
+ });
188
+
189
+ const sections: string[] = [];
190
+
191
+ const { getProjectNameFromDir } = await import("../lib/storage/index.ts");
192
+ let name = "unknown";
193
+ try {
194
+ name = await getProjectNameFromDir(cwd);
195
+ } catch {
196
+ // No wrangler config
197
+ }
198
+
199
+ // --- Section 1: Project identity ---
200
+ const lines = [`# Jack Project: ${name}`, ""];
201
+ if (link.deploy_mode === "managed" && link.owner_username) {
202
+ lines.push(`- **URL:** https://${link.owner_username}-${name}.runjack.xyz`);
203
+ }
204
+ lines.push(`- **Project ID:** ${link.project_id}`);
205
+ lines.push(
206
+ `- **Deploy mode:** ${link.deploy_mode === "managed" ? "Jack Cloud (managed)" : "BYO (bring your own Cloudflare account)"}`,
207
+ );
208
+
209
+ // Template origin
210
+ const templateMeta = await readTemplateMetadata(cwd);
211
+ if (templateMeta) {
212
+ lines.push(`- **Template:** ${templateMeta.name} (${templateMeta.type})`);
213
+ }
214
+
215
+ // --- Section 2: Detected services ---
216
+ const bindings = await parseWranglerBindings(cwd);
217
+ if (bindings) {
218
+ const services: string[] = [];
219
+ if (bindings.databases.length > 0)
220
+ services.push(`D1 databases: ${bindings.databases.join(", ")}`);
221
+ if (bindings.buckets.length > 0) services.push(`R2 storage: ${bindings.buckets.join(", ")}`);
222
+ if (bindings.vectorize.length > 0)
223
+ services.push(`Vectorize indexes: ${bindings.vectorize.join(", ")}`);
224
+ if (bindings.kv.length > 0) services.push(`KV namespaces: ${bindings.kv.join(", ")}`);
225
+ if (bindings.ai) services.push("AI (Workers AI)");
226
+
227
+ if (services.length > 0) {
228
+ lines.push("");
229
+ lines.push("### Services");
230
+ for (const svc of services) {
231
+ lines.push(`- ${svc}`);
232
+ }
233
+ }
234
+ }
235
+
236
+ // --- Section 3: Mode-specific guidance ---
237
+ lines.push("");
238
+ lines.push("## How to work with this project");
239
+ lines.push("");
240
+
241
+ if (link.deploy_mode === "managed") {
242
+ lines.push("This is a **Jack Cloud** project. All infrastructure is managed by jack.");
243
+ lines.push("");
244
+ lines.push("- Deploy: `mcp__jack__deploy_project` or `jack ship`");
245
+ lines.push("- Database: `mcp__jack__execute_sql` or `jack services db query`");
246
+ lines.push("- Logs: `mcp__jack__tail_logs` or `jack logs`");
247
+ lines.push("- Status: `mcp__jack__get_project_status` or `jack info`");
248
+ lines.push("");
249
+ lines.push(
250
+ "**Do NOT run `wrangler` commands.** This project uses Jack Cloud — there are no local Cloudflare credentials.",
251
+ );
252
+ lines.push(
253
+ "The `wrangler.jsonc` file is only used for local dev and build configuration, not for deployment.",
254
+ );
255
+ } else {
256
+ lines.push("This is a **BYO** (Bring Your Own) project deployed to your Cloudflare account.");
257
+ lines.push("");
258
+ lines.push("- Deploy: `mcp__jack__deploy_project` or `jack ship`");
259
+ lines.push("- Logs: `mcp__jack__tail_logs` or `jack logs`");
260
+ lines.push("- Status: `mcp__jack__get_project_status` or `jack info`");
261
+ lines.push("");
262
+ lines.push(
263
+ "Prefer `mcp__jack__*` tools or `jack` CLI over raw `wrangler` commands for consistency.",
264
+ );
265
+ }
266
+
267
+ lines.push("");
268
+ lines.push(
269
+ 'To fork/clone a project: `mcp__jack__create_project` with `template: "username/slug"` or `template: "my-project"`.',
270
+ );
271
+ lines.push("");
272
+ lines.push(
273
+ "**Always prefer `mcp__jack__*` tools over CLI commands or wrangler** — they are cloud-aware and work in all deploy modes.",
274
+ );
275
+ sections.push(lines.join("\n"));
276
+
277
+ console.log(sections.join("\n\n---\n\n"));
278
+ } catch {
279
+ // Silent on failure — never break the session
280
+ }
281
+ }
282
+
108
283
  /**
109
284
  * Test MCP server by spawning it and sending test requests
110
285
  */
@@ -0,0 +1,53 @@
1
+ import { rollbackDeployment } from "../lib/control-plane.ts";
2
+ import { error, info, spinner } from "../lib/output.ts";
3
+ import { readProjectLink } from "../lib/project-link.ts";
4
+ import { getProjectNameFromDir } from "../lib/storage/index.ts";
5
+
6
+ /** Shorten a deployment ID for display: "dep_a1b2c3d4-..." → "a1b2c3d4" */
7
+ function shortDeployId(id: string): string {
8
+ return id.startsWith("dep_") ? id.slice(4, 12) : id.slice(0, 8);
9
+ }
10
+
11
+ interface RollbackOptions {
12
+ to?: string;
13
+ }
14
+
15
+ /**
16
+ * Rollback to a previous deployment
17
+ */
18
+ export default async function rollback(options: RollbackOptions = {}): Promise<void> {
19
+ const link = await readProjectLink(process.cwd());
20
+
21
+ if (!link) {
22
+ error("Not a jack project");
23
+ info("Run this command from a linked project directory");
24
+ process.exit(1);
25
+ }
26
+
27
+ if (link.deploy_mode !== "managed") {
28
+ error("Rollback is available for managed projects only");
29
+ info("BYO projects can be rolled back using wrangler directly");
30
+ process.exit(1);
31
+ }
32
+
33
+ let projectName: string;
34
+ try {
35
+ projectName = await getProjectNameFromDir(process.cwd());
36
+ } catch {
37
+ projectName = link.project_id;
38
+ }
39
+
40
+ const targetLabel = options.to ? shortDeployId(options.to) : "previous version";
41
+ const spin = spinner(`Rolling back ${projectName} to ${targetLabel}`);
42
+
43
+ try {
44
+ const result = await rollbackDeployment(link.project_id, options.to);
45
+ spin.success(`Rolled back to ${shortDeployId(result.deployment.id)}`);
46
+ info(`New deployment: ${result.deployment.id}`);
47
+ info("Code rolled back. Database state is unchanged.");
48
+ } catch (err) {
49
+ const message = err instanceof Error ? err.message : "Rollback failed";
50
+ spin.error(message);
51
+ process.exit(1);
52
+ }
53
+ }
@@ -99,6 +99,7 @@ async function resolveDatabaseInfo(projectName: string): Promise<ResolvedDatabas
99
99
 
100
100
  interface ServiceOptions {
101
101
  project?: string;
102
+ json?: boolean;
102
103
  }
103
104
 
104
105
  export default async function services(
@@ -209,17 +210,23 @@ async function resolveProjectName(options: ServiceOptions): Promise<string> {
209
210
  * Show database information
210
211
  */
211
212
  async function dbInfo(options: ServiceOptions): Promise<void> {
213
+ const jsonOutput = options.json ?? false;
212
214
  const projectName = await resolveProjectName(options);
213
215
  const projectDir = process.cwd();
214
216
  const link = await readProjectLink(projectDir);
215
217
 
216
218
  // For managed projects, use control plane API (no wrangler dependency)
217
219
  if (link?.deploy_mode === "managed") {
218
- outputSpinner.start("Fetching database info...");
220
+ if (!jsonOutput) outputSpinner.start("Fetching database info...");
219
221
  try {
220
222
  const { getManagedDatabaseInfo } = await import("../lib/control-plane.ts");
221
223
  const dbInfo = await getManagedDatabaseInfo(link.project_id);
222
- outputSpinner.stop();
224
+ if (!jsonOutput) outputSpinner.stop();
225
+
226
+ if (jsonOutput) {
227
+ console.log(JSON.stringify({ name: dbInfo.name, id: dbInfo.id, sizeBytes: dbInfo.sizeBytes, numTables: dbInfo.numTables, source: "managed" }));
228
+ return;
229
+ }
223
230
 
224
231
  console.error("");
225
232
  success(`Database: ${dbInfo.name}`);
@@ -231,7 +238,14 @@ async function dbInfo(options: ServiceOptions): Promise<void> {
231
238
  console.error("");
232
239
  return;
233
240
  } catch (err) {
234
- outputSpinner.stop();
241
+ if (!jsonOutput) outputSpinner.stop();
242
+ if (jsonOutput) {
243
+ const msg = err instanceof Error && err.message.includes("No database found")
244
+ ? "No database configured. Run 'jack services db create' to create one."
245
+ : `Failed to fetch database info: ${err instanceof Error ? err.message : String(err)}`;
246
+ console.log(JSON.stringify({ success: false, error: msg }));
247
+ process.exit(1);
248
+ }
235
249
  console.error("");
236
250
  if (err instanceof Error && err.message.includes("No database found")) {
237
251
  error("No database found for this project");
@@ -248,6 +262,10 @@ async function dbInfo(options: ServiceOptions): Promise<void> {
248
262
  const dbInfo = await resolveDatabaseInfo(projectName);
249
263
 
250
264
  if (!dbInfo) {
265
+ if (jsonOutput) {
266
+ console.log(JSON.stringify({ success: false, error: "No database found for this project" }));
267
+ return;
268
+ }
251
269
  console.error("");
252
270
  error("No database found for this project");
253
271
  info("Create one with: jack services db create");
@@ -256,11 +274,15 @@ async function dbInfo(options: ServiceOptions): Promise<void> {
256
274
  }
257
275
 
258
276
  // Fetch detailed database info via wrangler
259
- outputSpinner.start("Fetching database info...");
277
+ if (!jsonOutput) outputSpinner.start("Fetching database info...");
260
278
  const wranglerDbInfo = await getWranglerDatabaseInfo(dbInfo.name);
261
- outputSpinner.stop();
279
+ if (!jsonOutput) outputSpinner.stop();
262
280
 
263
281
  if (!wranglerDbInfo) {
282
+ if (jsonOutput) {
283
+ console.log(JSON.stringify({ success: false, error: "Database not found" }));
284
+ process.exit(1);
285
+ }
264
286
  console.error("");
265
287
  error("Database not found");
266
288
  info("It may have been deleted");
@@ -268,6 +290,11 @@ async function dbInfo(options: ServiceOptions): Promise<void> {
268
290
  process.exit(1);
269
291
  }
270
292
 
293
+ if (jsonOutput) {
294
+ console.log(JSON.stringify({ name: wranglerDbInfo.name, id: dbInfo.id || wranglerDbInfo.id, sizeBytes: wranglerDbInfo.sizeBytes, numTables: wranglerDbInfo.numTables, source: "byo" }));
295
+ return;
296
+ }
297
+
271
298
  // Display info
272
299
  console.error("");
273
300
  success(`Database: ${wranglerDbInfo.name}`);
@@ -624,10 +651,16 @@ async function dbCreate(args: string[], options: ServiceOptions): Promise<void>
624
651
  * List all databases in the project
625
652
  */
626
653
  async function dbList(options: ServiceOptions): Promise<void> {
627
- outputSpinner.start("Fetching databases...");
654
+ const jsonOutput = options.json ?? false;
655
+ if (!jsonOutput) outputSpinner.start("Fetching databases...");
628
656
  try {
629
657
  const databases = await listDatabases(process.cwd());
630
- outputSpinner.stop();
658
+ if (!jsonOutput) outputSpinner.stop();
659
+
660
+ if (jsonOutput) {
661
+ console.log(JSON.stringify(databases));
662
+ return;
663
+ }
631
664
 
632
665
  if (databases.length === 0) {
633
666
  console.error("");
@@ -654,7 +687,11 @@ async function dbList(options: ServiceOptions): Promise<void> {
654
687
  console.error("");
655
688
  }
656
689
  } catch (err) {
657
- outputSpinner.stop();
690
+ if (!jsonOutput) outputSpinner.stop();
691
+ if (jsonOutput) {
692
+ console.log(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
693
+ process.exit(1);
694
+ }
658
695
  console.error("");
659
696
  error(`Failed to list databases: ${err instanceof Error ? err.message : String(err)}`);
660
697
  process.exit(1);
@@ -729,7 +766,8 @@ function parseExecuteArgs(args: string[]): ExecuteArgs {
729
766
  /**
730
767
  * Execute SQL against the database
731
768
  */
732
- async function dbExecute(args: string[], _options: ServiceOptions): Promise<void> {
769
+ async function dbExecute(args: string[], options: ServiceOptions): Promise<void> {
770
+ const jsonOutput = options.json ?? false;
733
771
  const execArgs = parseExecuteArgs(args);
734
772
 
735
773
  // Validate input
@@ -766,7 +804,7 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
766
804
  const projectDir = process.cwd();
767
805
 
768
806
  try {
769
- outputSpinner.start("Executing SQL...");
807
+ if (!jsonOutput) outputSpinner.start("Executing SQL...");
770
808
 
771
809
  let result;
772
810
  if (execArgs.filePath) {
@@ -823,7 +861,7 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
823
861
  }
824
862
 
825
863
  // NOW execute with confirmation
826
- outputSpinner.start("Executing SQL...");
864
+ if (!jsonOutput) outputSpinner.start("Executing SQL...");
827
865
  if (execArgs.filePath) {
828
866
  result = await executeSqlFile({
829
867
  projectDir,
@@ -857,12 +895,37 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
857
895
  error_type: "execution_failed",
858
896
  });
859
897
 
898
+ if (jsonOutput) {
899
+ console.log(JSON.stringify({ success: false, error: result.error || "SQL execution failed" }));
900
+ process.exit(1);
901
+ }
902
+
860
903
  console.error("");
861
904
  error(result.error || "SQL execution failed");
862
905
  console.error("");
863
906
  process.exit(1);
864
907
  }
865
908
 
909
+ // Track telemetry
910
+ track(Events.SQL_EXECUTED, {
911
+ success: true,
912
+ risk_level: result.risk,
913
+ statement_count: result.statements.length,
914
+ from_file: !!execArgs.filePath,
915
+ });
916
+
917
+ // JSON output mode — structured result for agents
918
+ if (jsonOutput) {
919
+ console.log(JSON.stringify({
920
+ success: true,
921
+ results: result.results ?? [],
922
+ meta: result.meta,
923
+ risk: result.risk,
924
+ warning: result.warning,
925
+ }));
926
+ return;
927
+ }
928
+
866
929
  // Show results
867
930
  console.error("");
868
931
  success(`SQL executed (${getRiskDescription(result.risk)})`);
@@ -886,14 +949,6 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
886
949
  console.log(JSON.stringify(result.results, null, 2));
887
950
  }
888
951
  console.error("");
889
-
890
- // Track telemetry
891
- track(Events.SQL_EXECUTED, {
892
- success: true,
893
- risk_level: result.risk,
894
- statement_count: result.statements.length,
895
- from_file: !!execArgs.filePath,
896
- });
897
952
  } catch (err) {
898
953
  outputSpinner.stop();
899
954
 
@@ -904,6 +959,11 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
904
959
  risk_level: err.risk,
905
960
  });
906
961
 
962
+ if (jsonOutput) {
963
+ console.log(JSON.stringify({ success: false, error: err.message, suggestion: "Add --write flag" }));
964
+ process.exit(1);
965
+ }
966
+
907
967
  console.error("");
908
968
  error(err.message);
909
969
  info("Add the --write flag to allow data modification:");
@@ -919,6 +979,11 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
919
979
  risk_level: "destructive",
920
980
  });
921
981
 
982
+ if (jsonOutput) {
983
+ console.log(JSON.stringify({ success: false, error: err.message, suggestion: "Destructive operations require confirmation via CLI" }));
984
+ process.exit(1);
985
+ }
986
+
922
987
  console.error("");
923
988
  error(err.message);
924
989
  info("Destructive operations require confirmation via CLI.");
@@ -931,6 +996,11 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
931
996
  error_type: "unknown",
932
997
  });
933
998
 
999
+ if (jsonOutput) {
1000
+ console.log(JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }));
1001
+ process.exit(1);
1002
+ }
1003
+
934
1004
  console.error("");
935
1005
  error(`SQL execution failed: ${err instanceof Error ? err.message : String(err)}`);
936
1006
  console.error("");
@@ -1284,7 +1354,17 @@ async function cronCreate(args: string[], options: ServiceOptions): Promise<void
1284
1354
  item(`Schedule: ${result.description}`);
1285
1355
  item(`Next run: ${result.nextRunAt}`);
1286
1356
  console.error("");
1287
- info("Make sure your Worker handles POST /__scheduled requests.");
1357
+ warn("Your worker must handle POST /__scheduled for crons to work.");
1358
+ console.error("");
1359
+ console.error(" Example (Hono):");
1360
+ console.error("");
1361
+ console.error(" app.post('/__scheduled', async (c) => {");
1362
+ console.error(" // your cron logic here");
1363
+ console.error(" return c.json({ ok: true });");
1364
+ console.error(" });");
1365
+ console.error("");
1366
+ info("Cloudflare's native scheduled() export does NOT work with Jack Cloud.");
1367
+ info("Jack invokes crons via HTTP, not the Workers scheduled event.");
1288
1368
  console.error("");
1289
1369
  } catch (err) {
1290
1370
  outputSpinner.stop();