@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,476 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Resolver - unified project discovery
|
|
3
|
+
*
|
|
4
|
+
* Users see "projects". We handle the plumbing.
|
|
5
|
+
*
|
|
6
|
+
* Resolution strategy:
|
|
7
|
+
* 1. Check local registry (fast cache)
|
|
8
|
+
* 2. Check control plane (if logged in)
|
|
9
|
+
* 3. Update registry cache with remote data
|
|
10
|
+
*
|
|
11
|
+
* Control plane is authoritative for managed projects.
|
|
12
|
+
* Registry is a cache that can be rebuilt.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { isLoggedIn } from "./auth/index.ts";
|
|
16
|
+
import {
|
|
17
|
+
type ManagedProject,
|
|
18
|
+
fetchProjectResources,
|
|
19
|
+
findProjectBySlug,
|
|
20
|
+
listManagedProjects,
|
|
21
|
+
} from "./control-plane.ts";
|
|
22
|
+
import { getAllLocalPaths } from "./local-paths.ts";
|
|
23
|
+
import {
|
|
24
|
+
type Project as RegistryProject,
|
|
25
|
+
getAllProjects,
|
|
26
|
+
getProject,
|
|
27
|
+
registerProject,
|
|
28
|
+
removeProject as removeFromRegistry,
|
|
29
|
+
} from "./registry.ts";
|
|
30
|
+
import {
|
|
31
|
+
type ResolvedResources,
|
|
32
|
+
convertControlPlaneResources,
|
|
33
|
+
parseWranglerResources,
|
|
34
|
+
} from "./resources.ts";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* User-facing project status
|
|
38
|
+
*/
|
|
39
|
+
export type ProjectStatus = "live" | "local-only" | "error" | "syncing";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Unified project representation
|
|
43
|
+
*/
|
|
44
|
+
export interface ResolvedProject {
|
|
45
|
+
name: string;
|
|
46
|
+
slug: string;
|
|
47
|
+
|
|
48
|
+
// User-facing status
|
|
49
|
+
status: ProjectStatus;
|
|
50
|
+
url?: string;
|
|
51
|
+
errorMessage?: string;
|
|
52
|
+
|
|
53
|
+
// Where we found it (internal, not shown to user)
|
|
54
|
+
sources: {
|
|
55
|
+
registry: boolean;
|
|
56
|
+
controlPlane: boolean;
|
|
57
|
+
filesystem: boolean;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Location details
|
|
61
|
+
localPath?: string;
|
|
62
|
+
remote?: {
|
|
63
|
+
projectId: string;
|
|
64
|
+
orgId: string;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Metadata
|
|
68
|
+
createdAt: string;
|
|
69
|
+
updatedAt?: string;
|
|
70
|
+
|
|
71
|
+
// Resources (fetched on-demand)
|
|
72
|
+
resources?: ResolvedResources;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Convert registry project to resolved project
|
|
77
|
+
*/
|
|
78
|
+
function fromRegistryProject(name: string, project: RegistryProject): ResolvedProject {
|
|
79
|
+
// Determine status from registry data
|
|
80
|
+
let status: ProjectStatus = "local-only";
|
|
81
|
+
if (project.status === "live") {
|
|
82
|
+
status = "live";
|
|
83
|
+
} else if (project.status === "build_failed") {
|
|
84
|
+
status = "error";
|
|
85
|
+
} else if (project.lastDeployed || project.remote) {
|
|
86
|
+
// If we have a deployment or remote metadata, assume it's live
|
|
87
|
+
status = "live";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
name,
|
|
92
|
+
slug: project.remote?.project_slug || name,
|
|
93
|
+
status,
|
|
94
|
+
url: project.workerUrl || project.remote?.runjack_url || undefined,
|
|
95
|
+
sources: {
|
|
96
|
+
registry: true,
|
|
97
|
+
controlPlane: false,
|
|
98
|
+
filesystem: false, // localPath removed from registry - filesystem detection done elsewhere
|
|
99
|
+
},
|
|
100
|
+
localPath: undefined, // localPath removed from registry
|
|
101
|
+
remote: project.remote
|
|
102
|
+
? {
|
|
103
|
+
projectId: project.remote.project_id,
|
|
104
|
+
orgId: project.remote.org_id,
|
|
105
|
+
}
|
|
106
|
+
: undefined,
|
|
107
|
+
createdAt: project.createdAt,
|
|
108
|
+
updatedAt: project.lastDeployed || undefined,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Convert managed project to resolved project
|
|
114
|
+
*/
|
|
115
|
+
function fromManagedProject(managed: ManagedProject): ResolvedProject {
|
|
116
|
+
const status: ProjectStatus = managed.status === "active" ? "live" : "error";
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
name: managed.name,
|
|
120
|
+
slug: managed.slug,
|
|
121
|
+
status,
|
|
122
|
+
url: `https://${managed.slug}.runjack.xyz`,
|
|
123
|
+
errorMessage: managed.status === "error" ? "deployment failed" : undefined,
|
|
124
|
+
sources: {
|
|
125
|
+
registry: false,
|
|
126
|
+
controlPlane: true,
|
|
127
|
+
filesystem: false,
|
|
128
|
+
},
|
|
129
|
+
remote: {
|
|
130
|
+
projectId: managed.id,
|
|
131
|
+
orgId: managed.org_id,
|
|
132
|
+
},
|
|
133
|
+
createdAt: managed.created_at,
|
|
134
|
+
updatedAt: managed.updated_at,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Merge registry and managed project data
|
|
140
|
+
*/
|
|
141
|
+
function mergeProjects(registry: ResolvedProject, managed: ResolvedProject): ResolvedProject {
|
|
142
|
+
return {
|
|
143
|
+
...registry,
|
|
144
|
+
status: managed.status, // Control plane is authoritative for status
|
|
145
|
+
url: managed.url || registry.url,
|
|
146
|
+
errorMessage: managed.errorMessage,
|
|
147
|
+
sources: {
|
|
148
|
+
registry: true,
|
|
149
|
+
controlPlane: true,
|
|
150
|
+
filesystem: registry.sources.filesystem,
|
|
151
|
+
},
|
|
152
|
+
remote: managed.remote,
|
|
153
|
+
updatedAt: managed.updatedAt || registry.updatedAt,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Options for resolving a project
|
|
159
|
+
*/
|
|
160
|
+
export interface ResolveProjectOptions {
|
|
161
|
+
/** Include resources in the resolved project (fetched on-demand) */
|
|
162
|
+
includeResources?: boolean;
|
|
163
|
+
/** Project path for BYO projects (defaults to cwd) */
|
|
164
|
+
projectPath?: string;
|
|
165
|
+
/** Allow fallback lookup by managed project name when slug lookup fails */
|
|
166
|
+
matchByName?: boolean;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
interface RegistryIndex {
|
|
170
|
+
byRemoteId: Map<string, string>;
|
|
171
|
+
byRemoteSlug: Map<string, string>;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function buildRegistryIndexes(registryProjects: Record<string, RegistryProject>): RegistryIndex {
|
|
175
|
+
const byRemoteId = new Map<string, string>();
|
|
176
|
+
const byRemoteSlug = new Map<string, string>();
|
|
177
|
+
|
|
178
|
+
for (const [name, project] of Object.entries(registryProjects)) {
|
|
179
|
+
const remote = project.remote;
|
|
180
|
+
if (!remote) continue;
|
|
181
|
+
if (remote.project_id) {
|
|
182
|
+
byRemoteId.set(remote.project_id, name);
|
|
183
|
+
}
|
|
184
|
+
if (remote.project_slug) {
|
|
185
|
+
byRemoteSlug.set(remote.project_slug, name);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return { byRemoteId, byRemoteSlug };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Resolve project resources based on deploy mode.
|
|
194
|
+
* For managed: fetch from control plane
|
|
195
|
+
* For BYO: parse from wrangler.jsonc
|
|
196
|
+
*/
|
|
197
|
+
export async function resolveProjectResources(
|
|
198
|
+
project: ResolvedProject,
|
|
199
|
+
projectPath?: string,
|
|
200
|
+
): Promise<ResolvedResources | null> {
|
|
201
|
+
// Managed: fetch from control plane
|
|
202
|
+
if (project.remote?.projectId) {
|
|
203
|
+
try {
|
|
204
|
+
const resources = await fetchProjectResources(project.remote.projectId);
|
|
205
|
+
// Cast ProjectResource[] to ControlPlaneResource[] (compatible shapes)
|
|
206
|
+
return convertControlPlaneResources(
|
|
207
|
+
resources as Parameters<typeof convertControlPlaneResources>[0],
|
|
208
|
+
);
|
|
209
|
+
} catch {
|
|
210
|
+
// Network error, return null
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// BYO: parse from wrangler config
|
|
216
|
+
const path = projectPath || process.cwd();
|
|
217
|
+
try {
|
|
218
|
+
return await parseWranglerResources(path);
|
|
219
|
+
} catch {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Resolve a project by name/slug
|
|
226
|
+
* Checks: registry → control plane → filesystem
|
|
227
|
+
* Caches result in registry
|
|
228
|
+
*/
|
|
229
|
+
export async function resolveProject(
|
|
230
|
+
name: string,
|
|
231
|
+
options?: ResolveProjectOptions,
|
|
232
|
+
): Promise<ResolvedProject | null> {
|
|
233
|
+
let resolved: ResolvedProject | null = null;
|
|
234
|
+
const matchByName = options?.matchByName !== false;
|
|
235
|
+
let registryName = name;
|
|
236
|
+
let registryProjects: Record<string, RegistryProject> | null = null;
|
|
237
|
+
let registryIndex: RegistryIndex | null = null;
|
|
238
|
+
|
|
239
|
+
// Check registry first (fast)
|
|
240
|
+
let registryProject = await getProject(name);
|
|
241
|
+
if (!registryProject) {
|
|
242
|
+
registryProjects = await getAllProjects();
|
|
243
|
+
registryIndex = buildRegistryIndexes(registryProjects);
|
|
244
|
+
const slugMatchName = registryIndex.byRemoteSlug.get(name);
|
|
245
|
+
if (slugMatchName) {
|
|
246
|
+
registryProject = registryProjects[slugMatchName] ?? null;
|
|
247
|
+
registryName = slugMatchName;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (registryProject) {
|
|
251
|
+
resolved = fromRegistryProject(registryName, registryProject);
|
|
252
|
+
|
|
253
|
+
// If it's a BYOC project, don't check control plane
|
|
254
|
+
if (registryProject.deploy_mode === "byo") {
|
|
255
|
+
// resolved stays as-is
|
|
256
|
+
} else if (registryProject.deploy_mode === "managed" && (await isLoggedIn())) {
|
|
257
|
+
// If it's managed, try to get latest status from control plane
|
|
258
|
+
try {
|
|
259
|
+
const managed = await findProjectBySlug(resolved.slug);
|
|
260
|
+
if (managed) {
|
|
261
|
+
resolved = mergeProjects(resolved, fromManagedProject(managed));
|
|
262
|
+
|
|
263
|
+
// Update registry cache with latest status
|
|
264
|
+
await registerProject(registryName, {
|
|
265
|
+
...registryProject,
|
|
266
|
+
status: managed.status === "active" ? "live" : "build_failed",
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
} catch {
|
|
270
|
+
// Control plane unavailable, use cached data
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
} else if (await isLoggedIn()) {
|
|
274
|
+
// Not in registry, check control plane if logged in
|
|
275
|
+
try {
|
|
276
|
+
let managed = await findProjectBySlug(name);
|
|
277
|
+
if (!managed && matchByName) {
|
|
278
|
+
const managedProjects = await listManagedProjects();
|
|
279
|
+
managed = managedProjects.find((project) => project.name === name) ?? null;
|
|
280
|
+
}
|
|
281
|
+
if (managed) {
|
|
282
|
+
if (!registryProjects || !registryIndex) {
|
|
283
|
+
registryProjects = await getAllProjects();
|
|
284
|
+
registryIndex = buildRegistryIndexes(registryProjects);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const existingRegistryName =
|
|
288
|
+
registryIndex.byRemoteId.get(managed.id) ?? registryIndex.byRemoteSlug.get(managed.slug);
|
|
289
|
+
|
|
290
|
+
if (existingRegistryName && registryProjects[existingRegistryName]) {
|
|
291
|
+
const existingProject = registryProjects[existingRegistryName];
|
|
292
|
+
resolved = mergeProjects(
|
|
293
|
+
fromRegistryProject(existingRegistryName, existingProject),
|
|
294
|
+
fromManagedProject(managed),
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
// Update registry cache with latest status
|
|
298
|
+
await registerProject(existingRegistryName, {
|
|
299
|
+
...existingProject,
|
|
300
|
+
status: managed.status === "active" ? "live" : "build_failed",
|
|
301
|
+
});
|
|
302
|
+
} else {
|
|
303
|
+
resolved = fromManagedProject(managed);
|
|
304
|
+
|
|
305
|
+
// Cache in registry for future lookups
|
|
306
|
+
await registerProject(managed.slug, {
|
|
307
|
+
workerUrl: resolved.url || null,
|
|
308
|
+
createdAt: managed.created_at,
|
|
309
|
+
lastDeployed: managed.updated_at,
|
|
310
|
+
status: managed.status === "active" ? "live" : "build_failed",
|
|
311
|
+
deploy_mode: "managed",
|
|
312
|
+
remote: {
|
|
313
|
+
project_id: managed.id,
|
|
314
|
+
project_slug: managed.slug,
|
|
315
|
+
org_id: managed.org_id,
|
|
316
|
+
runjack_url: `https://${managed.slug}.runjack.xyz`,
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
} catch {
|
|
322
|
+
// Control plane unavailable or not found
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Optionally fetch resources
|
|
327
|
+
if (resolved && options?.includeResources) {
|
|
328
|
+
const resources = await resolveProjectResources(resolved, options.projectPath || process.cwd());
|
|
329
|
+
if (resources) {
|
|
330
|
+
resolved.resources = resources;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return resolved;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* List ALL projects from all sources
|
|
339
|
+
* Merges and dedupes automatically
|
|
340
|
+
*/
|
|
341
|
+
export async function listAllProjects(): Promise<ResolvedProject[]> {
|
|
342
|
+
const projectMap = new Map<string, ResolvedProject>();
|
|
343
|
+
|
|
344
|
+
// Get all registry projects
|
|
345
|
+
const registryProjects = await getAllProjects();
|
|
346
|
+
const registryIndex = buildRegistryIndexes(registryProjects);
|
|
347
|
+
for (const [name, project] of Object.entries(registryProjects)) {
|
|
348
|
+
const key = project.remote?.project_slug ?? name;
|
|
349
|
+
projectMap.set(key, fromRegistryProject(name, project));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Get all managed projects if logged in
|
|
353
|
+
if (await isLoggedIn()) {
|
|
354
|
+
try {
|
|
355
|
+
const managedProjects = await listManagedProjects();
|
|
356
|
+
|
|
357
|
+
// Filter out deleted projects - they're orphaned control plane records
|
|
358
|
+
const activeProjects = managedProjects.filter((p) => p.status !== "deleted");
|
|
359
|
+
|
|
360
|
+
for (const managed of activeProjects) {
|
|
361
|
+
const existing = projectMap.get(managed.slug);
|
|
362
|
+
|
|
363
|
+
if (existing) {
|
|
364
|
+
// Merge with registry data
|
|
365
|
+
projectMap.set(managed.slug, mergeProjects(existing, fromManagedProject(managed)));
|
|
366
|
+
} else {
|
|
367
|
+
// New project not in registry
|
|
368
|
+
const resolved = fromManagedProject(managed);
|
|
369
|
+
projectMap.set(managed.slug, resolved);
|
|
370
|
+
|
|
371
|
+
// Cache in registry
|
|
372
|
+
const registryName =
|
|
373
|
+
registryIndex.byRemoteId.get(managed.id) ??
|
|
374
|
+
registryIndex.byRemoteSlug.get(managed.slug) ??
|
|
375
|
+
managed.slug;
|
|
376
|
+
await registerProject(registryName, {
|
|
377
|
+
workerUrl: resolved.url || null,
|
|
378
|
+
createdAt: managed.created_at,
|
|
379
|
+
lastDeployed: managed.updated_at,
|
|
380
|
+
status: managed.status === "active" ? "live" : "build_failed",
|
|
381
|
+
deploy_mode: "managed",
|
|
382
|
+
remote: {
|
|
383
|
+
project_id: managed.id,
|
|
384
|
+
project_slug: managed.slug,
|
|
385
|
+
org_id: managed.org_id,
|
|
386
|
+
runjack_url: `https://${managed.slug}.runjack.xyz`,
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
} catch {
|
|
392
|
+
// Control plane unavailable, use registry-only data
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Enrich with local paths
|
|
397
|
+
const localPaths = await getAllLocalPaths();
|
|
398
|
+
|
|
399
|
+
for (const [name, paths] of Object.entries(localPaths)) {
|
|
400
|
+
const existing = projectMap.get(name);
|
|
401
|
+
|
|
402
|
+
if (existing) {
|
|
403
|
+
// Update existing project with local info
|
|
404
|
+
existing.localPath = paths[0]; // Primary path
|
|
405
|
+
existing.sources.filesystem = true;
|
|
406
|
+
} else {
|
|
407
|
+
// Project exists locally but not in registry/cloud
|
|
408
|
+
projectMap.set(name, {
|
|
409
|
+
name,
|
|
410
|
+
slug: name,
|
|
411
|
+
status: "local-only",
|
|
412
|
+
sources: { registry: false, controlPlane: false, filesystem: true },
|
|
413
|
+
localPath: paths[0],
|
|
414
|
+
createdAt: new Date().toISOString(),
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return Array.from(projectMap.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Check if a name is available for new project
|
|
424
|
+
*/
|
|
425
|
+
export async function checkAvailability(name: string): Promise<{
|
|
426
|
+
available: boolean;
|
|
427
|
+
existingProject?: ResolvedProject;
|
|
428
|
+
}> {
|
|
429
|
+
const existing = await resolveProject(name, { matchByName: false });
|
|
430
|
+
|
|
431
|
+
return {
|
|
432
|
+
available: !existing,
|
|
433
|
+
existingProject: existing || undefined,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Remove a project from everywhere
|
|
439
|
+
* Handles: registry cleanup, control plane deletion, worker teardown
|
|
440
|
+
*/
|
|
441
|
+
export async function removeProject(name: string): Promise<{
|
|
442
|
+
removed: string[];
|
|
443
|
+
errors: string[];
|
|
444
|
+
}> {
|
|
445
|
+
const removed: string[] = [];
|
|
446
|
+
const errors: string[] = [];
|
|
447
|
+
|
|
448
|
+
// Resolve project to find all locations
|
|
449
|
+
const project = await resolveProject(name);
|
|
450
|
+
if (!project) {
|
|
451
|
+
return { removed, errors: [`Project "${name}" not found`] };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Remove from control plane if managed
|
|
455
|
+
if (project.remote?.projectId) {
|
|
456
|
+
try {
|
|
457
|
+
const { deleteManagedProject } = await import("./control-plane.ts");
|
|
458
|
+
await deleteManagedProject(project.remote.projectId);
|
|
459
|
+
removed.push("jack cloud");
|
|
460
|
+
} catch (error) {
|
|
461
|
+
errors.push(`Failed to delete from jack cloud: ${error}`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Remove from registry
|
|
466
|
+
if (project.sources.registry) {
|
|
467
|
+
try {
|
|
468
|
+
await removeFromRegistry(project.name);
|
|
469
|
+
removed.push("local registry");
|
|
470
|
+
} catch (error) {
|
|
471
|
+
errors.push(`Failed to remove from registry: ${error}`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return { removed, errors };
|
|
476
|
+
}
|
package/src/lib/registry.ts
CHANGED
|
@@ -2,46 +2,111 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { CONFIG_DIR } from "./config.ts";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Deploy mode for a project
|
|
7
|
+
*/
|
|
8
|
+
export type DeployMode = "managed" | "byo";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Template origin tracking for agent file regeneration
|
|
12
|
+
*/
|
|
13
|
+
export interface TemplateOrigin {
|
|
14
|
+
type: "builtin" | "github";
|
|
15
|
+
name: string; // "miniapp", "api", or "user/repo"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Remote metadata for managed projects
|
|
20
|
+
*/
|
|
21
|
+
export interface ManagedRemote {
|
|
22
|
+
project_id: string;
|
|
23
|
+
project_slug: string;
|
|
24
|
+
org_id: string;
|
|
25
|
+
runjack_url: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
5
28
|
/**
|
|
6
29
|
* Project data stored in registry
|
|
7
30
|
*/
|
|
8
31
|
export interface Project {
|
|
9
|
-
localPath: string | null;
|
|
10
32
|
workerUrl: string | null;
|
|
11
33
|
createdAt: string;
|
|
12
34
|
lastDeployed: string | null;
|
|
13
|
-
|
|
35
|
+
status?: "created" | "build_failed" | "live";
|
|
36
|
+
cloudflare?: {
|
|
14
37
|
accountId: string;
|
|
15
38
|
workerId: string;
|
|
16
39
|
};
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
};
|
|
21
|
-
};
|
|
40
|
+
template?: TemplateOrigin;
|
|
41
|
+
deploy_mode?: DeployMode;
|
|
42
|
+
remote?: ManagedRemote;
|
|
22
43
|
}
|
|
23
44
|
|
|
24
45
|
/**
|
|
25
46
|
* Project registry structure
|
|
26
47
|
*/
|
|
27
48
|
export interface Registry {
|
|
28
|
-
version:
|
|
49
|
+
version: 2;
|
|
29
50
|
projects: Record<string, Project>;
|
|
30
51
|
}
|
|
31
52
|
|
|
32
53
|
export const REGISTRY_PATH = join(CONFIG_DIR, "projects.json");
|
|
33
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Migrate registry from v1 to v2
|
|
57
|
+
* - Removes localPath field (no longer tracked)
|
|
58
|
+
* - Removes resources.services.db field (fetch from control plane instead)
|
|
59
|
+
*/
|
|
60
|
+
async function migrateV1ToV2(v1Registry: {
|
|
61
|
+
version: 1;
|
|
62
|
+
projects: Record<string, unknown>;
|
|
63
|
+
}): Promise<Registry> {
|
|
64
|
+
const migrated: Registry = {
|
|
65
|
+
version: 2,
|
|
66
|
+
projects: {},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
for (const [name, project] of Object.entries(v1Registry.projects)) {
|
|
70
|
+
const p = project as Record<string, unknown>;
|
|
71
|
+
// Remove localPath and resources, keep everything else
|
|
72
|
+
const { localPath, resources, ...rest } = p;
|
|
73
|
+
migrated.projects[name] = rest as unknown as Project;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return migrated;
|
|
77
|
+
}
|
|
78
|
+
|
|
34
79
|
/**
|
|
35
80
|
* Read project registry from disk
|
|
36
81
|
*/
|
|
37
82
|
export async function readRegistry(): Promise<Registry> {
|
|
38
83
|
if (!existsSync(REGISTRY_PATH)) {
|
|
39
|
-
return { version:
|
|
84
|
+
return { version: 2, projects: {} };
|
|
40
85
|
}
|
|
86
|
+
|
|
41
87
|
try {
|
|
42
|
-
|
|
88
|
+
const raw = await Bun.file(REGISTRY_PATH).json();
|
|
89
|
+
|
|
90
|
+
// Auto-migrate v1 to v2
|
|
91
|
+
if (raw.version === 1) {
|
|
92
|
+
const migrated = await migrateV1ToV2(raw);
|
|
93
|
+
await writeRegistry(migrated);
|
|
94
|
+
return migrated;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Handle unversioned (legacy) registries
|
|
98
|
+
if (!raw.version) {
|
|
99
|
+
const migrated = await migrateV1ToV2({
|
|
100
|
+
version: 1,
|
|
101
|
+
projects: raw.projects || {},
|
|
102
|
+
});
|
|
103
|
+
await writeRegistry(migrated);
|
|
104
|
+
return migrated;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return raw as Registry;
|
|
43
108
|
} catch {
|
|
44
|
-
return { version:
|
|
109
|
+
return { version: 2, projects: {} };
|
|
45
110
|
}
|
|
46
111
|
}
|
|
47
112
|
|
|
@@ -114,29 +179,3 @@ export async function getAllProjects(): Promise<Record<string, Project>> {
|
|
|
114
179
|
const registry = await readRegistry();
|
|
115
180
|
return registry.projects;
|
|
116
181
|
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Get database name for a project
|
|
120
|
-
* @returns Database name or null if no database is configured
|
|
121
|
-
*/
|
|
122
|
-
export function getProjectDatabaseName(project: Project): string | null {
|
|
123
|
-
return project.resources?.services?.db ?? null;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Update the database for a project using the new services structure
|
|
128
|
-
*/
|
|
129
|
-
export async function updateProjectDatabase(name: string, dbName: string | null): Promise<void> {
|
|
130
|
-
const project = await getProject(name);
|
|
131
|
-
if (!project) return;
|
|
132
|
-
|
|
133
|
-
await updateProject(name, {
|
|
134
|
-
resources: {
|
|
135
|
-
...project.resources,
|
|
136
|
-
services: {
|
|
137
|
-
...project.resources.services,
|
|
138
|
-
db: dbName,
|
|
139
|
-
},
|
|
140
|
-
},
|
|
141
|
-
});
|
|
142
|
-
}
|