@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getjack/jack",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
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
+ }
@@ -219,11 +219,13 @@ async function setSecret(args: string[], options: SecretsOptions): Promise<void>
219
219
  */
220
220
  async function setSecretManaged(projectId: string, name: string, value: string): Promise<void> {
221
221
  const { authFetch } = await import("../lib/auth/index.ts");
222
+ const { encryptSecretValue } = await import("../lib/crypto.ts");
222
223
 
224
+ const encryptedValue = await encryptSecretValue(value);
223
225
  const response = await authFetch(`${getControlApiUrl()}/v1/projects/${projectId}/secrets`, {
224
226
  method: "POST",
225
227
  headers: { "Content-Type": "application/json" },
226
- body: JSON.stringify({ name, value }),
228
+ body: JSON.stringify({ name, value: encryptedValue }),
227
229
  });
228
230
 
229
231
  if (!response.ok) {
@@ -1354,7 +1354,17 @@ async function cronCreate(args: string[], options: ServiceOptions): Promise<void
1354
1354
  item(`Schedule: ${result.description}`);
1355
1355
  item(`Next run: ${result.nextRunAt}`);
1356
1356
  console.error("");
1357
- 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.");
1358
1368
  console.error("");
1359
1369
  } catch (err) {
1360
1370
  outputSpinner.stop();
@@ -3,7 +3,7 @@ import { createReporter, output } from "../lib/output.ts";
3
3
  import { deployProject } from "../lib/project-operations.ts";
4
4
 
5
5
  export default async function ship(
6
- options: { managed?: boolean; byo?: boolean; dryRun?: boolean; json?: boolean } = {},
6
+ options: { managed?: boolean; byo?: boolean; dryRun?: boolean; json?: boolean; message?: string } = {},
7
7
  ): Promise<void> {
8
8
  const isCi = process.env.CI === "true" || process.env.CI === "1";
9
9
  const jsonOutput = options.json ?? false;
@@ -17,6 +17,7 @@ export default async function ship(
17
17
  managed: options.managed,
18
18
  byo: options.byo,
19
19
  dryRun: options.dryRun,
20
+ message: options.message,
20
21
  });
21
22
 
22
23
  if (jsonOutput) {
@@ -26,6 +27,7 @@ export default async function ship(
26
27
  projectName: result.projectName,
27
28
  url: result.workerUrl,
28
29
  deployMode: result.deployMode,
30
+ ...(options.message && { message: options.message }),
29
31
  }),
30
32
  );
31
33
  return;
@@ -51,6 +51,7 @@ function showHelp(): void {
51
51
  console.error("");
52
52
  console.error("Commands:");
53
53
  console.error(" create [name] Create a new API token");
54
+ console.error(" --expires <days> Token expires after N days");
54
55
  console.error(" list List active tokens");
55
56
  console.error(" revoke <id> Revoke a token");
56
57
  console.error("");
@@ -69,7 +70,18 @@ async function createToken(args: string[], flags: Record<string, unknown> = {}):
69
70
  name = args[0];
70
71
  }
71
72
 
72
- const data = await createApiToken(name);
73
+ // Parse --expires <days> flag
74
+ let expiresInDays: number | undefined;
75
+ if (flags.expires !== undefined) {
76
+ const parsed = Number(flags.expires);
77
+ if (!Number.isInteger(parsed) || parsed <= 0) {
78
+ error("--expires must be a positive integer (number of days)");
79
+ process.exit(1);
80
+ }
81
+ expiresInDays = parsed;
82
+ }
83
+
84
+ const data = await createApiToken(name, expiresInDays);
73
85
 
74
86
  track(Events.TOKEN_CREATED);
75
87
 
@@ -78,6 +90,9 @@ async function createToken(args: string[], flags: Record<string, unknown> = {}):
78
90
  console.error(` ${data.token}`);
79
91
  console.error("");
80
92
  console.error(" Save this token -- it will not be shown again.");
93
+ if (data.expires_at) {
94
+ console.error(` Expires: ${data.expires_at}`);
95
+ }
81
96
  console.error("");
82
97
  console.error(" Usage:");
83
98
  console.error(" export JACK_API_TOKEN=<token>");
@@ -1,28 +1,63 @@
1
+ import { authFetch } from "../lib/auth/index.ts";
1
2
  import { getCredentials } from "../lib/auth/store.ts";
2
- import { info, item, success } from "../lib/output.ts";
3
+ import { getControlApiUrl } from "../lib/control-plane.ts";
4
+ import { error, info, item, success } from "../lib/output.ts";
3
5
 
4
6
  export default async function whoami(): Promise<void> {
7
+ const apiToken = process.env.JACK_API_TOKEN;
5
8
  const creds = await getCredentials();
6
9
 
7
- if (!creds) {
10
+ if (!apiToken && !creds) {
8
11
  info("Not logged in");
9
12
  info("Run 'jack login' to sign in");
10
13
  return;
11
14
  }
12
15
 
13
16
  console.error("");
17
+
18
+ if (apiToken && !creds) {
19
+ // Token-only: fetch user info from control plane
20
+ try {
21
+ const res = await authFetch(`${getControlApiUrl()}/v1/me`);
22
+ if (!res.ok) {
23
+ error("API token is invalid or expired");
24
+ return;
25
+ }
26
+ const data = (await res.json()) as {
27
+ user?: { email?: string; id?: string; first_name?: string; last_name?: string };
28
+ };
29
+ if (data.user) {
30
+ success("Logged in");
31
+ if (data.user.email) item(`Email: ${data.user.email}`);
32
+ if (data.user.id) item(`ID: ${data.user.id}`);
33
+ if (data.user.first_name) {
34
+ item(`Name: ${data.user.first_name}${data.user.last_name ? ` ${data.user.last_name}` : ""}`);
35
+ }
36
+ } else {
37
+ success("Authenticated");
38
+ }
39
+ item(`Auth: API token (${apiToken.slice(4, 12)}...)`);
40
+ } catch {
41
+ error("Failed to reach control plane");
42
+ item(`Auth: API token (${apiToken.slice(4, 12)}...)`);
43
+ }
44
+ console.error("");
45
+ return;
46
+ }
47
+
48
+ // Has stored creds (with or without API token)
14
49
  success("Logged in");
15
- item(`Email: ${creds.user.email}`);
16
- item(`ID: ${creds.user.id}`);
50
+ item(`Email: ${creds!.user.email}`);
51
+ item(`ID: ${creds!.user.id}`);
17
52
 
18
- if (creds.user.first_name) {
19
- item(`Name: ${creds.user.first_name}${creds.user.last_name ? ` ${creds.user.last_name}` : ""}`);
53
+ if (creds!.user.first_name) {
54
+ item(`Name: ${creds!.user.first_name}${creds!.user.last_name ? ` ${creds!.user.last_name}` : ""}`);
20
55
  }
21
56
 
22
- if (process.env.JACK_API_TOKEN) {
57
+ if (apiToken) {
23
58
  item("Auth: API token");
24
59
  } else {
25
- const expiresIn = creds.expires_at - Math.floor(Date.now() / 1000);
60
+ const expiresIn = creds!.expires_at - Math.floor(Date.now() / 1000);
26
61
  if (expiresIn > 0) {
27
62
  const hours = Math.floor(expiresIn / 3600);
28
63
  const minutes = Math.floor((expiresIn % 3600) / 60);
package/src/index.ts CHANGED
@@ -18,10 +18,12 @@ const cli = meow(
18
18
  new <name> [path] Create and deploy a project
19
19
  vibe "<phrase>" Create from an idea
20
20
  ship Push changes to production
21
+ rollback Roll back to previous deploy
21
22
 
22
23
  Projects
23
24
  open [name] Open in browser
24
25
  logs Stream live logs
26
+ deploys List recent deployments
25
27
  down [name] Undeploy from cloud
26
28
  ls List all projects
27
29
  info [name] Show project details
@@ -185,6 +187,9 @@ const cli = meow(
185
187
  name: {
186
188
  type: "string",
187
189
  },
190
+ to: {
191
+ type: "string",
192
+ },
188
193
  },
189
194
  },
190
195
  );
@@ -292,6 +297,7 @@ try {
292
297
  byo: cli.flags.byo,
293
298
  dryRun: cli.flags.dryRun,
294
299
  json: cli.flags.json,
300
+ message: cli.flags.message,
295
301
  });
296
302
  break;
297
303
  }
@@ -301,6 +307,16 @@ try {
301
307
  await withTelemetry("logs", logs)({ label: cli.flags.label });
302
308
  break;
303
309
  }
310
+ case "deploys": {
311
+ const { default: deploys } = await import("./commands/deploys.ts");
312
+ await withTelemetry("deploys", deploys)({ all: cli.flags.all });
313
+ break;
314
+ }
315
+ case "rollback": {
316
+ const { default: rollback } = await import("./commands/rollback.ts");
317
+ await withTelemetry("rollback", rollback)({ to: cli.flags.to });
318
+ break;
319
+ }
304
320
  case "agents": {
305
321
  const { default: agents } = await import("./commands/agents.ts");
306
322
  await withTelemetry("agents", agents, { subcommand: args[0] })(args[0], args.slice(1), {