@getjack/jack 0.1.28 → 0.1.30

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 (125) hide show
  1. package/package.json +1 -1
  2. package/src/commands/cd.ts +163 -0
  3. package/src/commands/clone.ts +112 -68
  4. package/src/commands/domain.ts +506 -0
  5. package/src/commands/domains.ts +215 -0
  6. package/src/commands/down.ts +18 -12
  7. package/src/commands/hack.ts +185 -8
  8. package/src/commands/init.ts +52 -1
  9. package/src/commands/link.ts +25 -43
  10. package/src/commands/logs.ts +2 -2
  11. package/src/commands/mcp.ts +74 -3
  12. package/src/commands/new.ts +48 -54
  13. package/src/commands/projects.ts +53 -10
  14. package/src/commands/secrets.ts +5 -1
  15. package/src/commands/services.ts +16 -4
  16. package/src/commands/shell-init.ts +43 -0
  17. package/src/commands/ship.ts +2 -11
  18. package/src/commands/skills.ts +335 -0
  19. package/src/commands/update.ts +31 -0
  20. package/src/commands/upgrade.ts +14 -0
  21. package/src/index.ts +116 -24
  22. package/src/lib/agent-integration.ts +1 -2
  23. package/src/lib/agents.ts +2 -2
  24. package/src/lib/auth/login-flow.ts +1 -1
  25. package/src/lib/clone-core.ts +252 -0
  26. package/src/lib/config.ts +22 -0
  27. package/src/lib/control-plane.ts +31 -5
  28. package/src/lib/fuzzy.ts +93 -0
  29. package/src/lib/managed-deploy.ts +4 -1
  30. package/src/lib/managed-down.ts +20 -5
  31. package/src/lib/output.ts +90 -9
  32. package/src/lib/picker.ts +406 -0
  33. package/src/lib/project-detection.ts +5 -2
  34. package/src/lib/project-list.ts +66 -5
  35. package/src/lib/project-operations.ts +68 -6
  36. package/src/lib/prompts.ts +1 -1
  37. package/src/lib/services/db-execute.ts +8 -1
  38. package/src/lib/services/db-list.ts +4 -1
  39. package/src/lib/services/domain-operations.ts +379 -0
  40. package/src/lib/services/storage-config.ts +1 -5
  41. package/src/lib/services/storage-delete.ts +1 -1
  42. package/src/lib/services/storage-info.ts +2 -4
  43. package/src/lib/services/vectorize-config.ts +1 -5
  44. package/src/lib/services/vectorize-create.ts +3 -1
  45. package/src/lib/shell-integration.ts +202 -0
  46. package/src/lib/telemetry-config.ts +50 -4
  47. package/src/lib/telemetry.ts +71 -2
  48. package/src/lib/version-check.ts +1 -3
  49. package/src/lib/wrangler-config.test.ts +2 -2
  50. package/src/lib/wrangler-config.ts +1 -1
  51. package/src/lib/zip-packager.ts +1 -3
  52. package/src/mcp/tools/index.ts +261 -7
  53. package/src/templates/index.ts +10 -1
  54. package/templates/ai-chat/.jack.json +1 -5
  55. package/templates/ai-chat/public/chat.js +130 -130
  56. package/templates/ai-chat/src/index.ts +9 -13
  57. package/templates/ai-chat/src/jack-ai.ts +6 -2
  58. package/templates/saas/.jack.json +6 -1
  59. package/templates/saas/src/auth.ts +8 -4
  60. package/templates/saas/src/client/App.tsx +22 -7
  61. package/templates/saas/src/client/components/ProtectedRoute.tsx +9 -2
  62. package/templates/saas/src/client/components/ThemeToggle.tsx +1 -6
  63. package/templates/saas/src/client/components/ui/accordion.tsx +1 -1
  64. package/templates/saas/src/client/components/ui/alert-dialog.tsx +2 -2
  65. package/templates/saas/src/client/components/ui/alert.tsx +2 -2
  66. package/templates/saas/src/client/components/ui/avatar.tsx +1 -1
  67. package/templates/saas/src/client/components/ui/badge.tsx +2 -2
  68. package/templates/saas/src/client/components/ui/breadcrumb.tsx +1 -1
  69. package/templates/saas/src/client/components/ui/button-group.tsx +2 -2
  70. package/templates/saas/src/client/components/ui/button.tsx +2 -2
  71. package/templates/saas/src/client/components/ui/card.tsx +1 -1
  72. package/templates/saas/src/client/components/ui/carousel.tsx +2 -2
  73. package/templates/saas/src/client/components/ui/checkbox.tsx +1 -1
  74. package/templates/saas/src/client/components/ui/command.tsx +2 -2
  75. package/templates/saas/src/client/components/ui/context-menu.tsx +1 -1
  76. package/templates/saas/src/client/components/ui/dialog.tsx +1 -1
  77. package/templates/saas/src/client/components/ui/drawer.tsx +1 -1
  78. package/templates/saas/src/client/components/ui/dropdown-menu.tsx +1 -1
  79. package/templates/saas/src/client/components/ui/empty.tsx +1 -1
  80. package/templates/saas/src/client/components/ui/field.tsx +2 -2
  81. package/templates/saas/src/client/components/ui/form.tsx +5 -5
  82. package/templates/saas/src/client/components/ui/hover-card.tsx +1 -1
  83. package/templates/saas/src/client/components/ui/input-group.tsx +3 -3
  84. package/templates/saas/src/client/components/ui/input-otp.tsx +1 -1
  85. package/templates/saas/src/client/components/ui/input.tsx +1 -1
  86. package/templates/saas/src/client/components/ui/item.tsx +3 -3
  87. package/templates/saas/src/client/components/ui/label.tsx +1 -1
  88. package/templates/saas/src/client/components/ui/menubar.tsx +1 -1
  89. package/templates/saas/src/client/components/ui/navigation-menu.tsx +1 -1
  90. package/templates/saas/src/client/components/ui/pagination.tsx +2 -2
  91. package/templates/saas/src/client/components/ui/popover.tsx +1 -1
  92. package/templates/saas/src/client/components/ui/progress.tsx +1 -1
  93. package/templates/saas/src/client/components/ui/radio-group.tsx +1 -1
  94. package/templates/saas/src/client/components/ui/resizable.tsx +1 -1
  95. package/templates/saas/src/client/components/ui/scroll-area.tsx +1 -1
  96. package/templates/saas/src/client/components/ui/select.tsx +1 -1
  97. package/templates/saas/src/client/components/ui/separator.tsx +1 -1
  98. package/templates/saas/src/client/components/ui/sheet.tsx +1 -1
  99. package/templates/saas/src/client/components/ui/sidebar.tsx +4 -4
  100. package/templates/saas/src/client/components/ui/slider.tsx +1 -1
  101. package/templates/saas/src/client/components/ui/switch.tsx +1 -1
  102. package/templates/saas/src/client/components/ui/table.tsx +1 -1
  103. package/templates/saas/src/client/components/ui/tabs.tsx +1 -1
  104. package/templates/saas/src/client/components/ui/textarea.tsx +1 -1
  105. package/templates/saas/src/client/components/ui/toggle-group.tsx +3 -3
  106. package/templates/saas/src/client/components/ui/toggle.tsx +2 -2
  107. package/templates/saas/src/client/components/ui/tooltip.tsx +1 -1
  108. package/templates/saas/src/client/hooks/useSubscription.ts +5 -4
  109. package/templates/saas/src/client/lib/auth-client.ts +1 -1
  110. package/templates/saas/src/client/lib/plans.ts +1 -6
  111. package/templates/saas/src/client/lib/utils.ts +1 -1
  112. package/templates/saas/src/client/main.tsx +1 -1
  113. package/templates/saas/src/client/pages/DashboardPage.tsx +41 -9
  114. package/templates/saas/src/client/pages/ForgotPasswordPage.tsx +11 -2
  115. package/templates/saas/src/client/pages/HomePage.tsx +11 -2
  116. package/templates/saas/src/client/pages/LoginPage.tsx +11 -2
  117. package/templates/saas/src/client/pages/PricingPage.tsx +20 -10
  118. package/templates/saas/src/client/pages/ResetPasswordPage.tsx +14 -11
  119. package/templates/saas/src/client/pages/SignupPage.tsx +11 -2
  120. package/templates/saas/src/index.ts +28 -19
  121. package/templates/saas/vite.config.ts +1 -1
  122. package/templates/semantic-search/.jack.json +1 -5
  123. package/templates/semantic-search/src/index.ts +8 -4
  124. package/templates/semantic-search/src/jack-ai.ts +6 -2
  125. package/templates/semantic-search/src/jack-vectorize.ts +5 -1
