@getjack/jack 0.1.13 → 0.1.15
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 -6
- package/src/commands/clone.ts +6 -6
- package/src/commands/down.ts +21 -3
- package/src/commands/link.ts +8 -3
- package/src/commands/secrets.ts +11 -14
- package/src/commands/services.ts +148 -5
- 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/control-plane.ts +47 -0
- package/src/lib/deploy-mode.ts +23 -2
- package/src/lib/deploy-upload.ts +9 -0
- package/src/lib/hooks.ts +22 -45
- package/src/lib/project-operations.ts +401 -313
- package/src/lib/project-resolver.ts +12 -0
- 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/mcp/resources/index.ts +32 -0
- package/src/mcp/tools/index.ts +131 -1
- package/templates/miniapp/.jack.json +5 -5
- package/templates/miniapp/public/.well-known/farcaster.json +1 -1
- package/templates/miniapp/src/lib/api.ts +71 -3
- package/templates/miniapp/src/worker.ts +44 -0
|
@@ -373,6 +373,18 @@ export async function listAllProjects(): Promise<ResolvedProject[]> {
|
|
|
373
373
|
projectMap.set(managed.id, resolved);
|
|
374
374
|
}
|
|
375
375
|
}
|
|
376
|
+
|
|
377
|
+
// Mark orphaned local managed projects (not found on control plane) as errors
|
|
378
|
+
for (const [, project] of projectMap) {
|
|
379
|
+
if (
|
|
380
|
+
project.deployMode === "managed" &&
|
|
381
|
+
project.status === "syncing" &&
|
|
382
|
+
!project.sources.controlPlane
|
|
383
|
+
) {
|
|
384
|
+
project.status = "error";
|
|
385
|
+
project.errorMessage = "Project not found in jack cloud. Run: jack unlink && jack ship";
|
|
386
|
+
}
|
|
387
|
+
}
|
|
376
388
|
} catch {
|
|
377
389
|
// Control plane unavailable, use local-only data
|
|
378
390
|
}
|
package/src/lib/prompts.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isCancel, select, text } from "@clack/prompts";
|
|
2
2
|
import type { DetectedSecret } from "./env-parser.ts";
|
|
3
3
|
import { info, success, warn } from "./output.ts";
|
|
4
4
|
import { getSavedSecrets, getSecretsPath, maskSecret, saveSecrets } from "./secrets.ts";
|
|
@@ -25,14 +25,14 @@ export async function promptSaveSecrets(detected: DetectedSecret[]): Promise<voi
|
|
|
25
25
|
|
|
26
26
|
const action = await select({
|
|
27
27
|
message: "What would you like to do?",
|
|
28
|
-
|
|
29
|
-
{ value: "save",
|
|
30
|
-
{ value: "paste",
|
|
31
|
-
{ value: "skip",
|
|
28
|
+
options: [
|
|
29
|
+
{ value: "save", label: "Save to jack for future projects" },
|
|
30
|
+
{ value: "paste", label: "Paste additional secrets" },
|
|
31
|
+
{ value: "skip", label: "Skip for now" },
|
|
32
32
|
],
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
-
if (action === "skip") {
|
|
35
|
+
if (isCancel(action) || action === "skip") {
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -65,18 +65,22 @@ async function promptAdditionalSecrets(): Promise<
|
|
|
65
65
|
const secrets: Array<{ key: string; value: string; source: string }> = [];
|
|
66
66
|
|
|
67
67
|
while (true) {
|
|
68
|
-
const key = await
|
|
68
|
+
const key = await text({
|
|
69
69
|
message: "Enter secret name (or press enter to finish):",
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
-
if (!key.trim()) {
|
|
72
|
+
if (isCancel(key) || !key.trim()) {
|
|
73
73
|
break;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
const value = await
|
|
76
|
+
const value = await text({
|
|
77
77
|
message: `Enter value for ${key}:`,
|
|
78
78
|
});
|
|
79
79
|
|
|
80
|
+
if (isCancel(value)) {
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
|
|
80
84
|
if (value.trim()) {
|
|
81
85
|
secrets.push({ key: key.trim(), value: value.trim(), source: "manual" });
|
|
82
86
|
}
|
|
@@ -107,20 +111,19 @@ export async function promptUseSecrets(): Promise<Record<string, string> | null>
|
|
|
107
111
|
}
|
|
108
112
|
console.error("");
|
|
109
113
|
|
|
110
|
-
console.error(" Esc to skip\n");
|
|
111
114
|
const action = await select({
|
|
112
115
|
message: "Use them for this project?",
|
|
113
|
-
|
|
114
|
-
{
|
|
115
|
-
{
|
|
116
|
+
options: [
|
|
117
|
+
{ label: "Yes", value: "yes" },
|
|
118
|
+
{ label: "No", value: "no" },
|
|
116
119
|
],
|
|
117
120
|
});
|
|
118
121
|
|
|
119
|
-
if (action
|
|
120
|
-
return
|
|
122
|
+
if (isCancel(action) || action !== "yes") {
|
|
123
|
+
return null;
|
|
121
124
|
}
|
|
122
125
|
|
|
123
|
-
return
|
|
126
|
+
return saved;
|
|
124
127
|
}
|
|
125
128
|
|
|
126
129
|
/**
|
|
@@ -151,14 +154,13 @@ export async function promptUseSecretsFromList(
|
|
|
151
154
|
return false;
|
|
152
155
|
}
|
|
153
156
|
|
|
154
|
-
console.error(" Esc to skip\n");
|
|
155
157
|
const action = await select({
|
|
156
158
|
message: "Use saved secrets for this project?",
|
|
157
|
-
|
|
158
|
-
{
|
|
159
|
-
{
|
|
159
|
+
options: [
|
|
160
|
+
{ label: "Yes", value: "yes" },
|
|
161
|
+
{ label: "No", value: "no" },
|
|
160
162
|
],
|
|
161
163
|
});
|
|
162
164
|
|
|
163
|
-
return action === "yes";
|
|
165
|
+
return !isCancel(action) && action === "yes";
|
|
164
166
|
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database creation logic for jack services db create
|
|
3
|
+
*
|
|
4
|
+
* Handles both managed (control plane) and BYO (wrangler d1 create) modes.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { $ } from "bun";
|
|
9
|
+
import { createProjectResource } from "../control-plane.ts";
|
|
10
|
+
import { readProjectLink } from "../project-link.ts";
|
|
11
|
+
import { getProjectNameFromDir } from "../storage/index.ts";
|
|
12
|
+
import { addD1Binding, getExistingD1Bindings } from "../wrangler-config.ts";
|
|
13
|
+
|
|
14
|
+
export interface CreateDatabaseOptions {
|
|
15
|
+
name?: string;
|
|
16
|
+
interactive?: boolean; // Whether to prompt for deploy
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface CreateDatabaseResult {
|
|
20
|
+
databaseName: string;
|
|
21
|
+
databaseId: string;
|
|
22
|
+
bindingName: string;
|
|
23
|
+
created: boolean; // false if reused existing
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Convert a database name to SCREAMING_SNAKE_CASE for the binding name.
|
|
28
|
+
* Special case: first database in a project gets "DB" as the binding.
|
|
29
|
+
*/
|
|
30
|
+
function toBindingName(dbName: string, isFirst: boolean): string {
|
|
31
|
+
if (isFirst) {
|
|
32
|
+
return "DB";
|
|
33
|
+
}
|
|
34
|
+
// Convert kebab-case/snake_case to SCREAMING_SNAKE_CASE
|
|
35
|
+
return dbName
|
|
36
|
+
.replace(/-/g, "_")
|
|
37
|
+
.replace(/[^a-zA-Z0-9_]/g, "")
|
|
38
|
+
.toUpperCase();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Generate a unique database name for a project.
|
|
43
|
+
* First DB: {project}-db
|
|
44
|
+
* Subsequent DBs: {project}-db-{n}
|
|
45
|
+
*/
|
|
46
|
+
function generateDatabaseName(projectName: string, existingCount: number): string {
|
|
47
|
+
if (existingCount === 0) {
|
|
48
|
+
return `${projectName}-db`;
|
|
49
|
+
}
|
|
50
|
+
return `${projectName}-db-${existingCount + 1}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface ExistingDatabase {
|
|
54
|
+
uuid: string;
|
|
55
|
+
name: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* List all D1 databases in the Cloudflare account via wrangler
|
|
60
|
+
*/
|
|
61
|
+
async function listDatabasesViaWrangler(): Promise<ExistingDatabase[]> {
|
|
62
|
+
const result = await $`wrangler d1 list --json`.nothrow().quiet();
|
|
63
|
+
|
|
64
|
+
if (result.exitCode !== 0) {
|
|
65
|
+
// If wrangler fails, return empty list (might not be logged in)
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const output = result.stdout.toString().trim();
|
|
71
|
+
const data = JSON.parse(output);
|
|
72
|
+
// wrangler d1 list --json returns array: [{ "uuid": "...", "name": "...", ... }]
|
|
73
|
+
return Array.isArray(data) ? data : [];
|
|
74
|
+
} catch {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Find an existing D1 database by name
|
|
81
|
+
*/
|
|
82
|
+
async function findExistingDatabase(dbName: string): Promise<ExistingDatabase | null> {
|
|
83
|
+
const databases = await listDatabasesViaWrangler();
|
|
84
|
+
return databases.find((db) => db.name === dbName) ?? null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Create a D1 database via wrangler (for BYO mode)
|
|
89
|
+
*/
|
|
90
|
+
async function createDatabaseViaWrangler(
|
|
91
|
+
dbName: string,
|
|
92
|
+
): Promise<{ id: string; created: boolean }> {
|
|
93
|
+
// Check if database already exists
|
|
94
|
+
const existing = await findExistingDatabase(dbName);
|
|
95
|
+
if (existing) {
|
|
96
|
+
return { id: existing.uuid, created: false };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const result = await $`wrangler d1 create ${dbName} --json`.nothrow().quiet();
|
|
100
|
+
|
|
101
|
+
if (result.exitCode !== 0) {
|
|
102
|
+
const stderr = result.stderr.toString().trim();
|
|
103
|
+
throw new Error(stderr || `Failed to create database ${dbName}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const output = result.stdout.toString().trim();
|
|
108
|
+
const data = JSON.parse(output);
|
|
109
|
+
// wrangler d1 create --json returns: { "uuid": "...", "name": "..." }
|
|
110
|
+
return { id: data.uuid || "", created: true };
|
|
111
|
+
} catch {
|
|
112
|
+
throw new Error("Failed to parse wrangler d1 create output");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Create a D1 database for the current project.
|
|
118
|
+
*
|
|
119
|
+
* For managed projects: calls control plane POST /v1/projects/:id/resources/d1
|
|
120
|
+
* For BYO projects: uses wrangler d1 create
|
|
121
|
+
*
|
|
122
|
+
* In both cases, updates wrangler.jsonc with the new binding.
|
|
123
|
+
*/
|
|
124
|
+
export async function createDatabase(
|
|
125
|
+
projectDir: string,
|
|
126
|
+
options: CreateDatabaseOptions = {},
|
|
127
|
+
): Promise<CreateDatabaseResult> {
|
|
128
|
+
// Read project link to determine deploy mode
|
|
129
|
+
const link = await readProjectLink(projectDir);
|
|
130
|
+
if (!link) {
|
|
131
|
+
throw new Error("Not in a jack project. Run 'jack new' to create a project.");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Get project name from wrangler config
|
|
135
|
+
const projectName = await getProjectNameFromDir(projectDir);
|
|
136
|
+
|
|
137
|
+
// Get existing D1 bindings to determine naming
|
|
138
|
+
const wranglerPath = join(projectDir, "wrangler.jsonc");
|
|
139
|
+
const existingBindings = await getExistingD1Bindings(wranglerPath);
|
|
140
|
+
const existingCount = existingBindings.length;
|
|
141
|
+
|
|
142
|
+
// Determine database name
|
|
143
|
+
const databaseName = options.name ?? generateDatabaseName(projectName, existingCount);
|
|
144
|
+
|
|
145
|
+
// Determine binding name
|
|
146
|
+
const isFirst = existingCount === 0;
|
|
147
|
+
const bindingName = toBindingName(databaseName, isFirst);
|
|
148
|
+
|
|
149
|
+
// Check if binding name already exists
|
|
150
|
+
const bindingExists = existingBindings.some((b) => b.binding === bindingName);
|
|
151
|
+
if (bindingExists) {
|
|
152
|
+
throw new Error(`Binding "${bindingName}" already exists. Choose a different database name.`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let databaseId: string;
|
|
156
|
+
let created = true;
|
|
157
|
+
|
|
158
|
+
if (link.deploy_mode === "managed") {
|
|
159
|
+
// Managed mode: call control plane
|
|
160
|
+
// Note: Control plane will reuse existing DB if name matches
|
|
161
|
+
const resource = await createProjectResource(link.project_id, "d1", {
|
|
162
|
+
name: databaseName,
|
|
163
|
+
bindingName,
|
|
164
|
+
});
|
|
165
|
+
databaseId = resource.provider_id;
|
|
166
|
+
// Control plane always creates for now; could add reuse logic there too
|
|
167
|
+
} else {
|
|
168
|
+
// BYO mode: use wrangler d1 create (checks for existing first)
|
|
169
|
+
const result = await createDatabaseViaWrangler(databaseName);
|
|
170
|
+
databaseId = result.id;
|
|
171
|
+
created = result.created;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Update wrangler.jsonc with the new binding
|
|
175
|
+
await addD1Binding(wranglerPath, {
|
|
176
|
+
binding: bindingName,
|
|
177
|
+
database_name: databaseName,
|
|
178
|
+
database_id: databaseId,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
databaseName,
|
|
183
|
+
databaseId,
|
|
184
|
+
bindingName,
|
|
185
|
+
created,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database listing logic for jack services db list
|
|
3
|
+
*
|
|
4
|
+
* Lists D1 databases configured in wrangler.jsonc with their metadata.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { getExistingD1Bindings } from "../wrangler-config.ts";
|
|
9
|
+
import { getDatabaseInfo } from "./db.ts";
|
|
10
|
+
|
|
11
|
+
export interface DatabaseListEntry {
|
|
12
|
+
name: string;
|
|
13
|
+
binding: string;
|
|
14
|
+
id: string;
|
|
15
|
+
sizeBytes?: number;
|
|
16
|
+
numTables?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* List all D1 databases configured for a project.
|
|
21
|
+
*
|
|
22
|
+
* Reads bindings from wrangler.jsonc and fetches additional metadata
|
|
23
|
+
* (size, table count) via wrangler d1 info for each database.
|
|
24
|
+
*/
|
|
25
|
+
export async function listDatabases(projectDir: string): Promise<DatabaseListEntry[]> {
|
|
26
|
+
const wranglerPath = join(projectDir, "wrangler.jsonc");
|
|
27
|
+
|
|
28
|
+
// Get existing D1 bindings from wrangler.jsonc
|
|
29
|
+
const bindings = await getExistingD1Bindings(wranglerPath);
|
|
30
|
+
|
|
31
|
+
if (bindings.length === 0) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Fetch detailed info for each database
|
|
36
|
+
const entries: DatabaseListEntry[] = [];
|
|
37
|
+
|
|
38
|
+
for (const binding of bindings) {
|
|
39
|
+
const entry: DatabaseListEntry = {
|
|
40
|
+
name: binding.database_name,
|
|
41
|
+
binding: binding.binding,
|
|
42
|
+
id: binding.database_id,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Try to get additional metadata via wrangler
|
|
46
|
+
const info = await getDatabaseInfo(binding.database_name);
|
|
47
|
+
if (info) {
|
|
48
|
+
entry.sizeBytes = info.sizeBytes;
|
|
49
|
+
entry.numTables = info.numTables;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
entries.push(entry);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return entries;
|
|
56
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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");
|