@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.
- package/package.json +1 -1
- package/src/commands/deploys.ts +95 -0
- package/src/commands/link.ts +8 -0
- package/src/commands/mcp.ts +179 -4
- package/src/commands/rollback.ts +53 -0
- package/src/commands/secrets.ts +3 -1
- package/src/commands/services.ts +11 -1
- package/src/commands/ship.ts +3 -1
- package/src/commands/tokens.ts +16 -1
- package/src/commands/whoami.ts +43 -8
- package/src/index.ts +16 -0
- package/src/lib/agent-files.ts +54 -4
- package/src/lib/agent-integration.ts +4 -166
- package/src/lib/claude-hooks-installer.ts +55 -0
- package/src/lib/control-plane.ts +78 -40
- package/src/lib/crypto.ts +84 -0
- package/src/lib/debug.ts +2 -1
- package/src/lib/deploy-upload.ts +13 -3
- package/src/lib/hooks.ts +4 -3
- package/src/lib/managed-deploy.ts +12 -9
- package/src/lib/project-link.ts +6 -0
- package/src/lib/project-operations.ts +92 -30
- package/src/lib/prompts.ts +2 -2
- package/src/lib/telemetry.ts +2 -0
- package/src/mcp/README.md +1 -1
- package/src/mcp/resources/index.ts +1 -16
- package/src/mcp/server.ts +23 -0
- package/src/mcp/tools/index.ts +133 -17
- package/src/mcp/types.ts +1 -0
- package/src/mcp/utils.ts +2 -1
- package/src/templates/index.ts +25 -73
- package/templates/CLAUDE.md +62 -0
- package/templates/ai-chat/.jack.json +10 -5
- package/templates/ai-chat/bun.lock +50 -1
- package/templates/ai-chat/package.json +5 -0
- package/templates/ai-chat/public/app.js +73 -0
- package/templates/ai-chat/public/index.html +14 -197
- package/templates/ai-chat/schema.sql +14 -0
- package/templates/ai-chat/src/index.ts +86 -102
- package/templates/ai-chat/wrangler.jsonc +8 -1
- package/templates/cron/.jack.json +66 -0
- package/templates/cron/bun.lock +23 -0
- package/templates/cron/package.json +16 -0
- package/templates/cron/schema.sql +24 -0
- package/templates/cron/src/index.ts +117 -0
- package/templates/cron/src/jobs.ts +139 -0
- package/templates/cron/src/webhooks.ts +95 -0
- package/templates/cron/tsconfig.json +17 -0
- package/templates/cron/wrangler.jsonc +11 -0
- package/templates/miniapp/.jack.json +1 -1
- package/templates/nextjs/.jack.json +1 -1
- package/templates/nextjs-auth/.jack.json +44 -0
- package/templates/nextjs-auth/app/api/auth/[...all]/route.ts +11 -0
- package/templates/nextjs-auth/app/dashboard/loading.tsx +53 -0
- package/templates/nextjs-auth/app/dashboard/page.tsx +73 -0
- package/templates/nextjs-auth/app/error.tsx +44 -0
- package/templates/nextjs-auth/app/globals.css +1 -0
- package/templates/nextjs-auth/app/health/route.ts +3 -0
- package/templates/nextjs-auth/app/layout.tsx +24 -0
- package/templates/nextjs-auth/app/login/page.tsx +10 -0
- package/templates/nextjs-auth/app/page.tsx +86 -0
- package/templates/nextjs-auth/app/signup/page.tsx +10 -0
- package/templates/nextjs-auth/bun.lock +1065 -0
- package/templates/nextjs-auth/cloudflare-env.d.ts +8 -0
- package/templates/nextjs-auth/components/auth-form.tsx +191 -0
- package/templates/nextjs-auth/components/header.tsx +50 -0
- package/templates/nextjs-auth/components/user-menu.tsx +23 -0
- package/templates/nextjs-auth/lib/auth-client.ts +3 -0
- package/templates/nextjs-auth/lib/auth.ts +43 -0
- package/templates/nextjs-auth/lib/utils.ts +6 -0
- package/templates/nextjs-auth/middleware.ts +33 -0
- package/templates/nextjs-auth/next.config.ts +8 -0
- package/templates/nextjs-auth/open-next.config.ts +6 -0
- package/templates/nextjs-auth/package.json +33 -0
- package/templates/nextjs-auth/postcss.config.mjs +8 -0
- package/templates/nextjs-auth/schema.sql +49 -0
- package/templates/nextjs-auth/tsconfig.json +28 -0
- package/templates/nextjs-auth/wrangler.jsonc +23 -0
- package/templates/nextjs-clerk/.jack.json +54 -0
- package/templates/nextjs-clerk/app/dashboard/page.tsx +69 -0
- package/templates/nextjs-clerk/app/globals.css +1 -0
- package/templates/nextjs-clerk/app/health/route.ts +3 -0
- package/templates/nextjs-clerk/app/layout.tsx +28 -0
- package/templates/nextjs-clerk/app/page.tsx +86 -0
- package/templates/nextjs-clerk/app/sign-in/[[...sign-in]]/page.tsx +9 -0
- package/templates/nextjs-clerk/app/sign-up/[[...sign-up]]/page.tsx +9 -0
- package/templates/nextjs-clerk/bun.lock +1055 -0
- package/templates/nextjs-clerk/cloudflare-env.d.ts +3 -0
- package/templates/nextjs-clerk/components/header.tsx +40 -0
- package/templates/nextjs-clerk/lib/utils.ts +6 -0
- package/templates/nextjs-clerk/middleware.ts +18 -0
- package/templates/nextjs-clerk/next.config.ts +8 -0
- package/templates/nextjs-clerk/open-next.config.ts +6 -0
- package/templates/nextjs-clerk/package.json +31 -0
- package/templates/nextjs-clerk/postcss.config.mjs +8 -0
- package/templates/nextjs-clerk/tsconfig.json +28 -0
- package/templates/nextjs-clerk/wrangler.jsonc +17 -0
- package/templates/nextjs-shadcn/.jack.json +34 -0
- package/templates/nextjs-shadcn/app/dashboard/data.json +614 -0
- package/templates/nextjs-shadcn/app/dashboard/page.tsx +55 -0
- package/templates/nextjs-shadcn/app/globals.css +126 -0
- package/templates/nextjs-shadcn/app/health/route.ts +3 -0
- package/templates/nextjs-shadcn/app/layout.tsx +24 -0
- package/templates/nextjs-shadcn/app/login/page.tsx +19 -0
- package/templates/nextjs-shadcn/app/page.tsx +180 -0
- package/templates/nextjs-shadcn/app/showcase.tsx +1262 -0
- package/templates/nextjs-shadcn/bun.lock +1789 -0
- package/templates/nextjs-shadcn/cloudflare-env.d.ts +4 -0
- package/templates/nextjs-shadcn/components/app-sidebar.tsx +175 -0
- package/templates/nextjs-shadcn/components/chart-area-interactive.tsx +291 -0
- package/templates/nextjs-shadcn/components/data-table.tsx +807 -0
- package/templates/nextjs-shadcn/components/login-form.tsx +95 -0
- package/templates/nextjs-shadcn/components/nav-documents.tsx +92 -0
- package/templates/nextjs-shadcn/components/nav-main.tsx +73 -0
- package/templates/nextjs-shadcn/components/nav-projects.tsx +89 -0
- package/templates/nextjs-shadcn/components/nav-secondary.tsx +42 -0
- package/templates/nextjs-shadcn/components/nav-user.tsx +114 -0
- package/templates/nextjs-shadcn/components/section-cards.tsx +102 -0
- package/templates/nextjs-shadcn/components/site-header.tsx +30 -0
- package/templates/nextjs-shadcn/components/team-switcher.tsx +91 -0
- package/templates/nextjs-shadcn/components/ui/accordion.tsx +66 -0
- package/templates/nextjs-shadcn/components/ui/alert-dialog.tsx +196 -0
- package/templates/nextjs-shadcn/components/ui/alert.tsx +66 -0
- package/templates/nextjs-shadcn/components/ui/aspect-ratio.tsx +11 -0
- package/templates/nextjs-shadcn/components/ui/avatar.tsx +109 -0
- package/templates/nextjs-shadcn/components/ui/badge.tsx +48 -0
- package/templates/nextjs-shadcn/components/ui/breadcrumb.tsx +109 -0
- package/templates/nextjs-shadcn/components/ui/button-group.tsx +83 -0
- package/templates/nextjs-shadcn/components/ui/button.tsx +64 -0
- package/templates/nextjs-shadcn/components/ui/calendar.tsx +220 -0
- package/templates/nextjs-shadcn/components/ui/card.tsx +92 -0
- package/templates/nextjs-shadcn/components/ui/carousel.tsx +241 -0
- package/templates/nextjs-shadcn/components/ui/chart.tsx +357 -0
- package/templates/nextjs-shadcn/components/ui/checkbox.tsx +32 -0
- package/templates/nextjs-shadcn/components/ui/collapsible.tsx +33 -0
- package/templates/nextjs-shadcn/components/ui/combobox.tsx +310 -0
- package/templates/nextjs-shadcn/components/ui/command.tsx +184 -0
- package/templates/nextjs-shadcn/components/ui/context-menu.tsx +252 -0
- package/templates/nextjs-shadcn/components/ui/dialog.tsx +158 -0
- package/templates/nextjs-shadcn/components/ui/direction.tsx +22 -0
- package/templates/nextjs-shadcn/components/ui/drawer.tsx +135 -0
- package/templates/nextjs-shadcn/components/ui/dropdown-menu.tsx +257 -0
- package/templates/nextjs-shadcn/components/ui/empty.tsx +104 -0
- package/templates/nextjs-shadcn/components/ui/field.tsx +248 -0
- package/templates/nextjs-shadcn/components/ui/form.tsx +167 -0
- package/templates/nextjs-shadcn/components/ui/hover-card.tsx +44 -0
- package/templates/nextjs-shadcn/components/ui/input-group.tsx +170 -0
- package/templates/nextjs-shadcn/components/ui/input-otp.tsx +77 -0
- package/templates/nextjs-shadcn/components/ui/input.tsx +21 -0
- package/templates/nextjs-shadcn/components/ui/item.tsx +193 -0
- package/templates/nextjs-shadcn/components/ui/kbd.tsx +28 -0
- package/templates/nextjs-shadcn/components/ui/label.tsx +24 -0
- package/templates/nextjs-shadcn/components/ui/menubar.tsx +276 -0
- package/templates/nextjs-shadcn/components/ui/native-select.tsx +53 -0
- package/templates/nextjs-shadcn/components/ui/navigation-menu.tsx +168 -0
- package/templates/nextjs-shadcn/components/ui/pagination.tsx +127 -0
- package/templates/nextjs-shadcn/components/ui/popover.tsx +89 -0
- package/templates/nextjs-shadcn/components/ui/progress.tsx +31 -0
- package/templates/nextjs-shadcn/components/ui/radio-group.tsx +45 -0
- package/templates/nextjs-shadcn/components/ui/resizable.tsx +53 -0
- package/templates/nextjs-shadcn/components/ui/scroll-area.tsx +58 -0
- package/templates/nextjs-shadcn/components/ui/select.tsx +190 -0
- package/templates/nextjs-shadcn/components/ui/separator.tsx +28 -0
- package/templates/nextjs-shadcn/components/ui/sheet.tsx +143 -0
- package/templates/nextjs-shadcn/components/ui/sidebar.tsx +726 -0
- package/templates/nextjs-shadcn/components/ui/skeleton.tsx +13 -0
- package/templates/nextjs-shadcn/components/ui/slider.tsx +63 -0
- package/templates/nextjs-shadcn/components/ui/sonner.tsx +40 -0
- package/templates/nextjs-shadcn/components/ui/spinner.tsx +16 -0
- package/templates/nextjs-shadcn/components/ui/switch.tsx +35 -0
- package/templates/nextjs-shadcn/components/ui/table.tsx +116 -0
- package/templates/nextjs-shadcn/components/ui/tabs.tsx +91 -0
- package/templates/nextjs-shadcn/components/ui/textarea.tsx +18 -0
- package/templates/nextjs-shadcn/components/ui/toggle-group.tsx +83 -0
- package/templates/nextjs-shadcn/components/ui/toggle.tsx +47 -0
- package/templates/nextjs-shadcn/components/ui/tooltip.tsx +57 -0
- package/templates/nextjs-shadcn/components.json +23 -0
- package/templates/nextjs-shadcn/hooks/use-mobile.ts +19 -0
- package/templates/nextjs-shadcn/lib/utils.ts +6 -0
- package/templates/nextjs-shadcn/next-env.d.ts +6 -0
- package/templates/nextjs-shadcn/next.config.ts +8 -0
- package/templates/nextjs-shadcn/open-next.config.ts +6 -0
- package/templates/nextjs-shadcn/package.json +55 -0
- package/templates/nextjs-shadcn/postcss.config.mjs +8 -0
- package/templates/nextjs-shadcn/tsconfig.json +28 -0
- package/templates/nextjs-shadcn/wrangler.jsonc +23 -0
- package/templates/resend/.jack.json +64 -0
- package/templates/resend/bun.lock +23 -0
- package/templates/resend/package.json +16 -0
- package/templates/resend/schema.sql +13 -0
- package/templates/resend/src/email.ts +165 -0
- package/templates/resend/src/index.ts +108 -0
- package/templates/resend/tsconfig.json +17 -0
- package/templates/resend/wrangler.jsonc +11 -0
- package/templates/saas/.jack.json +1 -1
- package/templates/ai-chat/public/chat.js +0 -149
package/src/lib/agent-files.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { AgentConfig, AgentDefinition } from "./agents.ts";
|
|
|
7
7
|
* Template for AGENTS.md
|
|
8
8
|
*/
|
|
9
9
|
function generateAgentsMd(projectName: string, template: Template): string {
|
|
10
|
-
const summary = template.agentContext?.summary || "A
|
|
10
|
+
const summary = template.agentContext?.summary || "A jack project";
|
|
11
11
|
const fullText = template.agentContext?.full_text || "";
|
|
12
12
|
|
|
13
13
|
return `# ${projectName}
|
|
@@ -16,16 +16,66 @@ function generateAgentsMd(projectName: string, template: Template): string {
|
|
|
16
16
|
|
|
17
17
|
## Deployment
|
|
18
18
|
|
|
19
|
-
This project is deployed
|
|
19
|
+
This project is deployed and managed via jack:
|
|
20
20
|
|
|
21
21
|
\`\`\`bash
|
|
22
|
-
jack ship # Deploy to
|
|
22
|
+
jack ship # Deploy to production
|
|
23
23
|
jack logs # Stream production logs
|
|
24
24
|
\`\`\`
|
|
25
25
|
|
|
26
26
|
All deployment is handled by jack. Never run \`wrangler\` commands directly.
|
|
27
27
|
|
|
28
|
+
## Quick Commands
|
|
29
|
+
|
|
30
|
+
| Command | What it does |
|
|
31
|
+
|---------|--------------|
|
|
32
|
+
| \`jack ship\` | Deploy to production |
|
|
33
|
+
| \`jack logs\` | Stream live logs |
|
|
34
|
+
| \`jack services\` | Manage databases, KV, and other bindings |
|
|
35
|
+
| \`jack secrets\` | Manage environment secrets |
|
|
36
|
+
|
|
37
|
+
## Services & Bindings
|
|
38
|
+
|
|
39
|
+
Jack manages your project's services. To add a database:
|
|
40
|
+
|
|
41
|
+
\`\`\`bash
|
|
42
|
+
jack services db create
|
|
43
|
+
\`\`\`
|
|
44
|
+
|
|
45
|
+
To query it:
|
|
46
|
+
|
|
47
|
+
\`\`\`bash
|
|
48
|
+
jack services db query "SELECT * FROM users"
|
|
49
|
+
\`\`\`
|
|
50
|
+
|
|
28
51
|
${fullText}
|
|
52
|
+
|
|
53
|
+
## For AI Agents
|
|
54
|
+
|
|
55
|
+
### MCP Tools
|
|
56
|
+
|
|
57
|
+
If jack MCP is connected, **always prefer these tools over CLI commands or wrangler**:
|
|
58
|
+
|
|
59
|
+
| Tool | Use for |
|
|
60
|
+
|------|---------|
|
|
61
|
+
| \`mcp__jack__create_project\` | Create a new project (supports forking via \`template: "username/slug"\`) |
|
|
62
|
+
| \`mcp__jack__deploy_project\` | Deploy changes |
|
|
63
|
+
| \`mcp__jack__get_project_status\` | Check deployment status and URL |
|
|
64
|
+
| \`mcp__jack__list_projects\` | List all projects |
|
|
65
|
+
| \`mcp__jack__create_database\` | Create a database |
|
|
66
|
+
| \`mcp__jack__execute_sql\` | Query the database (read-only by default) |
|
|
67
|
+
| \`mcp__jack__list_databases\` | List project databases |
|
|
68
|
+
| \`mcp__jack__create_storage_bucket\` | Create object storage |
|
|
69
|
+
| \`mcp__jack__create_vectorize_index\` | Create vector search index |
|
|
70
|
+
| \`mcp__jack__tail_logs\` | Debug with live log samples |
|
|
71
|
+
| \`mcp__jack__start_log_session\` | Start real-time log stream |
|
|
72
|
+
| \`mcp__jack__create_cron\` | Create scheduled tasks |
|
|
73
|
+
| \`mcp__jack__list_domains\` | List custom domains |
|
|
74
|
+
| \`mcp__jack__connect_domain\` | Add a custom domain |
|
|
75
|
+
|
|
76
|
+
### Documentation
|
|
77
|
+
|
|
78
|
+
Full jack documentation: https://docs.getjack.org/llms-full.txt
|
|
29
79
|
`;
|
|
30
80
|
}
|
|
31
81
|
|
|
@@ -39,7 +89,7 @@ See [AGENTS.md](./AGENTS.md) for complete project context and deployment instruc
|
|
|
39
89
|
|
|
40
90
|
## Quick Commands
|
|
41
91
|
|
|
42
|
-
- **Deploy**: \`jack ship\` - Deploy to
|
|
92
|
+
- **Deploy**: \`jack ship\` - Deploy to production
|
|
43
93
|
- **Logs**: \`jack logs\` - Stream production logs
|
|
44
94
|
|
|
45
95
|
## Important
|
|
@@ -1,165 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Agent integration module
|
|
3
3
|
*
|
|
4
|
-
* Ensures AI agents have
|
|
4
|
+
* Ensures AI agents have MCP configured for jack projects.
|
|
5
5
|
* Called during both project creation and first BYO deploy.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { existsSync } from "node:fs";
|
|
9
|
-
import { join } from "node:path";
|
|
10
|
-
import type { Template } from "../templates/types.ts";
|
|
11
8
|
import { installMcpConfigsToAllApps, isAppInstalled } from "./mcp-config.ts";
|
|
12
9
|
|
|
13
10
|
export interface EnsureAgentResult {
|
|
14
11
|
mcpInstalled: string[];
|
|
15
|
-
jackMdCreated: boolean;
|
|
16
|
-
referencesAdded: string[];
|
|
17
12
|
}
|
|
18
13
|
|
|
19
14
|
export interface EnsureAgentOptions {
|
|
20
|
-
template?: Template;
|
|
21
15
|
silent?: boolean;
|
|
22
|
-
projectName?: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Generate JACK.md content
|
|
27
|
-
* Uses template agentContext if available, otherwise generic jack instructions
|
|
28
|
-
*/
|
|
29
|
-
export function generateJackMd(projectName?: string, template?: Template): string {
|
|
30
|
-
const header = projectName ? `# ${projectName}\n\n` : "# Jack\n\n";
|
|
31
|
-
|
|
32
|
-
const templateSummary = template?.agentContext?.summary;
|
|
33
|
-
const templateFullText = template?.agentContext?.full_text;
|
|
34
|
-
|
|
35
|
-
const summarySection = templateSummary ? `> ${templateSummary}\n\n` : "";
|
|
36
|
-
|
|
37
|
-
const templateSection = templateFullText ? `${templateFullText}\n\n` : "";
|
|
38
|
-
|
|
39
|
-
return `${header}${summarySection}This project is deployed and managed via jack.
|
|
40
|
-
|
|
41
|
-
## Quick Commands
|
|
42
|
-
|
|
43
|
-
| Command | What it does |
|
|
44
|
-
|---------|--------------|
|
|
45
|
-
| \`jack ship\` | Deploy to production |
|
|
46
|
-
| \`jack logs\` | Stream live logs |
|
|
47
|
-
| \`jack services\` | Manage databases, KV, and other bindings |
|
|
48
|
-
| \`jack secrets\` | Manage environment secrets |
|
|
49
|
-
|
|
50
|
-
## Important
|
|
51
|
-
|
|
52
|
-
- **Never run \`wrangler\` commands directly** - jack handles all infrastructure
|
|
53
|
-
- Use \`jack services db\` to create and query databases
|
|
54
|
-
- Secrets sync automatically across deploys
|
|
55
|
-
|
|
56
|
-
## Services & Bindings
|
|
57
|
-
|
|
58
|
-
Jack manages your project's services. To add a database:
|
|
59
|
-
|
|
60
|
-
\`\`\`bash
|
|
61
|
-
jack services db create
|
|
62
|
-
\`\`\`
|
|
63
|
-
|
|
64
|
-
To query it:
|
|
65
|
-
|
|
66
|
-
\`\`\`bash
|
|
67
|
-
jack services db query "SELECT * FROM users"
|
|
68
|
-
\`\`\`
|
|
69
|
-
|
|
70
|
-
More bindings (KV, R2, queues) coming soon.
|
|
71
|
-
|
|
72
|
-
${templateSection}## For AI Agents
|
|
73
|
-
|
|
74
|
-
### MCP Tools
|
|
75
|
-
|
|
76
|
-
If jack MCP is connected, prefer these tools over CLI commands:
|
|
77
|
-
|
|
78
|
-
| Tool | Use for |
|
|
79
|
-
|------|---------|
|
|
80
|
-
| \`mcp__jack__deploy_project\` | Deploy changes |
|
|
81
|
-
| \`mcp__jack__create_database\` | Create a new database |
|
|
82
|
-
| \`mcp__jack__execute_sql\` | Query the database |
|
|
83
|
-
| \`mcp__jack__list_projects\` | List all projects |
|
|
84
|
-
| \`mcp__jack__get_project_status\` | Check deployment status |
|
|
85
|
-
|
|
86
|
-
### Documentation
|
|
87
|
-
|
|
88
|
-
Full jack documentation: https://docs.getjack.org/llms-full.txt
|
|
89
|
-
`;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Create JACK.md if it doesn't exist
|
|
94
|
-
*/
|
|
95
|
-
async function ensureJackMd(
|
|
96
|
-
projectPath: string,
|
|
97
|
-
projectName?: string,
|
|
98
|
-
template?: Template,
|
|
99
|
-
): Promise<boolean> {
|
|
100
|
-
const jackMdPath = join(projectPath, "JACK.md");
|
|
101
|
-
|
|
102
|
-
if (existsSync(jackMdPath)) {
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const content = generateJackMd(projectName, template);
|
|
107
|
-
await Bun.write(jackMdPath, content);
|
|
108
|
-
return true;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Append JACK.md reference to existing agent files (CLAUDE.md, AGENTS.md)
|
|
113
|
-
*/
|
|
114
|
-
async function appendJackMdReferences(projectPath: string): Promise<string[]> {
|
|
115
|
-
const filesToCheck = ["CLAUDE.md", "AGENTS.md"];
|
|
116
|
-
const referencesAdded: string[] = [];
|
|
117
|
-
const jackMdPath = join(projectPath, "JACK.md");
|
|
118
|
-
|
|
119
|
-
// Only add references if JACK.md exists
|
|
120
|
-
if (!existsSync(jackMdPath)) {
|
|
121
|
-
return referencesAdded;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const referenceBlock = `<!-- Added by jack -->
|
|
125
|
-
> **Jack project** - See [JACK.md](./JACK.md) for deployment, services, and bindings.
|
|
126
|
-
|
|
127
|
-
`;
|
|
128
|
-
|
|
129
|
-
for (const filename of filesToCheck) {
|
|
130
|
-
const filePath = join(projectPath, filename);
|
|
131
|
-
|
|
132
|
-
if (!existsSync(filePath)) {
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
const content = await Bun.file(filePath).text();
|
|
138
|
-
|
|
139
|
-
// Skip if reference already exists
|
|
140
|
-
if (content.includes("JACK.md") || content.includes("<!-- Added by jack -->")) {
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Find position after first heading, or prepend if no heading
|
|
145
|
-
const headingMatch = content.match(/^#[^\n]*\n/m);
|
|
146
|
-
let newContent: string;
|
|
147
|
-
|
|
148
|
-
if (headingMatch && headingMatch.index !== undefined) {
|
|
149
|
-
const insertPos = headingMatch.index + headingMatch[0].length;
|
|
150
|
-
newContent = content.slice(0, insertPos) + "\n" + referenceBlock + content.slice(insertPos);
|
|
151
|
-
} else {
|
|
152
|
-
newContent = referenceBlock + content;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
await Bun.write(filePath, newContent);
|
|
156
|
-
referencesAdded.push(filename);
|
|
157
|
-
} catch {
|
|
158
|
-
// Ignore errors reading/writing individual files
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return referencesAdded;
|
|
163
16
|
}
|
|
164
17
|
|
|
165
18
|
/**
|
|
@@ -186,31 +39,16 @@ async function ensureMcpConfigured(): Promise<string[]> {
|
|
|
186
39
|
/**
|
|
187
40
|
* Ensure agent integration is set up for a project
|
|
188
41
|
*
|
|
189
|
-
*
|
|
190
|
-
* 1. Creates JACK.md if not exists (with template context if available)
|
|
191
|
-
* 2. Appends JACK.md reference to existing CLAUDE.md/AGENTS.md
|
|
192
|
-
* 3. Installs MCP config to detected AI apps
|
|
193
|
-
*
|
|
42
|
+
* Installs MCP config to detected AI apps.
|
|
194
43
|
* Safe to call multiple times - all operations are idempotent.
|
|
195
44
|
*/
|
|
196
45
|
export async function ensureAgentIntegration(
|
|
197
|
-
|
|
198
|
-
|
|
46
|
+
_projectPath: string,
|
|
47
|
+
_options: EnsureAgentOptions = {},
|
|
199
48
|
): Promise<EnsureAgentResult> {
|
|
200
|
-
const { template, projectName } = options;
|
|
201
|
-
|
|
202
|
-
// 1. Create JACK.md if not exists
|
|
203
|
-
const jackMdCreated = await ensureJackMd(projectPath, projectName, template);
|
|
204
|
-
|
|
205
|
-
// 2. Append references to existing agent files
|
|
206
|
-
const referencesAdded = await appendJackMdReferences(projectPath);
|
|
207
|
-
|
|
208
|
-
// 3. Ensure MCP is configured
|
|
209
49
|
const mcpInstalled = await ensureMcpConfigured();
|
|
210
50
|
|
|
211
51
|
return {
|
|
212
52
|
mcpInstalled,
|
|
213
|
-
jackMdCreated,
|
|
214
|
-
referencesAdded,
|
|
215
53
|
};
|
|
216
54
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
const HOOK_COMMAND = "jack mcp context 2>/dev/null || true";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Install a Claude Code SessionStart hook to the project's .claude/settings.json.
|
|
9
|
+
* Only fires when Claude Code is opened in this project directory.
|
|
10
|
+
* Non-destructive: preserves existing hooks and deduplicates.
|
|
11
|
+
*/
|
|
12
|
+
export async function installClaudeCodeHooks(projectPath: string): Promise<boolean> {
|
|
13
|
+
try {
|
|
14
|
+
const claudeDir = join(projectPath, ".claude");
|
|
15
|
+
const settingsPath = join(claudeDir, "settings.json");
|
|
16
|
+
|
|
17
|
+
// Ensure .claude directory exists
|
|
18
|
+
if (!existsSync(claudeDir)) {
|
|
19
|
+
await mkdir(claudeDir, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let settings: Record<string, unknown> = {};
|
|
23
|
+
if (existsSync(settingsPath)) {
|
|
24
|
+
try {
|
|
25
|
+
settings = await Bun.file(settingsPath).json();
|
|
26
|
+
} catch {
|
|
27
|
+
settings = {};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const hooks = (settings.hooks as Record<string, unknown[]>) ?? {};
|
|
32
|
+
const sessionStart = (hooks.SessionStart as Array<Record<string, unknown>>) ?? [];
|
|
33
|
+
|
|
34
|
+
// Check if jack hook is already installed
|
|
35
|
+
for (const entry of sessionStart) {
|
|
36
|
+
const entryHooks = entry.hooks as Array<Record<string, string>> | undefined;
|
|
37
|
+
if (entryHooks?.some((h) => h.command?.includes("jack mcp context"))) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
sessionStart.push({
|
|
43
|
+
matcher: "",
|
|
44
|
+
hooks: [{ type: "command", command: HOOK_COMMAND }],
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
hooks.SessionStart = sessionStart;
|
|
48
|
+
settings.hooks = hooks;
|
|
49
|
+
|
|
50
|
+
await Bun.write(settingsPath, JSON.stringify(settings, null, 2));
|
|
51
|
+
return true;
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/lib/control-plane.ts
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { debug } from "./debug.ts";
|
|
6
|
-
import { formatSize } from "./format.ts";
|
|
7
6
|
|
|
8
7
|
const DEFAULT_CONTROL_API_URL = "https://control.getjack.org";
|
|
9
8
|
|
|
@@ -16,12 +15,14 @@ export interface CreateProjectRequest {
|
|
|
16
15
|
slug?: string;
|
|
17
16
|
template?: string;
|
|
18
17
|
use_prebuilt?: boolean;
|
|
18
|
+
forked_from?: string;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export interface CreateManagedProjectOptions {
|
|
22
22
|
slug?: string;
|
|
23
23
|
template?: string;
|
|
24
24
|
usePrebuilt?: boolean;
|
|
25
|
+
forkedFrom?: string;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
export interface CreateProjectResponse {
|
|
@@ -111,6 +112,9 @@ export async function createManagedProject(
|
|
|
111
112
|
if (options?.usePrebuilt !== undefined) {
|
|
112
113
|
requestBody.use_prebuilt = options.usePrebuilt;
|
|
113
114
|
}
|
|
115
|
+
if (options?.forkedFrom) {
|
|
116
|
+
requestBody.forked_from = options.forkedFrom;
|
|
117
|
+
}
|
|
114
118
|
|
|
115
119
|
debug("Creating managed project", {
|
|
116
120
|
name,
|
|
@@ -451,6 +455,79 @@ export async function fetchProjectResources(projectId: string): Promise<ProjectR
|
|
|
451
455
|
return data.resources;
|
|
452
456
|
}
|
|
453
457
|
|
|
458
|
+
export interface DeploymentInfo {
|
|
459
|
+
id: string;
|
|
460
|
+
status: "queued" | "building" | "live" | "failed";
|
|
461
|
+
source: string;
|
|
462
|
+
error_message: string | null;
|
|
463
|
+
message: string | null;
|
|
464
|
+
created_at: string;
|
|
465
|
+
updated_at: string;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export interface DeploymentListResult {
|
|
469
|
+
deployments: DeploymentInfo[];
|
|
470
|
+
total: number;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Fetch recent deployments for a managed project.
|
|
475
|
+
* Returns deployments (up to 10) and total count.
|
|
476
|
+
*/
|
|
477
|
+
export async function fetchDeployments(projectId: string): Promise<DeploymentListResult> {
|
|
478
|
+
const { authFetch } = await import("./auth/index.ts");
|
|
479
|
+
|
|
480
|
+
const response = await authFetch(`${getControlApiUrl()}/v1/projects/${projectId}/deployments`);
|
|
481
|
+
|
|
482
|
+
if (!response.ok) {
|
|
483
|
+
return { deployments: [], total: 0 }; // Silent fail — deployment history is supplementary
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const data = (await response.json()) as { deployments: DeploymentInfo[]; total: number };
|
|
487
|
+
return { deployments: data.deployments, total: data.total };
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
export interface RollbackResponse {
|
|
491
|
+
deployment: {
|
|
492
|
+
id: string;
|
|
493
|
+
status: string;
|
|
494
|
+
source: string;
|
|
495
|
+
created_at: string;
|
|
496
|
+
updated_at: string;
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Rollback a managed project to a previous deployment.
|
|
502
|
+
* If no deploymentId given, rolls back to the previous successful deployment.
|
|
503
|
+
*/
|
|
504
|
+
export async function rollbackDeployment(
|
|
505
|
+
projectId: string,
|
|
506
|
+
deploymentId?: string,
|
|
507
|
+
): Promise<RollbackResponse> {
|
|
508
|
+
const { authFetch } = await import("./auth/index.ts");
|
|
509
|
+
|
|
510
|
+
const body: Record<string, string> = {};
|
|
511
|
+
if (deploymentId) {
|
|
512
|
+
body.deployment_id = deploymentId;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const response = await authFetch(`${getControlApiUrl()}/v1/projects/${projectId}/rollback`, {
|
|
516
|
+
method: "POST",
|
|
517
|
+
headers: { "Content-Type": "application/json" },
|
|
518
|
+
body: JSON.stringify(body),
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
if (!response.ok) {
|
|
522
|
+
const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
|
|
523
|
+
message?: string;
|
|
524
|
+
};
|
|
525
|
+
throw new Error(err.message || `Rollback failed: ${response.status}`);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return response.json() as Promise<RollbackResponse>;
|
|
529
|
+
}
|
|
530
|
+
|
|
454
531
|
/**
|
|
455
532
|
* Create a resource for a managed project.
|
|
456
533
|
* Uses POST /v1/projects/:id/resources/:type endpoint.
|
|
@@ -715,45 +792,6 @@ export async function getCurrentUserProfile(): Promise<UserProfile | null> {
|
|
|
715
792
|
}
|
|
716
793
|
}
|
|
717
794
|
|
|
718
|
-
export interface SourceSnapshotResponse {
|
|
719
|
-
success: boolean;
|
|
720
|
-
source_key: string;
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
/**
|
|
724
|
-
* Upload a source snapshot for a project.
|
|
725
|
-
* Used to enable project forking.
|
|
726
|
-
*/
|
|
727
|
-
export async function uploadSourceSnapshot(
|
|
728
|
-
projectId: string,
|
|
729
|
-
sourceZipPath: string,
|
|
730
|
-
): Promise<SourceSnapshotResponse> {
|
|
731
|
-
const { authFetch } = await import("./auth/index.ts");
|
|
732
|
-
|
|
733
|
-
const formData = new FormData();
|
|
734
|
-
const sourceFile = Bun.file(sourceZipPath);
|
|
735
|
-
formData.append("source", sourceFile);
|
|
736
|
-
|
|
737
|
-
const url = `${getControlApiUrl()}/v1/projects/${projectId}/source`;
|
|
738
|
-
debug(`Source snapshot: ${formatSize(sourceFile.size)}`);
|
|
739
|
-
|
|
740
|
-
const start = Date.now();
|
|
741
|
-
const response = await authFetch(url, {
|
|
742
|
-
method: "POST",
|
|
743
|
-
body: formData,
|
|
744
|
-
});
|
|
745
|
-
debug(`Source snapshot: ${response.status} (${((Date.now() - start) / 1000).toFixed(1)}s)`);
|
|
746
|
-
|
|
747
|
-
if (!response.ok) {
|
|
748
|
-
const error = (await response.json().catch(() => ({ message: "Upload failed" }))) as {
|
|
749
|
-
message?: string;
|
|
750
|
-
};
|
|
751
|
-
throw new Error(error.message || `Source upload failed: ${response.status}`);
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
return response.json() as Promise<SourceSnapshotResponse>;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
795
|
/**
|
|
758
796
|
* Publish a project to make it forkable by others.
|
|
759
797
|
*/
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side encryption for secrets sent to the control plane.
|
|
3
|
+
*
|
|
4
|
+
* Uses hybrid RSA-OAEP + AES-GCM so secrets of any size can be encrypted.
|
|
5
|
+
* The control plane holds the matching RSA private key as a worker secret.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface EncryptedEnvelope {
|
|
9
|
+
__encrypted: true;
|
|
10
|
+
kid: string;
|
|
11
|
+
wrappedKey: string; // base64url
|
|
12
|
+
iv: string; // base64url
|
|
13
|
+
ciphertext: string; // base64url
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const KEY_ID = "v1";
|
|
17
|
+
|
|
18
|
+
/** RSA-OAEP-256 public key for secrets encryption (generated by scripts/generate-encryption-keypair.ts) */
|
|
19
|
+
const PUBLIC_KEY_JWK: JsonWebKey = {
|
|
20
|
+
alg: "RSA-OAEP-256",
|
|
21
|
+
e: "AQAB",
|
|
22
|
+
ext: true,
|
|
23
|
+
key_ops: ["encrypt"],
|
|
24
|
+
kty: "RSA",
|
|
25
|
+
n: "q2Y4K6heGkv_ABFOYokNXcwHFLAG3JScxEhjnZQTi7K8JEdCM9inqcy3gGhtT4lP6YWqhF4IHRMFU4qhPuByLASNp3bMWzDDKlckyDeWyPRnJqjb6IvwPYLw0ky1WumjjypAX_OSpNKhuYHx1X1hu7KQq9oa3f6sHFM5XbofMM2f__HvcEHnBVgkJvjTL2dn94DPgnsmtTLSRUAde34DQnXAKjVJ2jDuoC_sDAUmcmsEZKt3AUaCTkLBtbfW-ZI6_4VD2yNw-ySuOEprhhsNi6UpbjPY1ncduB5nkNhb276kVsjWo8w89KvDlhNCRyyZ_c0QRYSxn-nYEIE3vtS_h9FC9keMcDnH_fE4VPn14cjPV_G-eiUAoow8q5qBnFEp9DaaOswZ8IwEhpaxN6jvgk1WikZIBd58WB4HHSFWQ-W-096_5FA4cltQE7Qgwy86AgPnhpuCLLTqwpx8XF3GLbWtt9h4QYpfjrLyGuj4gJWCI4AJSDY1bvqiZtTfO1LdhyiZteEH0XhSBvXjXb1dJHbNXIcrIa_owtfEKqb53AxxwTvPaZazkigT0MqZ-141e7x6kuDkG_gSSFyCGrESaAyGYRh2K4wcGuV4jyZlQ6dzbQd0DPn8uRW3kC_vpToyZxZVqWGXFD6TtMYQwo_zWK3IaYCYMB-TYBFJj8a41z8",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function base64urlEncode(bytes: ArrayBuffer | Uint8Array): string {
|
|
29
|
+
const u8 = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
30
|
+
let bin = "";
|
|
31
|
+
for (let i = 0; i < u8.length; i++) {
|
|
32
|
+
bin += String.fromCharCode(u8[i]);
|
|
33
|
+
}
|
|
34
|
+
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let _cachedPublicKey: CryptoKey | null = null;
|
|
38
|
+
|
|
39
|
+
async function getPublicKey(): Promise<CryptoKey> {
|
|
40
|
+
if (_cachedPublicKey) return _cachedPublicKey;
|
|
41
|
+
_cachedPublicKey = await crypto.subtle.importKey(
|
|
42
|
+
"jwk",
|
|
43
|
+
PUBLIC_KEY_JWK,
|
|
44
|
+
{ name: "RSA-OAEP", hash: "SHA-256" },
|
|
45
|
+
false,
|
|
46
|
+
["encrypt"],
|
|
47
|
+
);
|
|
48
|
+
return _cachedPublicKey;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Encrypt a single plaintext string into an encrypted envelope. */
|
|
52
|
+
export async function encryptSecretValue(plaintext: string): Promise<EncryptedEnvelope> {
|
|
53
|
+
const rsaKey = await getPublicKey();
|
|
54
|
+
|
|
55
|
+
// Generate ephemeral AES-256-GCM key
|
|
56
|
+
const aesKey = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, [
|
|
57
|
+
"encrypt",
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
// Encrypt plaintext with AES-GCM
|
|
61
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
62
|
+
const plainBytes = new TextEncoder().encode(plaintext);
|
|
63
|
+
const ciphertext = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, aesKey, plainBytes);
|
|
64
|
+
|
|
65
|
+
// Wrap the AES key with RSA-OAEP
|
|
66
|
+
const rawAesKey = await crypto.subtle.exportKey("raw", aesKey);
|
|
67
|
+
const wrappedKey = await crypto.subtle.encrypt({ name: "RSA-OAEP" }, rsaKey, rawAesKey);
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
__encrypted: true,
|
|
71
|
+
kid: KEY_ID,
|
|
72
|
+
wrappedKey: base64urlEncode(wrappedKey),
|
|
73
|
+
iv: base64urlEncode(iv),
|
|
74
|
+
ciphertext: base64urlEncode(ciphertext),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Encrypt a full secrets map (Record<string, string>) as a single envelope.
|
|
80
|
+
* The entire JSON object is serialized then encrypted as one blob.
|
|
81
|
+
*/
|
|
82
|
+
export async function encryptSecrets(secrets: Record<string, string>): Promise<EncryptedEnvelope> {
|
|
83
|
+
return encryptSecretValue(JSON.stringify(secrets));
|
|
84
|
+
}
|
package/src/lib/debug.ts
CHANGED
package/src/lib/deploy-upload.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { readFile } from "node:fs/promises";
|
|
|
6
6
|
import type { AssetManifest } from "./asset-hash.ts";
|
|
7
7
|
import { authFetch } from "./auth/index.ts";
|
|
8
8
|
import { getControlApiUrl } from "./control-plane.ts";
|
|
9
|
+
import { encryptSecrets } from "./crypto.ts";
|
|
9
10
|
import { debug } from "./debug.ts";
|
|
10
11
|
import { formatSize } from "./format.ts";
|
|
11
12
|
|
|
@@ -18,6 +19,7 @@ export interface DeployUploadOptions {
|
|
|
18
19
|
secretsPath?: string;
|
|
19
20
|
assetsZipPath?: string;
|
|
20
21
|
assetManifest?: AssetManifest;
|
|
22
|
+
message?: string;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export interface DeployUploadResult {
|
|
@@ -25,6 +27,7 @@ export interface DeployUploadResult {
|
|
|
25
27
|
project_id: string;
|
|
26
28
|
status: "queued" | "building" | "live" | "failed";
|
|
27
29
|
source: string;
|
|
30
|
+
error_message: string | null;
|
|
28
31
|
created_at: string;
|
|
29
32
|
}
|
|
30
33
|
|
|
@@ -65,13 +68,16 @@ export async function uploadDeployment(options: DeployUploadOptions): Promise<De
|
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
if (options.secretsPath) {
|
|
68
|
-
const secretsContent = await readFile(options.secretsPath);
|
|
71
|
+
const secretsContent = await readFile(options.secretsPath, "utf-8");
|
|
72
|
+
const secretsJson = JSON.parse(secretsContent) as Record<string, string>;
|
|
73
|
+
const encryptedEnvelope = await encryptSecrets(secretsJson);
|
|
74
|
+
const encryptedPayload = JSON.stringify(encryptedEnvelope);
|
|
69
75
|
formData.append(
|
|
70
76
|
"secrets",
|
|
71
|
-
new Blob([
|
|
77
|
+
new Blob([encryptedPayload], { type: "application/json" }),
|
|
72
78
|
"secrets.json",
|
|
73
79
|
);
|
|
74
|
-
totalSize +=
|
|
80
|
+
totalSize += encryptedPayload.length;
|
|
75
81
|
}
|
|
76
82
|
|
|
77
83
|
if (options.assetsZipPath) {
|
|
@@ -91,6 +97,10 @@ export async function uploadDeployment(options: DeployUploadOptions): Promise<De
|
|
|
91
97
|
totalSize += manifestJson.length;
|
|
92
98
|
}
|
|
93
99
|
|
|
100
|
+
if (options.message) {
|
|
101
|
+
formData.append("message", options.message);
|
|
102
|
+
}
|
|
103
|
+
|
|
94
104
|
const prepareMs = Date.now() - prepareStart;
|
|
95
105
|
debug(`Payload ready: ${formatSize(totalSize)} (${prepareMs}ms)`);
|
|
96
106
|
|
package/src/lib/hooks.ts
CHANGED
|
@@ -533,8 +533,7 @@ const actionHandlers: {
|
|
|
533
533
|
const { isCancel, text } = await import("@clack/prompts");
|
|
534
534
|
const value = await text({ message: promptMsg });
|
|
535
535
|
|
|
536
|
-
if (isCancel(value) || !value.trim()) {
|
|
537
|
-
ui.warn(`Skipped ${action.key}`);
|
|
536
|
+
if (isCancel(value) || !value || !value.trim()) {
|
|
538
537
|
return false;
|
|
539
538
|
}
|
|
540
539
|
|
|
@@ -702,7 +701,9 @@ const actionHandlers: {
|
|
|
702
701
|
const proc = Bun.spawn(["sh", "-c", command], {
|
|
703
702
|
cwd,
|
|
704
703
|
stdin: interactive ? "inherit" : "ignore",
|
|
705
|
-
stdout
|
|
704
|
+
// In non-interactive mode (MCP), stdout must NOT inherit because it would
|
|
705
|
+
// write into the JSON-RPC stdio transport and corrupt the protocol.
|
|
706
|
+
stdout: interactive ? "inherit" : "pipe",
|
|
706
707
|
stderr: "inherit",
|
|
707
708
|
});
|
|
708
709
|
await proc.exited;
|