@getjack/jack 0.1.2 → 0.1.3
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 +54 -47
- package/src/commands/agents.ts +145 -10
- package/src/commands/down.ts +110 -102
- package/src/commands/feedback.ts +189 -0
- package/src/commands/init.ts +8 -12
- package/src/commands/login.ts +88 -0
- package/src/commands/logout.ts +14 -0
- package/src/commands/logs.ts +21 -0
- package/src/commands/mcp.ts +134 -7
- package/src/commands/new.ts +43 -17
- package/src/commands/open.ts +13 -6
- package/src/commands/projects.ts +269 -143
- package/src/commands/secrets.ts +413 -0
- package/src/commands/services.ts +96 -123
- package/src/commands/ship.ts +5 -1
- package/src/commands/whoami.ts +31 -0
- package/src/index.ts +218 -144
- package/src/lib/agent-files.ts +34 -0
- package/src/lib/agents.ts +390 -22
- package/src/lib/asset-hash.ts +50 -0
- package/src/lib/auth/client.ts +115 -0
- package/src/lib/auth/constants.ts +5 -0
- package/src/lib/auth/guard.ts +57 -0
- package/src/lib/auth/index.ts +18 -0
- package/src/lib/auth/store.ts +54 -0
- package/src/lib/binding-validator.ts +136 -0
- package/src/lib/build-helper.ts +211 -0
- package/src/lib/cloudflare-api.ts +24 -0
- package/src/lib/config.ts +5 -6
- package/src/lib/control-plane.ts +295 -0
- package/src/lib/debug.ts +3 -1
- package/src/lib/deploy-mode.ts +93 -0
- package/src/lib/deploy-upload.ts +92 -0
- package/src/lib/errors.ts +2 -0
- package/src/lib/github.ts +31 -1
- package/src/lib/hooks.ts +4 -12
- package/src/lib/intent.ts +88 -0
- package/src/lib/jsonc.ts +125 -0
- package/src/lib/local-paths.test.ts +902 -0
- package/src/lib/local-paths.ts +258 -0
- package/src/lib/managed-deploy.ts +175 -0
- package/src/lib/managed-down.ts +159 -0
- package/src/lib/mcp-config.ts +55 -34
- package/src/lib/names.ts +9 -29
- package/src/lib/project-operations.ts +676 -249
- package/src/lib/project-resolver.ts +476 -0
- package/src/lib/registry.ts +76 -37
- package/src/lib/resources.ts +196 -0
- package/src/lib/schema.ts +30 -1
- package/src/lib/storage/file-filter.ts +1 -0
- package/src/lib/storage/index.ts +5 -1
- package/src/lib/telemetry.ts +14 -0
- package/src/lib/tty.ts +15 -0
- package/src/lib/zip-packager.ts +255 -0
- package/src/mcp/resources/index.ts +8 -2
- package/src/mcp/server.ts +32 -4
- package/src/mcp/tools/index.ts +35 -13
- package/src/mcp/types.ts +6 -0
- package/src/mcp/utils.ts +1 -1
- package/src/templates/index.ts +42 -4
- package/src/templates/types.ts +13 -0
- package/templates/CLAUDE.md +166 -0
- package/templates/api/.jack.json +4 -0
- package/templates/api/bun.lock +1 -0
- package/templates/api/wrangler.jsonc +5 -0
- package/templates/hello/.jack.json +28 -0
- package/templates/hello/package.json +10 -0
- package/templates/hello/src/index.ts +11 -0
- package/templates/hello/tsconfig.json +11 -0
- package/templates/hello/wrangler.jsonc +5 -0
- package/templates/miniapp/.jack.json +15 -4
- package/templates/miniapp/bun.lock +135 -40
- package/templates/miniapp/index.html +1 -0
- package/templates/miniapp/package.json +3 -1
- package/templates/miniapp/public/.well-known/farcaster.json +7 -5
- package/templates/miniapp/public/icon.png +0 -0
- package/templates/miniapp/public/og.png +0 -0
- package/templates/miniapp/schema.sql +8 -0
- package/templates/miniapp/src/App.tsx +254 -3
- package/templates/miniapp/src/components/ShareSheet.tsx +147 -0
- package/templates/miniapp/src/hooks/useAI.ts +35 -0
- package/templates/miniapp/src/hooks/useGuestbook.ts +11 -1
- package/templates/miniapp/src/hooks/useShare.ts +76 -0
- package/templates/miniapp/src/index.css +15 -0
- package/templates/miniapp/src/lib/api.ts +2 -1
- package/templates/miniapp/src/worker.ts +515 -1
- package/templates/miniapp/wrangler.jsonc +15 -3
- package/LICENSE +0 -190
- package/README.md +0 -55
- package/src/commands/cloud.ts +0 -230
- package/templates/api/wrangler.toml +0 -3
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource types and utilities for Jack CLI
|
|
3
|
+
*
|
|
4
|
+
* Resources are fetched on-demand from control plane (managed)
|
|
5
|
+
* or parsed from wrangler.jsonc (BYO).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { parseJsonc } from "./jsonc.ts";
|
|
11
|
+
|
|
12
|
+
// Resource types matching control plane schema
|
|
13
|
+
export type ResourceType =
|
|
14
|
+
| "worker"
|
|
15
|
+
| "d1"
|
|
16
|
+
| "r2_content"
|
|
17
|
+
| "kv"
|
|
18
|
+
| "queue"
|
|
19
|
+
| "ai"
|
|
20
|
+
| "hyperdrive"
|
|
21
|
+
| "vectorize";
|
|
22
|
+
|
|
23
|
+
// Resource from control plane API
|
|
24
|
+
export interface ControlPlaneResource {
|
|
25
|
+
id: string;
|
|
26
|
+
project_id: string;
|
|
27
|
+
resource_type: ResourceType;
|
|
28
|
+
resource_name: string;
|
|
29
|
+
provider_id: string;
|
|
30
|
+
status: "active" | "provisioning" | "error" | "deleted";
|
|
31
|
+
metadata?: Record<string, unknown>;
|
|
32
|
+
created_at: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Unified resource view (used by CLI)
|
|
36
|
+
export interface ResolvedResources {
|
|
37
|
+
d1?: { binding: string; name: string; id?: string };
|
|
38
|
+
ai?: { binding: string };
|
|
39
|
+
assets?: { binding: string; directory: string };
|
|
40
|
+
kv?: Array<{ binding: string; id: string; name?: string }>;
|
|
41
|
+
r2?: Array<{ binding: string; name: string }>;
|
|
42
|
+
queues?: Array<{ binding: string; name: string }>;
|
|
43
|
+
vars?: Record<string, string>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Convert control plane resources to unified format
|
|
48
|
+
*/
|
|
49
|
+
export function convertControlPlaneResources(resources: ControlPlaneResource[]): ResolvedResources {
|
|
50
|
+
const result: ResolvedResources = {};
|
|
51
|
+
|
|
52
|
+
for (const r of resources) {
|
|
53
|
+
switch (r.resource_type) {
|
|
54
|
+
case "d1":
|
|
55
|
+
result.d1 = {
|
|
56
|
+
binding: "DB",
|
|
57
|
+
name: r.resource_name,
|
|
58
|
+
id: r.provider_id,
|
|
59
|
+
};
|
|
60
|
+
break;
|
|
61
|
+
case "kv":
|
|
62
|
+
result.kv = result.kv || [];
|
|
63
|
+
result.kv.push({
|
|
64
|
+
binding: r.resource_name.toUpperCase().replace(/-/g, "_"),
|
|
65
|
+
id: r.provider_id,
|
|
66
|
+
name: r.resource_name,
|
|
67
|
+
});
|
|
68
|
+
break;
|
|
69
|
+
case "r2_content":
|
|
70
|
+
result.r2 = result.r2 || [];
|
|
71
|
+
result.r2.push({
|
|
72
|
+
binding: "BUCKET",
|
|
73
|
+
name: r.resource_name,
|
|
74
|
+
});
|
|
75
|
+
break;
|
|
76
|
+
// AI doesn't need provider_id, just indicates it's available
|
|
77
|
+
case "ai":
|
|
78
|
+
result.ai = { binding: "AI" };
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Parse resources from wrangler.jsonc for BYO projects.
|
|
88
|
+
* Returns a unified resource view.
|
|
89
|
+
*/
|
|
90
|
+
export async function parseWranglerResources(projectPath: string): Promise<ResolvedResources> {
|
|
91
|
+
const wranglerPath = join(projectPath, "wrangler.jsonc");
|
|
92
|
+
|
|
93
|
+
if (!existsSync(wranglerPath)) {
|
|
94
|
+
return {};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
// Read and parse JSONC (strip comments)
|
|
99
|
+
const content = await Bun.file(wranglerPath).text();
|
|
100
|
+
const config = parseJsonc<WranglerConfig>(content);
|
|
101
|
+
|
|
102
|
+
const resources: ResolvedResources = {};
|
|
103
|
+
|
|
104
|
+
// D1 databases
|
|
105
|
+
if (config.d1_databases?.[0]) {
|
|
106
|
+
const d1 = config.d1_databases[0];
|
|
107
|
+
resources.d1 = {
|
|
108
|
+
binding: d1.binding || "DB",
|
|
109
|
+
name: d1.database_name || d1.binding || "DB",
|
|
110
|
+
id: d1.database_id,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// AI binding
|
|
115
|
+
if (config.ai) {
|
|
116
|
+
resources.ai = {
|
|
117
|
+
binding: config.ai.binding || "AI",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Assets
|
|
122
|
+
if (config.assets?.directory) {
|
|
123
|
+
resources.assets = {
|
|
124
|
+
binding: config.assets.binding || "ASSETS",
|
|
125
|
+
directory: config.assets.directory,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// KV namespaces
|
|
130
|
+
if (config.kv_namespaces && config.kv_namespaces.length > 0) {
|
|
131
|
+
resources.kv = config.kv_namespaces.map((kv) => ({
|
|
132
|
+
binding: kv.binding,
|
|
133
|
+
id: kv.id,
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// R2 buckets
|
|
138
|
+
if (config.r2_buckets && config.r2_buckets.length > 0) {
|
|
139
|
+
resources.r2 = config.r2_buckets.map((r2) => ({
|
|
140
|
+
binding: r2.binding,
|
|
141
|
+
name: r2.bucket_name,
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Queues
|
|
146
|
+
if (config.queues?.producers && config.queues.producers.length > 0) {
|
|
147
|
+
resources.queues = config.queues.producers.map((q) => ({
|
|
148
|
+
binding: q.binding,
|
|
149
|
+
name: q.queue,
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Environment variables (vars)
|
|
154
|
+
if (config.vars && Object.keys(config.vars).length > 0) {
|
|
155
|
+
resources.vars = config.vars;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return resources;
|
|
159
|
+
} catch {
|
|
160
|
+
// Failed to parse, return empty
|
|
161
|
+
return {};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Wrangler config shape (partial, for resource parsing)
|
|
167
|
+
*/
|
|
168
|
+
interface WranglerConfig {
|
|
169
|
+
d1_databases?: Array<{
|
|
170
|
+
binding: string;
|
|
171
|
+
database_name?: string;
|
|
172
|
+
database_id?: string;
|
|
173
|
+
}>;
|
|
174
|
+
ai?: {
|
|
175
|
+
binding?: string;
|
|
176
|
+
};
|
|
177
|
+
assets?: {
|
|
178
|
+
binding?: string;
|
|
179
|
+
directory: string;
|
|
180
|
+
};
|
|
181
|
+
kv_namespaces?: Array<{
|
|
182
|
+
binding: string;
|
|
183
|
+
id: string;
|
|
184
|
+
}>;
|
|
185
|
+
r2_buckets?: Array<{
|
|
186
|
+
binding: string;
|
|
187
|
+
bucket_name: string;
|
|
188
|
+
}>;
|
|
189
|
+
queues?: {
|
|
190
|
+
producers?: Array<{
|
|
191
|
+
binding: string;
|
|
192
|
+
queue: string;
|
|
193
|
+
}>;
|
|
194
|
+
};
|
|
195
|
+
vars?: Record<string, string>;
|
|
196
|
+
}
|
package/src/lib/schema.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { $ } from "bun";
|
|
4
4
|
import { debug } from "./debug.ts";
|
|
5
|
+
import { parseJsonc } from "./jsonc.ts";
|
|
5
6
|
import { output } from "./output.ts";
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -60,6 +61,31 @@ export async function hasD1Config(projectDir: string): Promise<boolean> {
|
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
export interface D1Binding {
|
|
65
|
+
binding?: string;
|
|
66
|
+
database_id?: string;
|
|
67
|
+
database_name?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Read D1 bindings from wrangler.jsonc
|
|
72
|
+
*/
|
|
73
|
+
export async function getD1Bindings(projectDir: string): Promise<D1Binding[]> {
|
|
74
|
+
const wranglerPath = join(projectDir, "wrangler.jsonc");
|
|
75
|
+
|
|
76
|
+
if (!existsSync(wranglerPath)) {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const content = await Bun.file(wranglerPath).text();
|
|
82
|
+
const config = parseJsonc(content) as { d1_databases?: D1Binding[] };
|
|
83
|
+
return Array.isArray(config.d1_databases) ? config.d1_databases : [];
|
|
84
|
+
} catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
63
89
|
/**
|
|
64
90
|
* Get the D1 database name from wrangler config
|
|
65
91
|
* Returns the database_name field which is needed for wrangler d1 execute
|
|
@@ -74,7 +100,10 @@ export async function getD1DatabaseName(projectDir: string): Promise<string | nu
|
|
|
74
100
|
try {
|
|
75
101
|
const content = await Bun.file(wranglerPath).text();
|
|
76
102
|
// Strip comments for parsing
|
|
77
|
-
|
|
103
|
+
// Note: Only remove line comments at the start of a line to avoid breaking URLs
|
|
104
|
+
const cleaned = content
|
|
105
|
+
.replace(/\/\*[\s\S]*?\*\//g, "") // block comments
|
|
106
|
+
.replace(/^\s*\/\/.*$/gm, ""); // line comments at start of line only
|
|
78
107
|
const config = JSON.parse(cleaned);
|
|
79
108
|
|
|
80
109
|
return config.d1_databases?.[0]?.database_name || null;
|
package/src/lib/storage/index.ts
CHANGED
|
@@ -80,7 +80,11 @@ export async function getProjectNameFromDir(projectDir: string): Promise<string>
|
|
|
80
80
|
try {
|
|
81
81
|
const content = await Bun.file(jsoncPath).text();
|
|
82
82
|
// Remove comments and parse JSON
|
|
83
|
-
|
|
83
|
+
// Note: Only remove line comments at the start of a line (with optional whitespace)
|
|
84
|
+
// to avoid breaking URLs like https://example.com
|
|
85
|
+
const jsonContent = content
|
|
86
|
+
.replace(/\/\*[\s\S]*?\*\//g, "") // block comments
|
|
87
|
+
.replace(/^\s*\/\/.*$/gm, ""); // line comments at start of line only
|
|
84
88
|
const config = JSON.parse(jsonContent);
|
|
85
89
|
if (config.name) {
|
|
86
90
|
return config.name;
|
package/src/lib/telemetry.ts
CHANGED
|
@@ -16,6 +16,20 @@ export const Events = {
|
|
|
16
16
|
PROJECT_CREATED: "project_created",
|
|
17
17
|
DEPLOY_STARTED: "deploy_started",
|
|
18
18
|
CONFIG_CHANGED: "config_changed",
|
|
19
|
+
|
|
20
|
+
// Intent-driven creation events
|
|
21
|
+
INTENT_MATCHED: "intent_matched",
|
|
22
|
+
INTENT_NO_MATCH: "intent_no_match",
|
|
23
|
+
INTENT_CUSTOMIZATION_STARTED: "intent_customization_started",
|
|
24
|
+
INTENT_CUSTOMIZATION_COMPLETED: "intent_customization_completed",
|
|
25
|
+
INTENT_CUSTOMIZATION_FAILED: "intent_customization_failed",
|
|
26
|
+
|
|
27
|
+
// Deploy mode events
|
|
28
|
+
DEPLOY_MODE_SELECTED: "deploy_mode_selected",
|
|
29
|
+
MANAGED_PROJECT_CREATED: "managed_project_created",
|
|
30
|
+
MANAGED_DEPLOY_STARTED: "managed_deploy_started",
|
|
31
|
+
MANAGED_DEPLOY_COMPLETED: "managed_deploy_completed",
|
|
32
|
+
MANAGED_DEPLOY_FAILED: "managed_deploy_failed",
|
|
19
33
|
} as const;
|
|
20
34
|
|
|
21
35
|
type EventName = (typeof Events)[keyof typeof Events];
|
package/src/lib/tty.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function restoreTty(): void {
|
|
2
|
+
if (!process.stdin.isTTY) return;
|
|
3
|
+
|
|
4
|
+
try {
|
|
5
|
+
process.stdin.setRawMode(false);
|
|
6
|
+
} catch {
|
|
7
|
+
// Ignore if stdin does not support raw mode
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
process.stdin.pause();
|
|
11
|
+
|
|
12
|
+
if (process.stderr.isTTY) {
|
|
13
|
+
process.stderr.write("\x1b[?25h");
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { createWriteStream } from "node:fs";
|
|
3
|
+
import { mkdir, readFile, readdir, rm, stat } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join, relative } from "node:path";
|
|
6
|
+
import archiver from "archiver";
|
|
7
|
+
import { type AssetManifest, computeAssetHash } from "./asset-hash.ts";
|
|
8
|
+
import type { BuildOutput, WranglerConfig } from "./build-helper.ts";
|
|
9
|
+
import { scanProjectFiles } from "./storage/file-filter.ts";
|
|
10
|
+
|
|
11
|
+
export interface ZipPackageResult {
|
|
12
|
+
bundleZipPath: string;
|
|
13
|
+
sourceZipPath: string;
|
|
14
|
+
manifestPath: string;
|
|
15
|
+
schemaPath: string | null;
|
|
16
|
+
secretsPath: string | null;
|
|
17
|
+
assetsZipPath: string | null;
|
|
18
|
+
assetManifest: AssetManifest | null;
|
|
19
|
+
cleanup: () => Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ManifestData {
|
|
23
|
+
version: 1;
|
|
24
|
+
entrypoint: string;
|
|
25
|
+
compatibility_date: string;
|
|
26
|
+
compatibility_flags?: string[];
|
|
27
|
+
module_format: "esm";
|
|
28
|
+
assets_dir?: string;
|
|
29
|
+
built_at: string;
|
|
30
|
+
bindings?: {
|
|
31
|
+
d1?: { binding: string };
|
|
32
|
+
ai?: { binding: string };
|
|
33
|
+
assets?: {
|
|
34
|
+
binding: string;
|
|
35
|
+
directory: string;
|
|
36
|
+
not_found_handling?: "single-page-application" | "404-page" | "none";
|
|
37
|
+
html_handling?:
|
|
38
|
+
| "auto-trailing-slash"
|
|
39
|
+
| "force-trailing-slash"
|
|
40
|
+
| "drop-trailing-slash"
|
|
41
|
+
| "none";
|
|
42
|
+
};
|
|
43
|
+
vars?: Record<string, string>;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Creates a ZIP archive from source directory
|
|
49
|
+
* @param outputPath - Absolute path for output ZIP file
|
|
50
|
+
* @param sourceDir - Absolute path to directory to archive
|
|
51
|
+
* @param files - Optional list of specific files to include (relative to sourceDir)
|
|
52
|
+
* @returns Promise that resolves when ZIP is created
|
|
53
|
+
*/
|
|
54
|
+
async function createZipArchive(
|
|
55
|
+
outputPath: string,
|
|
56
|
+
sourceDir: string,
|
|
57
|
+
files?: string[],
|
|
58
|
+
): Promise<void> {
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
const output = createWriteStream(outputPath);
|
|
61
|
+
const archive = archiver("zip", { zlib: { level: 9 } });
|
|
62
|
+
|
|
63
|
+
output.on("close", () => resolve());
|
|
64
|
+
archive.on("error", (err) => reject(err));
|
|
65
|
+
|
|
66
|
+
archive.pipe(output);
|
|
67
|
+
|
|
68
|
+
if (files) {
|
|
69
|
+
// Add specific files
|
|
70
|
+
for (const file of files) {
|
|
71
|
+
const filePath = join(sourceDir, file);
|
|
72
|
+
archive.file(filePath, { name: file });
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
// Add entire directory
|
|
76
|
+
archive.directory(sourceDir, false);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
archive.finalize();
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Recursively collects all file paths in a directory
|
|
85
|
+
*/
|
|
86
|
+
async function collectAllFiles(dir: string, baseDir: string = dir): Promise<string[]> {
|
|
87
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
88
|
+
const files: string[] = [];
|
|
89
|
+
|
|
90
|
+
for (const entry of entries) {
|
|
91
|
+
const fullPath = join(dir, entry.name);
|
|
92
|
+
if (entry.isDirectory()) {
|
|
93
|
+
files.push(...(await collectAllFiles(fullPath, baseDir)));
|
|
94
|
+
} else if (entry.isFile()) {
|
|
95
|
+
files.push(fullPath);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return files;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Creates an asset manifest for all files in a directory.
|
|
104
|
+
* Maps paths starting with "/" (e.g., "/index.html", "/assets/app.js")
|
|
105
|
+
* to their content-addressable hashes.
|
|
106
|
+
*
|
|
107
|
+
* @param assetsDir - Absolute path to the assets directory
|
|
108
|
+
* @returns Asset manifest mapping paths to hash entries
|
|
109
|
+
*/
|
|
110
|
+
export async function createAssetManifest(assetsDir: string): Promise<AssetManifest> {
|
|
111
|
+
const allFiles = await collectAllFiles(assetsDir);
|
|
112
|
+
|
|
113
|
+
const entries = await Promise.all(
|
|
114
|
+
allFiles.map(async (filePath) => {
|
|
115
|
+
const content = await readFile(filePath);
|
|
116
|
+
const hash = await computeAssetHash(new Uint8Array(content), filePath);
|
|
117
|
+
const fileStats = await stat(filePath);
|
|
118
|
+
const relativePath = "/" + relative(assetsDir, filePath);
|
|
119
|
+
return [relativePath, { hash, size: fileStats.size }] as const;
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
return Object.fromEntries(entries);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Extracts binding intent from wrangler config for the manifest.
|
|
128
|
+
* Returns undefined if no bindings are configured.
|
|
129
|
+
*/
|
|
130
|
+
function extractBindingsFromConfig(config?: WranglerConfig): ManifestData["bindings"] | undefined {
|
|
131
|
+
if (!config) return undefined;
|
|
132
|
+
|
|
133
|
+
const bindings: NonNullable<ManifestData["bindings"]> = {};
|
|
134
|
+
|
|
135
|
+
// Extract D1 database binding (use first one if multiple)
|
|
136
|
+
if (config.d1_databases && config.d1_databases.length > 0) {
|
|
137
|
+
const firstDb = config.d1_databases[0];
|
|
138
|
+
if (firstDb) {
|
|
139
|
+
bindings.d1 = { binding: firstDb.binding };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Extract AI binding (default binding name: "AI")
|
|
144
|
+
if (config.ai) {
|
|
145
|
+
bindings.ai = { binding: config.ai.binding || "AI" };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Extract assets binding (defaults: binding="ASSETS", directory="./dist")
|
|
149
|
+
if (config.assets) {
|
|
150
|
+
bindings.assets = {
|
|
151
|
+
binding: config.assets.binding || "ASSETS",
|
|
152
|
+
directory: config.assets.directory || "./dist",
|
|
153
|
+
not_found_handling: config.assets.not_found_handling,
|
|
154
|
+
html_handling: config.assets.html_handling,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Extract vars
|
|
159
|
+
if (config.vars && Object.keys(config.vars).length > 0) {
|
|
160
|
+
bindings.vars = config.vars;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Return undefined if no bindings were extracted
|
|
164
|
+
return Object.keys(bindings).length > 0 ? bindings : undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Packages a built project for deployment to jack cloud
|
|
169
|
+
* @param projectPath - Absolute path to project directory
|
|
170
|
+
* @param buildOutput - Build output from buildProject()
|
|
171
|
+
* @param config - Optional wrangler config to extract binding intent
|
|
172
|
+
* @returns Package result with ZIP paths and cleanup function
|
|
173
|
+
*/
|
|
174
|
+
export async function packageForDeploy(
|
|
175
|
+
projectPath: string,
|
|
176
|
+
buildOutput: BuildOutput,
|
|
177
|
+
config?: WranglerConfig,
|
|
178
|
+
): Promise<ZipPackageResult> {
|
|
179
|
+
// Create temp directory for package artifacts
|
|
180
|
+
const packageId = `jack-package-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
181
|
+
const packageDir = join(tmpdir(), packageId);
|
|
182
|
+
await mkdir(packageDir, { recursive: true });
|
|
183
|
+
|
|
184
|
+
// Define artifact paths
|
|
185
|
+
const bundleZipPath = join(packageDir, "bundle.zip");
|
|
186
|
+
const sourceZipPath = join(packageDir, "source.zip");
|
|
187
|
+
const manifestPath = join(packageDir, "manifest.json");
|
|
188
|
+
|
|
189
|
+
// 1. Create bundle.zip from build output directory
|
|
190
|
+
await createZipArchive(bundleZipPath, buildOutput.outDir);
|
|
191
|
+
|
|
192
|
+
// 2. Create source.zip from project files (filtered)
|
|
193
|
+
const projectFiles = await scanProjectFiles(projectPath);
|
|
194
|
+
const sourceFiles = projectFiles.map((f) => f.path);
|
|
195
|
+
await createZipArchive(sourceZipPath, projectPath, sourceFiles);
|
|
196
|
+
|
|
197
|
+
// 3. Create manifest.json
|
|
198
|
+
const manifest: ManifestData = {
|
|
199
|
+
version: 1,
|
|
200
|
+
entrypoint: buildOutput.entrypoint,
|
|
201
|
+
compatibility_date: buildOutput.compatibilityDate,
|
|
202
|
+
compatibility_flags:
|
|
203
|
+
buildOutput.compatibilityFlags.length > 0 ? buildOutput.compatibilityFlags : undefined,
|
|
204
|
+
module_format: "esm",
|
|
205
|
+
assets_dir: buildOutput.assetsDir ? "assets" : undefined,
|
|
206
|
+
built_at: new Date().toISOString(),
|
|
207
|
+
bindings: extractBindingsFromConfig(config),
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
await Bun.write(manifestPath, JSON.stringify(manifest, null, 2));
|
|
211
|
+
|
|
212
|
+
// 4. Check for optional files (schema.sql and .secrets.json)
|
|
213
|
+
let schemaPath: string | null = null;
|
|
214
|
+
const schemaSrcPath = join(projectPath, "schema.sql");
|
|
215
|
+
if (existsSync(schemaSrcPath)) {
|
|
216
|
+
schemaPath = join(packageDir, "schema.sql");
|
|
217
|
+
await Bun.write(schemaPath, await readFile(schemaSrcPath));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
let secretsPath: string | null = null;
|
|
221
|
+
const secretsSrcPath = join(projectPath, ".secrets.json");
|
|
222
|
+
if (existsSync(secretsSrcPath)) {
|
|
223
|
+
secretsPath = join(packageDir, ".secrets.json");
|
|
224
|
+
await Bun.write(secretsPath, await readFile(secretsSrcPath));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 5. If assets directory exists, create assets.zip and asset manifest
|
|
228
|
+
let assetsZipPath: string | null = null;
|
|
229
|
+
let assetManifest: AssetManifest | null = null;
|
|
230
|
+
if (buildOutput.assetsDir) {
|
|
231
|
+
// Create zip and manifest in parallel for speed
|
|
232
|
+
const [, manifest] = await Promise.all([
|
|
233
|
+
createZipArchive(join(packageDir, "assets.zip"), buildOutput.assetsDir),
|
|
234
|
+
createAssetManifest(buildOutput.assetsDir),
|
|
235
|
+
]);
|
|
236
|
+
assetsZipPath = join(packageDir, "assets.zip");
|
|
237
|
+
assetManifest = manifest;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Return package result with cleanup function
|
|
241
|
+
return {
|
|
242
|
+
bundleZipPath,
|
|
243
|
+
sourceZipPath,
|
|
244
|
+
manifestPath,
|
|
245
|
+
schemaPath,
|
|
246
|
+
secretsPath,
|
|
247
|
+
assetsZipPath,
|
|
248
|
+
assetManifest,
|
|
249
|
+
cleanup: async () => {
|
|
250
|
+
await rm(packageDir, { recursive: true, force: true });
|
|
251
|
+
// Also cleanup build output directory
|
|
252
|
+
await rm(buildOutput.outDir, { recursive: true, force: true });
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
}
|
|
@@ -5,11 +5,16 @@ import {
|
|
|
5
5
|
ListResourcesRequestSchema,
|
|
6
6
|
ReadResourceRequestSchema,
|
|
7
7
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
-
import type { McpServerOptions } from "../types.ts";
|
|
8
|
+
import type { DebugLogger, McpServerOptions } from "../types.ts";
|
|
9
9
|
|
|
10
|
-
export function registerResources(
|
|
10
|
+
export function registerResources(
|
|
11
|
+
server: McpServer,
|
|
12
|
+
options: McpServerOptions,
|
|
13
|
+
debug: DebugLogger,
|
|
14
|
+
) {
|
|
11
15
|
// Register resource list handler
|
|
12
16
|
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
17
|
+
debug("resources/list requested");
|
|
13
18
|
return {
|
|
14
19
|
resources: [
|
|
15
20
|
{
|
|
@@ -26,6 +31,7 @@ export function registerResources(server: McpServer, options: McpServerOptions)
|
|
|
26
31
|
// Register resource read handler
|
|
27
32
|
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
28
33
|
const uri = request.params.uri;
|
|
34
|
+
debug("resources/read requested", { uri });
|
|
29
35
|
|
|
30
36
|
if (uri === "agents://context") {
|
|
31
37
|
const projectPath = options.projectPath ?? process.cwd();
|
package/src/mcp/server.ts
CHANGED
|
@@ -5,7 +5,25 @@ import { registerResources } from "./resources/index.ts";
|
|
|
5
5
|
import { registerTools } from "./tools/index.ts";
|
|
6
6
|
import type { McpServerOptions } from "./types.ts";
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Debug logger that writes to stderr (doesn't interfere with stdio MCP protocol)
|
|
10
|
+
*/
|
|
11
|
+
export function createDebugLogger(enabled: boolean) {
|
|
12
|
+
return (message: string, data?: unknown) => {
|
|
13
|
+
if (!enabled) return;
|
|
14
|
+
const timestamp = new Date().toISOString();
|
|
15
|
+
const line = data
|
|
16
|
+
? `[jack-mcp ${timestamp}] ${message}: ${JSON.stringify(data)}`
|
|
17
|
+
: `[jack-mcp ${timestamp}] ${message}`;
|
|
18
|
+
console.error(line);
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
8
22
|
export async function createMcpServer(options: McpServerOptions = {}) {
|
|
23
|
+
const debug = createDebugLogger(options.debug ?? false);
|
|
24
|
+
|
|
25
|
+
debug("Creating MCP server", { version: pkg.version, options });
|
|
26
|
+
|
|
9
27
|
const server = new McpServer(
|
|
10
28
|
{
|
|
11
29
|
name: "jack",
|
|
@@ -19,14 +37,24 @@ export async function createMcpServer(options: McpServerOptions = {}) {
|
|
|
19
37
|
},
|
|
20
38
|
);
|
|
21
39
|
|
|
22
|
-
registerTools(server, options);
|
|
23
|
-
registerResources(server, options);
|
|
40
|
+
registerTools(server, options, debug);
|
|
41
|
+
registerResources(server, options, debug);
|
|
24
42
|
|
|
25
|
-
return server;
|
|
43
|
+
return { server, debug };
|
|
26
44
|
}
|
|
27
45
|
|
|
28
46
|
export async function startMcpServer(options: McpServerOptions = {}) {
|
|
29
|
-
const server = await createMcpServer(options);
|
|
47
|
+
const { server, debug } = await createMcpServer(options);
|
|
30
48
|
const transport = new StdioServerTransport();
|
|
49
|
+
|
|
50
|
+
debug("Starting MCP server on stdio transport");
|
|
51
|
+
|
|
52
|
+
// Always log startup to stderr so user knows it's running
|
|
53
|
+
console.error(
|
|
54
|
+
`[jack-mcp] Server started (v${pkg.version})${options.debug ? " [debug mode]" : ""}`,
|
|
55
|
+
);
|
|
56
|
+
|
|
31
57
|
await server.connect(transport);
|
|
58
|
+
|
|
59
|
+
debug("MCP server connected and ready");
|
|
32
60
|
}
|