@@ -1,3 +1,4 @@
1
+ import { mkdirSync } from "node:fs";
1
2
  import { join } from "node:path";
2
3
  import { getAuthState } from "../lib/auth/index.ts";
3
4
  import {
@@ -6,6 +7,7 @@ import {
6
7
  deleteWorker,
7
8
  exportDatabase,
8
9
  } from "../lib/cloudflare-api.ts";
10
+ import { getJackHome } from "../lib/config.ts";
9
11
  import { fetchProjectResources } from "../lib/control-plane.ts";
10
12
  import { promptSelect } from "../lib/hooks.ts";
11
13
  import { managedDown } from "../lib/managed-down.ts";
@@ -88,7 +90,7 @@ export default async function down(projectName?: string, flags: DownFlags = {}):
88
90
  // Check if found only on control plane (orphaned managed project)
89
91
  if (resolved?.sources.controlPlane && !resolved.sources.filesystem) {
90
92
  console.error("");
91
- info(`Found "${name}" on jack cloud, linking locally...`);
93
+ info(`Found "${name}" on jack cloud`);
92
94
  }
93
95
 
94
96
  // Guard against mismatched resolutions when an explicit name is provided
@@ -115,6 +117,7 @@ export default async function down(projectName?: string, flags: DownFlags = {}):
115
117
  // Get the project ID from link or resolved data
116
118
  const projectId = link?.project_id || resolved?.remote?.projectId;
117
119
  const runjackUrl = resolved?.url || null;
120
+ const localPath = resolved?.localPath || null;
118
121
 
119
122
  if (!projectId) {
120
123
  error("Cannot determine project ID for managed deletion");
@@ -122,18 +125,19 @@ export default async function down(projectName?: string, flags: DownFlags = {}):
122
125
  }
123
126
 
124
127
  // Route to managed deletion flow
125
- const deleteSuccess = await managedDown({ projectId, runjackUrl }, name, flags);
128
+ const deleteSuccess = await managedDown({ projectId, runjackUrl, localPath }, name, flags);
126
129
  if (!deleteSuccess) {
127
130
  process.exit(0); // User cancelled
128
131
  }
129
132
 
130
- // Clean up local tracking state
131
- const localPath = resolved?.localPath || process.cwd();
132
- try {
133
- await unlinkProject(localPath);
134
- await unregisterPath(projectId, localPath);
135
- } catch {
136
- // Non-fatal: local cleanup failed but cloud deletion succeeded
133
+ // Clean up local tracking state (only if project has local path)
134
+ if (resolved?.localPath) {
135
+ try {
136
+ await unlinkProject(resolved.localPath);
137
+ await unregisterPath(projectId, resolved.localPath);
138
+ } catch {
139
+ // Non-fatal: local cleanup failed but cloud deletion succeeded
140
+ }
137
141
  }
138
142
  return;
139
143
  }
@@ -203,8 +207,10 @@ export default async function down(projectName?: string, flags: DownFlags = {}):
203
207
  // Auto-export database if it exists (no prompt)
204
208
  let exportPath: string | null = null;
205
209
  if (dbName) {
206
- exportPath = join(process.cwd(), `${dbName}-backup.sql`);
207
- output.start(`Exporting database to ${exportPath}...`);
210
+ const backupDir = resolved?.localPath ?? join(getJackHome(), name);
211
+ mkdirSync(backupDir, { recursive: true });
212
+ exportPath = join(backupDir, `${dbName}-backup.sql`);
213
+ output.start("Exporting database...");
208
214
  try {
209
215
  await exportDatabase(dbName, exportPath);
210
216
  output.stop();
@@ -267,7 +273,7 @@ export default async function down(projectName?: string, flags: DownFlags = {}):
267
273
  console.error("");
268
274
  success(`Undeployed '${name}'`);
269
275
  if (exportPath) {
270
- info(`Backup saved to ./${dbName}-backup.sql`);
276
+ info(`Database backup: ${exportPath}`);
271
277
  }
272
278
  console.error("");
273
279
  } catch (err) {
@@ -1,18 +1,195 @@
1
+ const reset = "\x1b[0m";
2
+ const bright = "\x1b[1m";
3
+ const dim = "\x1b[2m";
4
+ const cyan = "\x1b[38;2;0;255;255m";
5
+ const dimCyan = "\x1b[38;2;0;180;180m";
6
+ const green = "\x1b[38;2;0;200;0m";
7
+
8
+ const matrixChars = "░▒▓█▀▄■□◆◇●◐◑◒◓ヲアウエオカキクケコ";
9
+
10
+ function randomChar(): string {
11
+ return matrixChars[Math.floor(Math.random() * matrixChars.length)];
12
+ }
13
+
14
+ async function sleep(ms: number) {
15
+ await new Promise((r) => setTimeout(r, ms));
16
+ }
17
+
18
+ async function type(text: string, delay = 30) {
19
+ for (const char of text) {
20
+ process.stdout.write(char);
21
+ await sleep(delay);
22
+ }
23
+ }
24
+
25
+ async function typeLine(text: string, delay = 30) {
26
+ await type(text, delay);
27
+ console.log();
28
+ }
29
+
30
+ async function matrixDecode(text: string, indent = "") {
31
+ const frames = 30;
32
+ const delay = 50;
33
+ const len = text.length;
34
+
35
+ for (let frame = 0; frame <= frames; frame++) {
36
+ const resolvedCount = Math.floor((frame / frames) * len);
37
+ let line = indent;
38
+
39
+ for (let i = 0; i < len; i++) {
40
+ if (i < resolvedCount) {
41
+ line += `${bright}${cyan}${text[i]}${reset}`;
42
+ } else {
43
+ line += `${dimCyan}${randomChar()}${reset}`;
44
+ }
45
+ }
46
+
47
+ process.stdout.write(`\x1b[2K\r${line}`);
48
+ await sleep(delay);
49
+ }
50
+
51
+ process.stdout.write(`\x1b[2K\r${indent}${bright}${cyan}${text}${reset}\n`);
52
+ }
53
+
54
+ async function bootSequence() {
55
+ const modulePool = [
56
+ "consciousness.ko",
57
+ "cyberspace.ko",
58
+ "deploy.ko",
59
+ "neural.ko",
60
+ "ice-breaker.ko",
61
+ "daemon.ko",
62
+ "matrix.ko",
63
+ "decrypt.ko",
64
+ "intrusion.ko",
65
+ "phantom.ko",
66
+ ];
67
+ const modules = pickRandom(modulePool, 3);
68
+
69
+ console.log();
70
+ console.log(`${dim}JACK OS v1.0${reset}`);
71
+ console.log(`${dim}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${reset}`);
72
+ await sleep(300);
73
+
74
+ await typeLine(`${green}Initializing...${reset}`, 20);
75
+ for (const mod of modules) {
76
+ await type(` ${dim}├─${reset} ${mod}`, 15);
77
+ await sleep(100 + Math.random() * 200);
78
+ console.log(` ${green}[OK]${reset}`);
79
+ }
80
+ await sleep(200);
81
+ }
82
+
83
+ function pickRandom<T>(arr: T[], count: number): T[] {
84
+ const shuffled = [...arr].sort(() => Math.random() - 0.5);
85
+ return shuffled.slice(0, count);
86
+ }
87
+
88
+ async function traceRoute() {
89
+ const username = process.env.USER || process.env.USERNAME || "user";
90
+
91
+ const serverPool = [
92
+ "node.chiba.city",
93
+ "relay.freeside.orbital",
94
+ "proxy.night.city",
95
+ "vault.zaibatsu.corp",
96
+ "node.sprawl.net",
97
+ "relay.tessier-ashpool.ice",
98
+ "cache.construct.sim",
99
+ "gate.zion.cluster",
100
+ "hub.screaming.fist",
101
+ "core.wintermute.ai",
102
+ "edge.straylight.run",
103
+ "sync.maelstrom.net",
104
+ ];
105
+
106
+ const midHops = pickRandom(serverPool, 3);
107
+ const hops = [
108
+ ["localhost", "0.1"],
109
+ [`${username}.meat.space`, String(10 + Math.floor(Math.random() * 20))],
110
+ [midHops[0], String(50 + Math.floor(Math.random() * 50))],
111
+ [midHops[1], String(100 + Math.floor(Math.random() * 100))],
112
+ [midHops[2], "███"],
113
+ ];
114
+
115
+ console.log();
116
+ await typeLine(`${green}Tracing route...${reset}`, 20);
117
+
118
+ for (let i = 0; i < hops.length; i++) {
119
+ const [host, ms] = hops[i];
120
+ await type(` ${dim}${i + 1}${reset} ${host}`, 10);
121
+ await sleep(150 + Math.random() * 300);
122
+ console.log(` ${dimCyan}${ms}ms${reset}`);
123
+ }
124
+ await sleep(200);
125
+ }
126
+
127
+ async function sshConnect(target: string) {
128
+ console.log();
129
+ await typeLine(`${green}Connecting to ${target}:22...${reset}`, 15);
130
+ await sleep(300);
131
+ await type(`${dim}RSA fingerprint: ${reset}${dimCyan}`);
132
+ for (let i = 0; i < 12; i++) {
133
+ process.stdout.write(randomChar());
134
+ await sleep(30);
135
+ }
136
+ console.log(`${reset}`);
137
+ await sleep(400);
138
+ console.log(`${bright}${green}ACCESS GRANTED${reset}`);
139
+ await sleep(500);
140
+ }
141
+
1
142
  export default async function hack(): Promise<void> {
2
143
  const quotes = [
144
+ // Sneakers (1992)
145
+ "The world isn't run by weapons anymore. It's run by ones and zeroes.",
146
+ "No more secrets.",
3
147
  // Gibson - Neuromancer
4
148
  "The sky above the port was the color of television, tuned to a dead channel.",
5
149
  "Cyberspace. A consensual hallucination.",
6
150
  // Gibson - various
7
151
  "The future is already here — it's just not evenly distributed.",
8
- "The street finds its own uses for things.",
9
- // Sterling
10
- "Anything that can be done to a rat can be done to a human being.",
11
- // Stephenson - Snow Crash
12
- "The Metaverse. It's a fictional structure made out of code.",
13
- // Generic cyberpunk vibes
14
- "We are all cyborgs now.",
152
+ // Cyberpunk 2077 - Johnny Silverhand
153
+ "Wake the fuck up, Samurai. We have a city to burn.",
154
+ // Hackers (1995)
155
+ "Mess with the best, die like the rest.",
156
+ "Hack the planet!",
157
+ // Hacker wisdom
158
+ "Playfully doing something difficult, whether useful or not, that is hacking.",
159
+ "There's nothing more permanent than a temporary hack.",
160
+ // jack philosophy - from SPIRIT.md
161
+ "Context-switching to dashboards is violence.",
162
+ "GUIs are for browsing. CLIs are for flow.",
163
+ "The best infrastructure is invisible.",
164
+ "Every friction point is a creative thought lost.",
165
+ "Create and ship before your first commit.",
166
+ "Don't punish exploration. Creation is free.",
15
167
  ];
16
168
  const quote = quotes[Math.floor(Math.random() * quotes.length)];
17
- console.error(`\n "${quote}"\n`);
169
+
170
+ const serverPool = [
171
+ "node.chiba.city",
172
+ "relay.freeside.orbital",
173
+ "proxy.night.city",
174
+ "vault.zaibatsu.corp",
175
+ "node.sprawl.net",
176
+ "relay.tessier-ashpool.ice",
177
+ "cache.construct.sim",
178
+ "gate.zion.cluster",
179
+ "hub.screaming.fist",
180
+ "core.wintermute.ai",
181
+ "edge.straylight.run",
182
+ "sync.maelstrom.net",
183
+ ];
184
+ const target = serverPool[Math.floor(Math.random() * serverPool.length)];
185
+
186
+ await bootSequence();
187
+ await traceRoute();
188
+ await sshConnect(target);
189
+
190
+ console.log();
191
+ console.log(`${dim}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${reset}`);
192
+ console.log();
193
+ await matrixDecode(`"${quote}"`);
194
+ console.log();
18
195
  }
@@ -5,8 +5,19 @@ import {
5
5
  updateAgent,
6
6
  } from "../lib/agents.ts";
7
7
  import { readConfig, writeConfig } from "../lib/config.ts";
8
+ import { promptSelect } from "../lib/hooks.ts";
8
9
  import { getAppDisplayName, installMcpConfigsToAllApps, saveMcpConfig } from "../lib/mcp-config.ts";
9
10
  import { info, item, spinner, success } from "../lib/output.ts";
11
+ import {
12
+ detectShell,
13
+ getRcFileName,
14
+ getRcFilePath,
15
+ getShellFileDisplayPath,
16
+ getShellName,
17
+ hasLegacyInstall,
18
+ install as installShellIntegration,
19
+ isInstalled as isShellIntegrationInstalled,
20
+ } from "../lib/shell-integration.ts";
10
21
  import { ensureAuth, ensureWrangler, isAuthenticated } from "../lib/wrangler.ts";
11
22
 
12
23
  export async function isInitialized(): Promise<boolean> {
@@ -122,7 +133,47 @@ export default async function init(options: InitOptions = {}): Promise<void> {
122
133
  }
123
134
  }
124
135
 
125
- // Step 5: Save config (preserve existing agents, just update init status)
136
+ // Step 5: Shell integration
137
+ const shell = detectShell();
138
+ const rcFile = getRcFilePath(shell);
139
+
140
+ if (rcFile && shell !== "unknown") {
141
+ const alreadyInstalled = isShellIntegrationInstalled(rcFile);
142
+ const hasLegacy = hasLegacyInstall(rcFile);
143
+
144
+ if (alreadyInstalled && !hasLegacy) {
145
+ // Already installed
146
+ } else if (hasLegacy) {
147
+ console.error("");
148
+ info("Upgrading shell integration...");
149
+ try {
150
+ const result = installShellIntegration(rcFile);
151
+ if (result.migrated) {
152
+ success(`Upgraded to ${getShellFileDisplayPath()}`);
153
+ info(`Restart your terminal or run: source ~/${getRcFileName(rcFile)}`);
154
+ }
155
+ } catch {
156
+ info("Could not upgrade shell integration (non-critical)");
157
+ }
158
+ } else {
159
+ console.error("");
160
+ info("Enable 'jack cd' and 'jack new' to auto-change directories?");
161
+ const choice = await promptSelect(["Yes", "No"]);
162
+
163
+ if (choice === 0) {
164
+ try {
165
+ installShellIntegration(rcFile);
166
+ success(`Added to ~/${getRcFileName(rcFile)}`);
167
+ info(`Restart your terminal or run: source ~/${getRcFileName(rcFile)}`);
168
+ } catch {
169
+ info("Could not update shell config (non-critical)");
170
+ info('Add manually: eval "$(jack shell-init)"');
171
+ }
172
+ }
173
+ }
174
+ }
175
+
176
+ // Step 6: Save config (preserve existing agents, just update init status)
126
177
  const existingConfig = await readConfig();
127
178
  await writeConfig({
128
179
  version: 1,
@@ -9,14 +9,10 @@
9
9
 
10
10
  import { existsSync } from "node:fs";
11
11
  import { isLoggedIn } from "../lib/auth/index.ts";
12
- import { isCancel, promptSelectValue } from "../lib/hooks.ts";
13
- import {
14
- type ManagedProject,
15
- findProjectBySlug,
16
- listManagedProjects,
17
- } from "../lib/control-plane.ts";
12
+ import { findProjectById, findProjectBySlug } from "../lib/control-plane.ts";
18
13
  import { error, info, output, success } from "../lib/output.ts";
19
14
  import { registerPath } from "../lib/paths-index.ts";
15
+ import { pickProject, requireTTY } from "../lib/picker.ts";
20
16
  import { generateByoProjectId, linkProject, readProjectLink } from "../lib/project-link.ts";
21
17
 
22
18
  export interface LinkFlags {
@@ -27,8 +23,17 @@ export default async function link(projectName?: string, flags: LinkFlags = {}):
27
23
  // Check if already linked
28
24
  const existingLink = await readProjectLink(process.cwd());
29
25
  if (existingLink) {
26
+ // Try to look up project name for better UX
27
+ let projectDisplay = existingLink.project_id;
28
+ if (existingLink.deploy_mode === "managed") {
29
+ const project = await findProjectById(existingLink.project_id);
30
+ if (project) {
31
+ projectDisplay = project.slug;
32
+ }
33
+ }
34
+
30
35
  error("This directory is already linked");
31
- info(`Project ID: ${existingLink.project_id}`);
36
+ info(`Linked to: ${projectDisplay}`);
32
37
  info("To re-link, first run: jack unlink");
33
38
  process.exit(1);
34
39
  }
@@ -105,51 +110,28 @@ export default async function link(projectName?: string, flags: LinkFlags = {}):
105
110
  return;
106
111
  }
107
112
 
108
- // Interactive mode - list and select project
109
- output.start("Loading projects...");
110
- let projects: ManagedProject[] = [];
111
- try {
112
- projects = await listManagedProjects();
113
- } catch (err) {
114
- output.stop();
115
- error("Failed to load projects");
116
- if (err instanceof Error) {
117
- info(err.message);
118
- }
119
- process.exit(1);
120
- }
121
- output.stop();
122
-
123
- if (projects.length === 0) {
124
- error("No projects found");
125
- info("Create one with: jack new");
126
- info("Or link to your Cloudflare account: jack link --byo");
127
- process.exit(1);
128
- }
113
+ // Interactive mode - use fuzzy picker with cloud-only projects
114
+ requireTTY();
129
115
 
130
- console.error("");
131
- const choice = await promptSelectValue(
132
- "Select a project to link:",
133
- projects.map((p) => ({
134
- value: p.id,
135
- label: `${p.slug} (${p.status})`,
136
- })),
137
- );
116
+ const result = await pickProject({ cloudOnly: true });
138
117
 
139
- if (isCancel(choice)) {
118
+ if (result.action === "cancel") {
140
119
  info("Cancelled");
141
120
  process.exit(0);
142
121
  }
143
122
 
144
- const selected = projects.find((p) => p.id === choice);
145
- if (!selected) {
146
- error("No project selected");
123
+ const selected = result.project;
124
+
125
+ // Need project ID - fetch from control plane by slug
126
+ const project = await findProjectBySlug(selected.name);
127
+ if (!project) {
128
+ error(`Could not find project: ${selected.name}`);
147
129
  process.exit(1);
148
130
  }
149
131
 
150
132
  output.start("Linking project...");
151
- await linkProject(process.cwd(), selected.id, "managed");
152
- await registerPath(selected.id, process.cwd());
133
+ await linkProject(process.cwd(), project.id, "managed");
134
+ await registerPath(project.id, process.cwd());
153
135
  output.stop();
154
- success(`Linked to: ${selected.slug}`);
136
+ success(`Linked to: ${selected.name}`);
155
137
  }
@@ -1,8 +1,8 @@
1
1
  import { existsSync } from "node:fs";
2
- import { output } from "../lib/output.ts";
3
- import { getDeployMode, getProjectId } from "../lib/project-link.ts";
4
2
  import { authFetch } from "../lib/auth/index.ts";
5
3
  import { getControlApiUrl, startLogSession } from "../lib/control-plane.ts";
4
+ import { output } from "../lib/output.ts";
5
+ import { getDeployMode, getProjectId } from "../lib/project-link.ts";
6
6
 
7
7
  // Lines containing these strings will be filtered out
8
8
  const FILTERED_PATTERNS = ["⛅️ wrangler"];
@@ -1,9 +1,15 @@
1
1
  import { spawn } from "node:child_process";
2
- import { rm, mkdtemp } from "node:fs/promises";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
- import { error, info, success } from "../lib/output.ts";
6
+ import {
7
+ APP_MCP_CONFIGS,
8
+ getAppDisplayName,
9
+ installMcpConfigToApp,
10
+ isAppInstalled,
11
+ } from "../lib/mcp-config.ts";
12
+ import { error, info, item, success } from "../lib/output.ts";
7
13
  import { startMcpServer } from "../mcp/server.ts";
8
14
 
9
15
  const cliRoot = fileURLToPath(new URL("../..", import.meta.url));
@@ -27,13 +33,78 @@ export default async function mcp(subcommand?: string, options: McpOptions = {})
27
33
  return;
28
34
  }
29
35
 
30
- error("Unknown subcommand. Use: jack mcp serve or jack mcp test");
36
+ if (subcommand === "install") {
37
+ await installMcpConfig();
38
+ return;
39
+ }
40
+
41
+ error("Unknown subcommand. Use: jack mcp serve, jack mcp install, or jack mcp test");
31
42
  info("Usage:");
32
43
  info(" jack mcp serve [--project /path] [--debug] Start MCP server");
44
+ info(" jack mcp install Install/repair MCP config for AI agents");
33
45
  info(" jack mcp test Test MCP server connectivity");
34
46
  process.exit(1);
35
47
  }
36
48
 
49
+ /**
50
+ * Install or repair MCP configuration for all detected apps
51
+ */
52
+ async function installMcpConfig(): Promise<void> {
53
+ info("Installing jack MCP server configuration...\n");
54
+
55
+ const installed: string[] = [];
56
+ const skipped: string[] = [];
57
+ const failed: string[] = [];
58
+
59
+ for (const appId of Object.keys(APP_MCP_CONFIGS)) {
60
+ const displayName = getAppDisplayName(appId);
61
+
62
+ if (!isAppInstalled(appId)) {
63
+ skipped.push(displayName);
64
+ continue;
65
+ }
66
+
67
+ try {
68
+ const result = await installMcpConfigToApp(appId);
69
+ if (result) {
70
+ installed.push(displayName);
71
+ } else {
72
+ failed.push(displayName);
73
+ }
74
+ } catch {
75
+ failed.push(displayName);
76
+ }
77
+ }
78
+
79
+ // Report results
80
+ if (installed.length > 0) {
81
+ success(`Installed to ${installed.length} app(s):`);
82
+ for (const app of installed) {
83
+ item(` ${app}`);
84
+ }
85
+ }
86
+
87
+ if (skipped.length > 0) {
88
+ info(`\nSkipped (not installed):`);
89
+ for (const app of skipped) {
90
+ item(` ${app}`);
91
+ }
92
+ }
93
+
94
+ if (failed.length > 0) {
95
+ error(`\nFailed to install:`);
96
+ for (const app of failed) {
97
+ item(` ${app}`);
98
+ }
99
+ }
100
+
101
+ if (installed.length > 0) {
102
+ info("\nRestart your AI agent (Claude Code, Claude Desktop) to use jack MCP tools.");
103
+ } else if (failed.length === 0 && skipped.length > 0) {
104
+ info("\nNo supported AI agents detected. Install Claude Code or Claude Desktop first.");
105
+ }
106
+ }
107
+
37
108
  /**
38
109
  * Test MCP server by spawning it and sending test requests
39
110
  */