@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/package.json
CHANGED
|
@@ -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
|
+
}
|
package/src/commands/link.ts
CHANGED
|
@@ -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");
|
package/src/commands/mcp.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import {
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
+
}
|
package/src/commands/secrets.ts
CHANGED
|
@@ -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) {
|
package/src/commands/services.ts
CHANGED
|
@@ -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
|
-
|
|
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();
|
package/src/commands/ship.ts
CHANGED
|
@@ -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;
|
package/src/commands/tokens.ts
CHANGED
|
@@ -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
|
-
|
|
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>");
|
package/src/commands/whoami.ts
CHANGED
|
@@ -1,28 +1,63 @@
|
|
|
1
|
+
import { authFetch } from "../lib/auth/index.ts";
|
|
1
2
|
import { getCredentials } from "../lib/auth/store.ts";
|
|
2
|
-
import {
|
|
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
|
|
16
|
-
item(`ID: ${creds
|
|
50
|
+
item(`Email: ${creds!.user.email}`);
|
|
51
|
+
item(`ID: ${creds!.user.id}`);
|
|
17
52
|
|
|
18
|
-
if (creds
|
|
19
|
-
item(`Name: ${creds
|
|
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 (
|
|
57
|
+
if (apiToken) {
|
|
23
58
|
item("Auth: API token");
|
|
24
59
|
} else {
|
|
25
|
-
const expiresIn = creds
|
|
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), {
|