@getjack/jack 0.1.14 → 0.1.16
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 +2 -2
- package/src/commands/clone.ts +8 -7
- package/src/commands/down.ts +12 -5
- package/src/commands/link.ts +13 -8
- package/src/commands/projects.ts +2 -2
- package/src/commands/publish.ts +2 -2
- package/src/commands/secrets.ts +11 -14
- package/src/commands/services.ts +154 -14
- package/src/commands/sync.ts +6 -4
- package/src/commands/update.ts +53 -0
- package/src/index.ts +31 -0
- package/src/lib/auth/login-flow.ts +11 -3
- package/src/lib/build-helper.ts +3 -3
- package/src/lib/control-plane.ts +47 -0
- package/src/lib/hooks.ts +22 -45
- package/src/lib/project-operations.ts +19 -11
- package/src/lib/prompts.ts +23 -21
- package/src/lib/services/db-create.ts +187 -0
- package/src/lib/services/db-list.ts +56 -0
- package/src/lib/version-check.ts +170 -0
- package/src/lib/wrangler.ts +2 -0
- package/src/mcp/resources/index.ts +32 -0
- package/src/mcp/tools/index.ts +131 -1
- package/src/mcp/utils.ts +1 -1
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version checking utilities for self-update functionality
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { $ } from "bun";
|
|
8
|
+
import pkg from "../../package.json";
|
|
9
|
+
import { CONFIG_DIR } from "./config.ts";
|
|
10
|
+
|
|
11
|
+
const VERSION_CACHE_PATH = join(CONFIG_DIR, "version-cache.json");
|
|
12
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
13
|
+
const PACKAGE_NAME = "@getjack/jack";
|
|
14
|
+
const NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
15
|
+
|
|
16
|
+
interface VersionCache {
|
|
17
|
+
latestVersion: string;
|
|
18
|
+
checkedAt: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get the current installed version
|
|
23
|
+
*/
|
|
24
|
+
export function getCurrentVersion(): string {
|
|
25
|
+
return pkg.version;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Fetch the latest version from npm registry
|
|
30
|
+
*/
|
|
31
|
+
async function fetchLatestVersion(): Promise<string | null> {
|
|
32
|
+
try {
|
|
33
|
+
const response = await fetch(NPM_REGISTRY_URL, {
|
|
34
|
+
headers: { Accept: "application/json" },
|
|
35
|
+
signal: AbortSignal.timeout(5000), // 5 second timeout
|
|
36
|
+
});
|
|
37
|
+
if (!response.ok) return null;
|
|
38
|
+
const data = (await response.json()) as { version: string };
|
|
39
|
+
return data.version;
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Read cached version info
|
|
47
|
+
*/
|
|
48
|
+
async function readVersionCache(): Promise<VersionCache | null> {
|
|
49
|
+
if (!existsSync(VERSION_CACHE_PATH)) return null;
|
|
50
|
+
try {
|
|
51
|
+
return await Bun.file(VERSION_CACHE_PATH).json();
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Write version info to cache
|
|
59
|
+
*/
|
|
60
|
+
async function writeVersionCache(cache: VersionCache): Promise<void> {
|
|
61
|
+
try {
|
|
62
|
+
await Bun.write(VERSION_CACHE_PATH, JSON.stringify(cache));
|
|
63
|
+
} catch {
|
|
64
|
+
// Ignore cache write errors
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Compare semver versions (simple comparison, assumes valid semver)
|
|
70
|
+
*/
|
|
71
|
+
function isNewerVersion(latest: string, current: string): boolean {
|
|
72
|
+
const latestParts = latest.split(".").map(Number);
|
|
73
|
+
const currentParts = current.split(".").map(Number);
|
|
74
|
+
|
|
75
|
+
for (let i = 0; i < 3; i++) {
|
|
76
|
+
const l = latestParts[i] ?? 0;
|
|
77
|
+
const c = currentParts[i] ?? 0;
|
|
78
|
+
if (l > c) return true;
|
|
79
|
+
if (l < c) return false;
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Check if an update is available (uses cache, non-blocking)
|
|
86
|
+
* Returns the latest version if newer, null otherwise
|
|
87
|
+
*/
|
|
88
|
+
export async function checkForUpdate(): Promise<string | null> {
|
|
89
|
+
const currentVersion = getCurrentVersion();
|
|
90
|
+
|
|
91
|
+
// Check cache first
|
|
92
|
+
const cache = await readVersionCache();
|
|
93
|
+
const now = Date.now();
|
|
94
|
+
|
|
95
|
+
if (cache && now - cache.checkedAt < CACHE_TTL_MS) {
|
|
96
|
+
// Use cached value
|
|
97
|
+
if (isNewerVersion(cache.latestVersion, currentVersion)) {
|
|
98
|
+
return cache.latestVersion;
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Fetch fresh version (don't await in caller for non-blocking)
|
|
104
|
+
const latestVersion = await fetchLatestVersion();
|
|
105
|
+
if (!latestVersion) return null;
|
|
106
|
+
|
|
107
|
+
// Update cache
|
|
108
|
+
await writeVersionCache({ latestVersion, checkedAt: now });
|
|
109
|
+
|
|
110
|
+
if (isNewerVersion(latestVersion, currentVersion)) {
|
|
111
|
+
return latestVersion;
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Perform the actual update
|
|
118
|
+
*/
|
|
119
|
+
export async function performUpdate(): Promise<{
|
|
120
|
+
success: boolean;
|
|
121
|
+
version?: string;
|
|
122
|
+
error?: string;
|
|
123
|
+
}> {
|
|
124
|
+
try {
|
|
125
|
+
// Run bun add -g to update
|
|
126
|
+
const result = await $`bun add -g ${PACKAGE_NAME}@latest`.nothrow().quiet();
|
|
127
|
+
|
|
128
|
+
if (result.exitCode !== 0) {
|
|
129
|
+
return {
|
|
130
|
+
success: false,
|
|
131
|
+
error: result.stderr.toString() || "Update failed",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Verify the new version
|
|
136
|
+
const newVersionResult = await $`bun pm ls -g`.nothrow().quiet();
|
|
137
|
+
const output = newVersionResult.stdout.toString();
|
|
138
|
+
|
|
139
|
+
// Try to extract version from output
|
|
140
|
+
const versionMatch = output.match(/@getjack\/jack@(\d+\.\d+\.\d+)/);
|
|
141
|
+
const newVersion = versionMatch?.[1];
|
|
142
|
+
|
|
143
|
+
// Clear version cache so next check gets fresh data
|
|
144
|
+
try {
|
|
145
|
+
await Bun.write(VERSION_CACHE_PATH, "");
|
|
146
|
+
} catch {
|
|
147
|
+
// Ignore
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
success: true,
|
|
152
|
+
version: newVersion,
|
|
153
|
+
};
|
|
154
|
+
} catch (err) {
|
|
155
|
+
return {
|
|
156
|
+
success: false,
|
|
157
|
+
error: err instanceof Error ? err.message : "Unknown error",
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Check if running via bunx (vs global install)
|
|
164
|
+
* bunx runs from a temp cache directory
|
|
165
|
+
*/
|
|
166
|
+
export function isRunningViaBunx(): boolean {
|
|
167
|
+
// bunx runs from ~/.bun/install/cache or similar temp location
|
|
168
|
+
const execPath = process.argv[1] ?? "";
|
|
169
|
+
return execPath.includes(".bun/install/cache") || execPath.includes("/.cache/");
|
|
170
|
+
}
|
package/src/lib/wrangler.ts
CHANGED
|
@@ -32,6 +32,7 @@ export async function ensureWrangler(): Promise<void> {
|
|
|
32
32
|
|
|
33
33
|
if (install.exitCode !== 0) {
|
|
34
34
|
spin.error("Failed to install wrangler");
|
|
35
|
+
error("Try manually: bun add -g wrangler");
|
|
35
36
|
process.exit(1);
|
|
36
37
|
}
|
|
37
38
|
spin.success("Installed wrangler");
|
|
@@ -53,6 +54,7 @@ export async function ensureAuth(): Promise<void> {
|
|
|
53
54
|
|
|
54
55
|
if (login.exitCode !== 0) {
|
|
55
56
|
error("Failed to authenticate with Cloudflare");
|
|
57
|
+
error("Try manually: wrangler login");
|
|
56
58
|
process.exit(1);
|
|
57
59
|
}
|
|
58
60
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
ListResourcesRequestSchema,
|
|
6
6
|
ReadResourceRequestSchema,
|
|
7
7
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
+
import packageJson from "../../../package.json" with { type: "json" };
|
|
8
9
|
import type { DebugLogger, McpServerOptions } from "../types.ts";
|
|
9
10
|
|
|
10
11
|
export function registerResources(
|
|
@@ -24,6 +25,12 @@ export function registerResources(
|
|
|
24
25
|
"Project-specific context files (AGENTS.md, CLAUDE.md) for AI agents working on this project",
|
|
25
26
|
mimeType: "text/markdown",
|
|
26
27
|
},
|
|
28
|
+
{
|
|
29
|
+
uri: "jack://capabilities",
|
|
30
|
+
name: "Jack Capabilities",
|
|
31
|
+
description: "Semantic information about jack's capabilities for AI agents",
|
|
32
|
+
mimeType: "application/json",
|
|
33
|
+
},
|
|
27
34
|
],
|
|
28
35
|
};
|
|
29
36
|
});
|
|
@@ -33,6 +40,31 @@ export function registerResources(
|
|
|
33
40
|
const uri = request.params.uri;
|
|
34
41
|
debug("resources/read requested", { uri });
|
|
35
42
|
|
|
43
|
+
if (uri === "jack://capabilities") {
|
|
44
|
+
const capabilities = {
|
|
45
|
+
version: packageJson.version,
|
|
46
|
+
services: {
|
|
47
|
+
supported: ["d1", "kv", "r2"],
|
|
48
|
+
create_supported: ["d1"],
|
|
49
|
+
},
|
|
50
|
+
guidance: {
|
|
51
|
+
prefer_jack_over_wrangler: true,
|
|
52
|
+
database_creation: "Use create_database tool or jack services db create",
|
|
53
|
+
deployment: "Use deploy_project tool or jack ship",
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
contents: [
|
|
59
|
+
{
|
|
60
|
+
uri,
|
|
61
|
+
mimeType: "application/json",
|
|
62
|
+
text: JSON.stringify(capabilities, null, 2),
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
36
68
|
if (uri === "agents://context") {
|
|
37
69
|
const projectPath = options.projectPath ?? process.cwd();
|
|
38
70
|
const agentsPath = join(projectPath, "AGENTS.md");
|
package/src/mcp/tools/index.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { z } from "zod";
|
|
|
4
4
|
import { JackError, JackErrorCode } from "../../lib/errors.ts";
|
|
5
5
|
import { createProject, deployProject, getProjectStatus } from "../../lib/project-operations.ts";
|
|
6
6
|
import { listAllProjects } from "../../lib/project-resolver.ts";
|
|
7
|
+
import { createDatabase } from "../../lib/services/db-create.ts";
|
|
8
|
+
import { listDatabases } from "../../lib/services/db-list.ts";
|
|
7
9
|
import { Events, track, withTelemetry } from "../../lib/telemetry.ts";
|
|
8
10
|
import type { DebugLogger, McpServerOptions } from "../types.ts";
|
|
9
11
|
import { formatErrorResponse, formatSuccessResponse } from "../utils.ts";
|
|
@@ -36,6 +38,21 @@ const ListProjectsSchema = z.object({
|
|
|
36
38
|
.describe("Filter projects by status (defaults to 'all')"),
|
|
37
39
|
});
|
|
38
40
|
|
|
41
|
+
const CreateDatabaseSchema = z.object({
|
|
42
|
+
name: z.string().optional().describe("Database name (auto-generated if not provided)"),
|
|
43
|
+
project_path: z
|
|
44
|
+
.string()
|
|
45
|
+
.optional()
|
|
46
|
+
.describe("Path to project directory (defaults to current directory)"),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const ListDatabasesSchema = z.object({
|
|
50
|
+
project_path: z
|
|
51
|
+
.string()
|
|
52
|
+
.optional()
|
|
53
|
+
.describe("Path to project directory (defaults to current directory)"),
|
|
54
|
+
});
|
|
55
|
+
|
|
39
56
|
export function registerTools(server: McpServer, _options: McpServerOptions, debug: DebugLogger) {
|
|
40
57
|
// Register tool list handler
|
|
41
58
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
@@ -107,6 +124,37 @@ export function registerTools(server: McpServer, _options: McpServerOptions, deb
|
|
|
107
124
|
},
|
|
108
125
|
},
|
|
109
126
|
},
|
|
127
|
+
{
|
|
128
|
+
name: "create_database",
|
|
129
|
+
description:
|
|
130
|
+
"Create a D1 database for a project. Returns deploy_required=true since binding needs deploy to activate.",
|
|
131
|
+
inputSchema: {
|
|
132
|
+
type: "object",
|
|
133
|
+
properties: {
|
|
134
|
+
name: {
|
|
135
|
+
type: "string",
|
|
136
|
+
description: "Database name (auto-generated if not provided)",
|
|
137
|
+
},
|
|
138
|
+
project_path: {
|
|
139
|
+
type: "string",
|
|
140
|
+
description: "Path to project directory (defaults to current directory)",
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: "list_databases",
|
|
147
|
+
description: "List all D1 databases for a project.",
|
|
148
|
+
inputSchema: {
|
|
149
|
+
type: "object",
|
|
150
|
+
properties: {
|
|
151
|
+
project_path: {
|
|
152
|
+
type: "string",
|
|
153
|
+
description: "Path to project directory (defaults to current directory)",
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
},
|
|
110
158
|
],
|
|
111
159
|
};
|
|
112
160
|
});
|
|
@@ -195,7 +243,15 @@ export function registerTools(server: McpServer, _options: McpServerOptions, deb
|
|
|
195
243
|
const wrappedGetProjectStatus = withTelemetry(
|
|
196
244
|
"get_project_status",
|
|
197
245
|
async (name?: string, projectPath?: string) => {
|
|
198
|
-
|
|
246
|
+
const status = await getProjectStatus(name, projectPath);
|
|
247
|
+
if (status === null) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
// Add available_services to tell agents what services can be created
|
|
251
|
+
return {
|
|
252
|
+
...status,
|
|
253
|
+
available_services: ["d1", "kv", "r2"],
|
|
254
|
+
};
|
|
199
255
|
},
|
|
200
256
|
{ platform: "mcp" },
|
|
201
257
|
);
|
|
@@ -259,6 +315,80 @@ export function registerTools(server: McpServer, _options: McpServerOptions, deb
|
|
|
259
315
|
};
|
|
260
316
|
}
|
|
261
317
|
|
|
318
|
+
case "create_database": {
|
|
319
|
+
const args = CreateDatabaseSchema.parse(request.params.arguments ?? {});
|
|
320
|
+
const projectPath = args.project_path ?? process.cwd();
|
|
321
|
+
|
|
322
|
+
const wrappedCreateDatabase = withTelemetry(
|
|
323
|
+
"create_database",
|
|
324
|
+
async (projectDir: string, name?: string) => {
|
|
325
|
+
const result = await createDatabase(projectDir, {
|
|
326
|
+
name,
|
|
327
|
+
interactive: false,
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// Track business event
|
|
331
|
+
track(Events.SERVICE_CREATED, {
|
|
332
|
+
service_type: "d1",
|
|
333
|
+
platform: "mcp",
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
database_name: result.databaseName,
|
|
338
|
+
database_id: result.databaseId,
|
|
339
|
+
binding_name: result.bindingName,
|
|
340
|
+
created: result.created,
|
|
341
|
+
deploy_required: true,
|
|
342
|
+
};
|
|
343
|
+
},
|
|
344
|
+
{ platform: "mcp" },
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
const result = await wrappedCreateDatabase(projectPath, args.name);
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
content: [
|
|
351
|
+
{
|
|
352
|
+
type: "text",
|
|
353
|
+
text: JSON.stringify(formatSuccessResponse(result, startTime), null, 2),
|
|
354
|
+
},
|
|
355
|
+
],
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
case "list_databases": {
|
|
360
|
+
const args = ListDatabasesSchema.parse(request.params.arguments ?? {});
|
|
361
|
+
const projectPath = args.project_path ?? process.cwd();
|
|
362
|
+
|
|
363
|
+
const wrappedListDatabases = withTelemetry(
|
|
364
|
+
"list_databases",
|
|
365
|
+
async (projectDir: string) => {
|
|
366
|
+
const databases = await listDatabases(projectDir);
|
|
367
|
+
return {
|
|
368
|
+
databases: databases.map((db) => ({
|
|
369
|
+
name: db.name,
|
|
370
|
+
binding: db.binding,
|
|
371
|
+
id: db.id,
|
|
372
|
+
size_bytes: db.sizeBytes,
|
|
373
|
+
num_tables: db.numTables,
|
|
374
|
+
})),
|
|
375
|
+
};
|
|
376
|
+
},
|
|
377
|
+
{ platform: "mcp" },
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
const result = await wrappedListDatabases(projectPath);
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
content: [
|
|
384
|
+
{
|
|
385
|
+
type: "text",
|
|
386
|
+
text: JSON.stringify(formatSuccessResponse(result, startTime), null, 2),
|
|
387
|
+
},
|
|
388
|
+
],
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
262
392
|
default:
|
|
263
393
|
throw new Error(`Unknown tool: ${toolName}`);
|
|
264
394
|
}
|
package/src/mcp/utils.ts
CHANGED
|
@@ -142,6 +142,6 @@ export function getSuggestionForError(code: McpErrorCode): string {
|
|
|
142
142
|
return "Review the error message and ensure all required fields are provided correctly.";
|
|
143
143
|
|
|
144
144
|
default:
|
|
145
|
-
return "
|
|
145
|
+
return "Check the error message above for details. If the problem persists, try running the equivalent CLI command directly.";
|
|
146
146
|
}
|
|
147
147
|
}
|