@getjack/jack 0.1.0 → 0.1.1

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.
@@ -0,0 +1,261 @@
1
+ import type { Server as McpServer } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
3
+ import { z } from "zod";
4
+ import { JackError, JackErrorCode } from "../../lib/errors.ts";
5
+ import {
6
+ createProject,
7
+ deployProject,
8
+ getProjectStatus,
9
+ listAllProjects,
10
+ } from "../../lib/project-operations.ts";
11
+ import { Events, track, withTelemetry } from "../../lib/telemetry.ts";
12
+ import type { McpServerOptions } from "../types.ts";
13
+ import { formatErrorResponse, formatSuccessResponse } from "../utils.ts";
14
+
15
+ // Tool schemas
16
+ const CreateProjectSchema = z.object({
17
+ name: z.string().optional().describe("Project name (auto-generated if not provided)"),
18
+ template: z.string().optional().describe("Template to use (e.g., 'miniapp', 'api')"),
19
+ });
20
+
21
+ const DeployProjectSchema = z.object({
22
+ project_path: z
23
+ .string()
24
+ .optional()
25
+ .describe("Path to project directory (defaults to current directory)"),
26
+ });
27
+
28
+ const GetProjectStatusSchema = z.object({
29
+ name: z.string().optional().describe("Project name (auto-detected if not provided)"),
30
+ project_path: z
31
+ .string()
32
+ .optional()
33
+ .describe("Path to project directory (defaults to current directory)"),
34
+ });
35
+
36
+ const ListProjectsSchema = z.object({
37
+ filter: z
38
+ .enum(["all", "local", "deployed", "cloud"])
39
+ .optional()
40
+ .describe("Filter projects by status (defaults to 'all')"),
41
+ });
42
+
43
+ export function registerTools(server: McpServer, _options: McpServerOptions) {
44
+ // Register tool list handler
45
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
46
+ return {
47
+ tools: [
48
+ {
49
+ name: "create_project",
50
+ description:
51
+ "Create a new Cloudflare Workers project from a template. Automatically installs dependencies, deploys to Cloudflare, and registers the project.",
52
+ inputSchema: {
53
+ type: "object",
54
+ properties: {
55
+ name: {
56
+ type: "string",
57
+ description: "Project name (auto-generated if not provided)",
58
+ },
59
+ template: {
60
+ type: "string",
61
+ description: "Template to use (e.g., 'miniapp', 'api')",
62
+ },
63
+ },
64
+ },
65
+ },
66
+ {
67
+ name: "deploy_project",
68
+ description:
69
+ "Deploy an existing project to Cloudflare Workers. Builds the project if needed and pushes to production.",
70
+ inputSchema: {
71
+ type: "object",
72
+ properties: {
73
+ project_path: {
74
+ type: "string",
75
+ description: "Path to project directory (defaults to current directory)",
76
+ },
77
+ },
78
+ },
79
+ },
80
+ {
81
+ name: "get_project_status",
82
+ description:
83
+ "Get detailed status information for a specific project, including deployment status, local path, and cloud backup status.",
84
+ inputSchema: {
85
+ type: "object",
86
+ properties: {
87
+ name: {
88
+ type: "string",
89
+ description: "Project name (auto-detected if not provided)",
90
+ },
91
+ project_path: {
92
+ type: "string",
93
+ description: "Path to project directory (defaults to current directory)",
94
+ },
95
+ },
96
+ },
97
+ },
98
+ {
99
+ name: "list_projects",
100
+ description:
101
+ "List all known projects with their status information. Can filter by local, deployed, or cloud-backed projects.",
102
+ inputSchema: {
103
+ type: "object",
104
+ properties: {
105
+ filter: {
106
+ type: "string",
107
+ enum: ["all", "local", "deployed", "cloud"],
108
+ description: "Filter projects by status (defaults to 'all')",
109
+ },
110
+ },
111
+ },
112
+ },
113
+ ],
114
+ };
115
+ });
116
+
117
+ // Register single tools/call handler that dispatches to individual tool implementations
118
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
119
+ const startTime = Date.now();
120
+
121
+ try {
122
+ switch (request.params.name) {
123
+ case "create_project": {
124
+ const args = CreateProjectSchema.parse(request.params.arguments ?? {});
125
+
126
+ const wrappedCreateProject = withTelemetry(
127
+ "create_project",
128
+ async (name?: string, template?: string) => {
129
+ const result = await createProject(name, {
130
+ template,
131
+ interactive: false,
132
+ });
133
+
134
+ // Track business event
135
+ track(Events.PROJECT_CREATED, {
136
+ template: template ?? "default",
137
+ platform: "mcp",
138
+ });
139
+
140
+ return result;
141
+ },
142
+ { platform: "mcp" },
143
+ );
144
+
145
+ const result = await wrappedCreateProject(args.name, args.template);
146
+
147
+ return {
148
+ content: [
149
+ {
150
+ type: "text",
151
+ text: JSON.stringify(formatSuccessResponse(result, startTime), null, 2),
152
+ },
153
+ ],
154
+ };
155
+ }
156
+
157
+ case "deploy_project": {
158
+ const args = DeployProjectSchema.parse(request.params.arguments ?? {});
159
+
160
+ const wrappedDeployProject = withTelemetry(
161
+ "deploy_project",
162
+ async (projectPath?: string) => {
163
+ const result = await deployProject({
164
+ projectPath,
165
+ interactive: false,
166
+ includeSecrets: false,
167
+ includeSync: false,
168
+ });
169
+
170
+ // Track business event
171
+ track(Events.DEPLOY_STARTED, {
172
+ platform: "mcp",
173
+ });
174
+
175
+ return result;
176
+ },
177
+ { platform: "mcp" },
178
+ );
179
+
180
+ const result = await wrappedDeployProject(args.project_path);
181
+
182
+ return {
183
+ content: [
184
+ {
185
+ type: "text",
186
+ text: JSON.stringify(formatSuccessResponse(result, startTime), null, 2),
187
+ },
188
+ ],
189
+ };
190
+ }
191
+
192
+ case "get_project_status": {
193
+ const args = GetProjectStatusSchema.parse(request.params.arguments ?? {});
194
+
195
+ const wrappedGetProjectStatus = withTelemetry(
196
+ "get_project_status",
197
+ async (name?: string, projectPath?: string) => {
198
+ return await getProjectStatus(name, projectPath);
199
+ },
200
+ { platform: "mcp" },
201
+ );
202
+
203
+ const result = await wrappedGetProjectStatus(args.name, args.project_path);
204
+
205
+ if (result === null) {
206
+ throw new JackError(
207
+ JackErrorCode.PROJECT_NOT_FOUND,
208
+ "Project not found",
209
+ "Use list_projects to see available projects",
210
+ );
211
+ }
212
+
213
+ return {
214
+ content: [
215
+ {
216
+ type: "text",
217
+ text: JSON.stringify(formatSuccessResponse(result, startTime), null, 2),
218
+ },
219
+ ],
220
+ };
221
+ }
222
+
223
+ case "list_projects": {
224
+ const args = ListProjectsSchema.parse(request.params.arguments ?? {});
225
+
226
+ const wrappedListProjects = withTelemetry(
227
+ "list_projects",
228
+ async (filter?: "all" | "local" | "deployed" | "cloud") => {
229
+ return await listAllProjects(filter);
230
+ },
231
+ { platform: "mcp" },
232
+ );
233
+
234
+ const result = await wrappedListProjects(args.filter);
235
+
236
+ return {
237
+ content: [
238
+ {
239
+ type: "text",
240
+ text: JSON.stringify(formatSuccessResponse(result, startTime), null, 2),
241
+ },
242
+ ],
243
+ };
244
+ }
245
+
246
+ default:
247
+ throw new Error(`Unknown tool: ${request.params.name}`);
248
+ }
249
+ } catch (error) {
250
+ return {
251
+ content: [
252
+ {
253
+ type: "text",
254
+ text: JSON.stringify(formatErrorResponse(error, startTime), null, 2),
255
+ },
256
+ ],
257
+ isError: true,
258
+ };
259
+ }
260
+ });
261
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * MCP tool response format
3
+ * Provides structured, machine-readable responses for AI agents
4
+ */
5
+ export interface McpToolResponse<T = unknown> {
6
+ success: boolean;
7
+ data?: T;
8
+ error?: {
9
+ code: string; // Machine-readable: 'AUTH_FAILED', 'PROJECT_NOT_FOUND'
10
+ message: string; // Human-readable description
11
+ suggestion?: string; // What to do next
12
+ };
13
+ meta?: {
14
+ duration_ms: number;
15
+ jack_version: string;
16
+ };
17
+ }
18
+
19
+ /**
20
+ * Error codes for MCP tool responses
21
+ */
22
+ export { JackErrorCode as McpErrorCode } from "../lib/errors.ts";
23
+
24
+ /**
25
+ * MCP server configuration options
26
+ */
27
+ export interface McpServerOptions {
28
+ projectPath?: string;
29
+ }
@@ -0,0 +1,147 @@
1
+ import packageJson from "../../package.json" with { type: "json" };
2
+ import { isJackError } from "../lib/errors.ts";
3
+ import { McpErrorCode, type McpToolResponse } from "./types.ts";
4
+
5
+ /**
6
+ * Format a successful MCP tool response
7
+ */
8
+ export function formatSuccessResponse<T>(data: T, startTime: number): McpToolResponse<T> {
9
+ return {
10
+ success: true,
11
+ data,
12
+ meta: {
13
+ duration_ms: Date.now() - startTime,
14
+ jack_version: packageJson.version,
15
+ },
16
+ };
17
+ }
18
+
19
+ /**
20
+ * Format an error MCP tool response
21
+ */
22
+ export function formatErrorResponse(error: unknown, startTime: number): McpToolResponse {
23
+ const message = error instanceof Error ? error.message : String(error);
24
+ const code = classifyMcpError(error);
25
+ const suggestion = isJackError(error)
26
+ ? error.suggestion ?? getSuggestionForError(code)
27
+ : getSuggestionForError(code);
28
+
29
+ return {
30
+ success: false,
31
+ error: {
32
+ code,
33
+ message,
34
+ suggestion,
35
+ },
36
+ meta: {
37
+ duration_ms: Date.now() - startTime,
38
+ jack_version: packageJson.version,
39
+ },
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Classify an error into an MCP error code
45
+ */
46
+ export function classifyMcpError(error: unknown): McpErrorCode {
47
+ if (isJackError(error)) {
48
+ return error.code as McpErrorCode;
49
+ }
50
+
51
+ if (!(error instanceof Error)) {
52
+ return McpErrorCode.INTERNAL_ERROR;
53
+ }
54
+
55
+ const message = error.message.toLowerCase();
56
+
57
+ // Authentication errors
58
+ if (
59
+ message.includes("not authenticated") ||
60
+ message.includes("authentication failed") ||
61
+ message.includes("invalid token")
62
+ ) {
63
+ return McpErrorCode.AUTH_FAILED;
64
+ }
65
+
66
+ // Wrangler-specific auth
67
+ if (
68
+ message.includes("wrangler") &&
69
+ (message.includes("auth") || message.includes("login") || message.includes("expired"))
70
+ ) {
71
+ return McpErrorCode.WRANGLER_AUTH_EXPIRED;
72
+ }
73
+
74
+ // Project not found
75
+ if (
76
+ message.includes("project not found") ||
77
+ message.includes("no project") ||
78
+ message.includes("directory not found")
79
+ ) {
80
+ return McpErrorCode.PROJECT_NOT_FOUND;
81
+ }
82
+
83
+ // Template not found
84
+ if (message.includes("template not found") || message.includes("invalid template")) {
85
+ return McpErrorCode.TEMPLATE_NOT_FOUND;
86
+ }
87
+
88
+ // Build failures
89
+ if (
90
+ message.includes("build failed") ||
91
+ message.includes("compilation error") ||
92
+ message.includes("syntax error")
93
+ ) {
94
+ return McpErrorCode.BUILD_FAILED;
95
+ }
96
+
97
+ // Deploy failures
98
+ if (
99
+ message.includes("deploy failed") ||
100
+ message.includes("deployment failed") ||
101
+ message.includes("publish failed")
102
+ ) {
103
+ return McpErrorCode.DEPLOY_FAILED;
104
+ }
105
+
106
+ // Validation errors
107
+ if (
108
+ message.includes("validation") ||
109
+ message.includes("invalid") ||
110
+ message.includes("required")
111
+ ) {
112
+ return McpErrorCode.VALIDATION_ERROR;
113
+ }
114
+
115
+ return McpErrorCode.INTERNAL_ERROR;
116
+ }
117
+
118
+ /**
119
+ * Get a helpful suggestion for an error code
120
+ */
121
+ export function getSuggestionForError(code: McpErrorCode): string {
122
+ switch (code) {
123
+ case McpErrorCode.AUTH_FAILED:
124
+ return "Check your authentication credentials and try again.";
125
+
126
+ case McpErrorCode.WRANGLER_AUTH_EXPIRED:
127
+ return "Run 'wrangler login' to re-authenticate with Cloudflare.";
128
+
129
+ case McpErrorCode.PROJECT_NOT_FOUND:
130
+ return "Ensure you're in a valid jack project directory or specify the project path.";
131
+
132
+ case McpErrorCode.TEMPLATE_NOT_FOUND:
133
+ return "Use a valid template name. Run 'jack new --help' to see available templates.";
134
+
135
+ case McpErrorCode.BUILD_FAILED:
136
+ return "Check your code for syntax errors and ensure all dependencies are installed.";
137
+
138
+ case McpErrorCode.DEPLOY_FAILED:
139
+ return "Verify your Cloudflare configuration and check the deployment logs for details.";
140
+
141
+ case McpErrorCode.VALIDATION_ERROR:
142
+ return "Review the error message and ensure all required fields are provided correctly.";
143
+
144
+ default:
145
+ return "An unexpected error occurred. Check the error message for details.";
146
+ }
147
+ }
@@ -60,6 +60,7 @@ async function loadTemplate(name: string): Promise<Template> {
60
60
  description: string;
61
61
  secrets: string[];
62
62
  capabilities?: Template["capabilities"];
63
+ requires?: Template["requires"];
63
64
  hooks?: Template["hooks"];
64
65
  } = { name, description: "", secrets: [] };
65
66
  if (existsSync(metadataPath)) {
@@ -73,6 +74,7 @@ async function loadTemplate(name: string): Promise<Template> {
73
74
  description: metadata.description,
74
75
  secrets: metadata.secrets,
75
76
  capabilities: metadata.capabilities,
77
+ requires: metadata.requires,
76
78
  hooks: metadata.hooks,
77
79
  files,
78
80
  };
@@ -2,13 +2,17 @@
2
2
  export type HookAction =
3
3
  | { action: "message"; text: string }
4
4
  | { action: "box"; title: string; lines: string[] } // boxed info panel
5
- | { action: "link"; url: string; label?: string; prompt?: boolean } // show link, optionally ask to open
6
- | { action: "open"; url: string } // auto-open (use sparingly)
7
- | { action: "checkSecret"; secret: string; message?: string; setupUrl?: string }
8
- | { action: "checkEnv"; env: string; message?: string }
9
- | { action: "copy"; text: string; message?: string }
10
- | { action: "wait"; message?: string } // press enter to continue
11
- | { action: "run"; command: string; cwd?: "project"; message?: string }; // run shell command
5
+ | { action: "url"; url: string; label?: string; open?: boolean; prompt?: boolean }
6
+ | { action: "clipboard"; text: string; message?: string }
7
+ | { action: "shell"; command: string; cwd?: "project"; message?: string }
8
+ | { action: "pause"; message?: string } // press enter to continue
9
+ | {
10
+ action: "require";
11
+ source: "secret" | "env";
12
+ key: string;
13
+ message?: string;
14
+ setupUrl?: string;
15
+ };
12
16
 
13
17
  export interface TemplateHooks {
14
18
  preDeploy?: HookAction[];
@@ -18,6 +22,9 @@ export interface TemplateHooks {
18
22
  // Supported infrastructure capabilities
19
23
  export type Capability = "db" | "kv" | "r2" | "queue" | "ai";
20
24
 
25
+ // Service type key from services library
26
+ import type { ServiceTypeKey } from "../lib/services/index.ts";
27
+
21
28
  export interface AgentContext {
22
29
  summary: string;
23
30
  full_text: string;
@@ -26,7 +33,8 @@ export interface AgentContext {
26
33
  export interface Template {
27
34
  files: Record<string, string>; // path -> content
28
35
  secrets?: string[]; // required secret keys (e.g., ["NEYNAR_API_KEY"])
29
- capabilities?: Capability[]; // infrastructure requirements
36
+ capabilities?: Capability[]; // infrastructure requirements (deprecated, use requires)
37
+ requires?: ServiceTypeKey[]; // service requirements (DB, KV, CRON, QUEUE, STORAGE)
30
38
  description?: string; // for help text
31
39
  hooks?: TemplateHooks;
32
40
  agentContext?: AgentContext;
@@ -85,11 +85,112 @@ Before shipping a new template:
85
85
  - [ ] All scripts work without global tools except wrangler
86
86
  - [ ] `.gitignore` includes `.env`, `.dev.vars`, `.secrets.json`
87
87
 
88
+ ## Placeholder System
89
+
90
+ All templates use **`jack-template`** as the universal placeholder. When a user runs `jack new my-app`, every occurrence of `jack-template` is replaced with `my-app`.
91
+
92
+ ```
93
+ # In template files:
94
+ name = "jack-template" → name = "my-app"
95
+ "database_name": "jack-template-db" → "database_name": "my-app-db"
96
+ ```
97
+
98
+ **Rules:**
99
+ - Use `jack-template` for project name in all files (wrangler.toml, package.json, etc.)
100
+ - Use `jack-template-db` for database names (replaced with `my-app-db`)
101
+ - The `-db` variant is replaced first to avoid partial matches
102
+ - No other placeholder syntax needed—just these two strings
103
+
104
+ **Why universal placeholder?**
105
+ - Templates are self-contained (no jack core changes needed)
106
+ - GitHub templates work automatically
107
+ - Simple string replacement, no complex parsing
108
+
109
+ ## Hook System
110
+
111
+ Templates can define hooks in `.jack.json` that run at specific lifecycle points.
112
+
113
+ ### Hook Lifecycle
114
+
115
+ ```json
116
+ {
117
+ "hooks": {
118
+ "preDeploy": [...], // Before wrangler deploy (validation)
119
+ "postDeploy": [...] // After successful deploy (notifications, testing)
120
+ }
121
+ }
122
+ ```
123
+
124
+ ### Available Actions
125
+
126
+ | Action | Purpose | Example |
127
+ |--------|---------|---------|
128
+ | `message` | Print info message | `{"action": "message", "text": "Setting up..."}` |
129
+ | `box` | Display boxed message | `{"action": "box", "title": "Done", "lines": ["URL: {{url}}"]}` |
130
+ | `url` | Show URL (optional prompt/open) | `{"action": "url", "url": "{{url}}", "label": "Open site", "prompt": true}` |
131
+ | `clipboard` | Copy text to clipboard | `{"action": "clipboard", "text": "{{url}}", "message": "Copied!"}` |
132
+ | `shell` | Execute shell command | `{"action": "shell", "command": "curl {{url}}/health"}` |
133
+ | `pause` | Wait for Enter key | `{"action": "pause", "message": "Press Enter..."}` |
134
+ | `require` | Verify secret or env | `{"action": "require", "source": "secret", "key": "API_KEY"}` |
135
+
136
+ ### Non-Interactive Mode
137
+
138
+ Hooks run in a non-interactive mode for MCP/silent execution. In this mode:
139
+
140
+ - `url` prints `Label: URL` (no prompt, no auto-open)
141
+ - `clipboard` prints the text (no clipboard access)
142
+ - `pause` is skipped
143
+ - `require` still validates; if `setupUrl` exists it prints `Setup: ...`
144
+ - `shell` runs with stdin ignored to avoid hangs
145
+
146
+ ### Hook Variables
147
+
148
+ These variables are substituted at runtime (different from template placeholders):
149
+
150
+ | Variable | Value | Available in |
151
+ |----------|-------|--------------|
152
+ | `{{url}}` | Full deployed URL | postDeploy |
153
+ | `{{domain}}` | Domain without protocol | postDeploy |
154
+ | `{{name}}` | Project name | preDeploy, postDeploy |
155
+
156
+ ### Example: API Template Hooks
157
+
158
+ ```json
159
+ {
160
+ "hooks": {
161
+ "postDeploy": [
162
+ {"action": "clipboard", "text": "{{url}}", "message": "URL copied"},
163
+ {"action": "shell", "command": "curl -s {{url}}/health"},
164
+ {"action": "box", "title": "{{name}}", "lines": ["{{url}}", "", "API is live!"]}
165
+ ]
166
+ }
167
+ }
168
+ ```
169
+
170
+ ### Example: Miniapp Template Hooks
171
+
172
+ ```json
173
+ {
174
+ "hooks": {
175
+ "preDeploy": [
176
+ {"action": "require", "source": "secret", "key": "NEYNAR_API_KEY", "setupUrl": "https://neynar.com"}
177
+ ],
178
+ "postDeploy": [
179
+ {"action": "clipboard", "text": "{{url}}"},
180
+ {"action": "box", "title": "Deployed: {{name}}", "lines": ["URL: {{url}}"]},
181
+ {"action": "url", "url": "https://farcaster.xyz/.../manifest?domain={{domain}}", "label": "Generate manifest"},
182
+ {"action": "url", "url": "https://farcaster.xyz/.../preview?url={{url}}", "label": "Preview"}
183
+ ]
184
+ }
185
+ }
186
+ ```
187
+
88
188
  ## Adding New Templates
89
189
 
90
190
  1. Create directory: `templates/my-template/`
91
191
  2. Add `.jack.json` with metadata and hooks
92
- 3. Add all template files
93
- 4. Generate lockfile: `cd templates/my-template && bun install`
94
- 5. Test: `jack new test-project -t my-template`
95
- 6. Verify install time with `./test-lockfile-timing.sh`
192
+ 3. Use `jack-template` placeholder in all files
193
+ 4. Add all template files
194
+ 5. Generate lockfile: `cd templates/my-template && bun install`
195
+ 6. Test: `jack new test-project -t my-template`
196
+ 7. Verify install time with `./test-lockfile-timing.sh`
@@ -1,5 +1,24 @@
1
1
  {
2
2
  "name": "api",
3
3
  "description": "Hono API with routing",
4
- "secrets": []
4
+ "secrets": [],
5
+ "hooks": {
6
+ "postDeploy": [
7
+ {
8
+ "action": "clipboard",
9
+ "text": "{{url}}",
10
+ "message": "Deploy URL copied to clipboard"
11
+ },
12
+ {
13
+ "action": "shell",
14
+ "command": "curl -s {{url}}/health | head -c 200",
15
+ "message": "Testing API..."
16
+ },
17
+ {
18
+ "action": "box",
19
+ "title": "Deployed: {{name}}",
20
+ "lines": ["URL: {{url}}", "", "API is live!"]
21
+ }
22
+ ]
23
+ }
5
24
  }
@@ -6,7 +6,7 @@ const app = new Hono();
6
6
  app.use("/*", cors());
7
7
 
8
8
  app.get("/", (c) => {
9
- return c.json({ message: "Hello from {{name}}!" });
9
+ return c.json({ message: "Hello from jack-template!" });
10
10
  });
11
11
 
12
12
  app.get("/health", (c) => {
@@ -3,6 +3,7 @@
3
3
  "description": "Farcaster Miniapp (React + Vite)",
4
4
  "secrets": ["NEYNAR_API_KEY"],
5
5
  "capabilities": ["db"],
6
+ "requires": ["DB"],
6
7
  "agentContext": {
7
8
  "summary": "A Farcaster miniapp using React + Vite frontend, Hono API on Cloudflare Workers, with D1 SQLite database.",
8
9
  "full_text": "## Project Structure\n\n- `src/App.tsx` - React application entry point\n- `src/worker.ts` - Hono API routes for backend\n- `src/components/` - React components\n- `schema.sql` - D1 database schema\n- `wrangler.jsonc` - Cloudflare Workers configuration\n\n## Conventions\n\n- API routes are defined with Hono in `src/worker.ts`\n- Frontend uses Vite for bundling and is served as static assets\n- Database uses D1 prepared statements for queries\n- Secrets are managed via jack and pushed to Cloudflare\n- Wrangler is installed globally by jack, not in project dependencies\n\n## Resources\n\n- [Farcaster Miniapp Docs](https://miniapps.farcaster.xyz)\n- [Hono Documentation](https://hono.dev)\n- [Cloudflare D1 Docs](https://developers.cloudflare.com/d1)"
@@ -10,15 +11,16 @@
10
11
  "hooks": {
11
12
  "preDeploy": [
12
13
  {
13
- "action": "checkSecret",
14
- "secret": "NEYNAR_API_KEY",
14
+ "action": "require",
15
+ "source": "secret",
16
+ "key": "NEYNAR_API_KEY",
15
17
  "message": "NEYNAR_API_KEY is required for Farcaster miniapps",
16
18
  "setupUrl": "https://neynar.com"
17
19
  }
18
20
  ],
19
21
  "postDeploy": [
20
22
  {
21
- "action": "copy",
23
+ "action": "clipboard",
22
24
  "text": "{{url}}",
23
25
  "message": "Deploy URL copied to clipboard"
24
26
  },
@@ -28,12 +30,12 @@
28
30
  "lines": ["URL: {{url}}", "", "Next: Generate a manifest, then preview your miniapp"]
29
31
  },
30
32
  {
31
- "action": "link",
33
+ "action": "url",
32
34
  "url": "https://farcaster.xyz/~/developers/mini-apps/manifest?domain={{domain}}",
33
35
  "label": "Generate manifest"
34
36
  },
35
37
  {
36
- "action": "link",
38
+ "action": "url",
37
39
  "url": "https://farcaster.xyz/~/developers/mini-apps/preview?url={{url}}",
38
40
  "label": "Preview in Farcaster"
39
41
  }