@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.
- package/README.md +16 -0
- package/package.json +47 -39
- package/src/commands/agents.ts +40 -9
- package/src/commands/cloud.ts +8 -4
- package/src/commands/down.ts +120 -69
- package/src/commands/init.ts +41 -3
- package/src/commands/mcp.ts +18 -0
- package/src/commands/new.ts +64 -334
- package/src/commands/projects.ts +139 -143
- package/src/commands/services.ts +315 -0
- package/src/commands/ship.ts +33 -139
- package/src/index.ts +27 -3
- package/src/lib/agent-files.ts +0 -41
- package/src/lib/agents.ts +238 -64
- package/src/lib/cloudflare-api.ts +3 -2
- package/src/lib/config.ts +8 -0
- package/src/lib/errors.ts +53 -0
- package/src/lib/hooks.ts +93 -41
- package/src/lib/mcp-config.ts +175 -0
- package/src/lib/project-operations.ts +793 -0
- package/src/lib/prompts.ts +15 -7
- package/src/lib/registry.ts +29 -1
- package/src/lib/services/db.ts +81 -0
- package/src/lib/services/index.ts +27 -0
- package/src/lib/telemetry.ts +10 -1
- package/src/mcp/README.md +142 -0
- package/src/mcp/resources/index.ts +87 -0
- package/src/mcp/server.ts +32 -0
- package/src/mcp/tools/index.ts +261 -0
- package/src/mcp/types.ts +29 -0
- package/src/mcp/utils.ts +147 -0
- package/src/templates/index.ts +2 -0
- package/src/templates/types.ts +16 -8
- package/templates/CLAUDE.md +105 -4
- package/templates/api/.jack.json +20 -1
- package/templates/api/src/index.ts +1 -1
- package/templates/miniapp/.jack.json +7 -5
|
@@ -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
|
+
}
|
package/src/mcp/types.ts
ADDED
|
@@ -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
|
+
}
|
package/src/mcp/utils.ts
ADDED
|
@@ -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
|
+
}
|
package/src/templates/index.ts
CHANGED
|
@@ -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
|
};
|
package/src/templates/types.ts
CHANGED
|
@@ -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: "
|
|
6
|
-
| { action: "
|
|
7
|
-
| { action: "
|
|
8
|
-
| { action: "
|
|
9
|
-
| {
|
|
10
|
-
|
|
11
|
-
|
|
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;
|
package/templates/CLAUDE.md
CHANGED
|
@@ -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.
|
|
93
|
-
4.
|
|
94
|
-
5.
|
|
95
|
-
6.
|
|
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`
|
package/templates/api/.jack.json
CHANGED
|
@@ -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
|
}
|
|
@@ -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": "
|
|
14
|
-
"
|
|
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": "
|
|
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": "
|
|
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": "
|
|
38
|
+
"action": "url",
|
|
37
39
|
"url": "https://farcaster.xyz/~/developers/mini-apps/preview?url={{url}}",
|
|
38
40
|
"label": "Preview in Farcaster"
|
|
39
41
|
}
|