@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.
Files changed (91) hide show
  1. package/package.json +54 -47
  2. package/src/commands/agents.ts +145 -10
  3. package/src/commands/down.ts +110 -102
  4. package/src/commands/feedback.ts +189 -0
  5. package/src/commands/init.ts +8 -12
  6. package/src/commands/login.ts +88 -0
  7. package/src/commands/logout.ts +14 -0
  8. package/src/commands/logs.ts +21 -0
  9. package/src/commands/mcp.ts +134 -7
  10. package/src/commands/new.ts +43 -17
  11. package/src/commands/open.ts +13 -6
  12. package/src/commands/projects.ts +269 -143
  13. package/src/commands/secrets.ts +413 -0
  14. package/src/commands/services.ts +96 -123
  15. package/src/commands/ship.ts +5 -1
  16. package/src/commands/whoami.ts +31 -0
  17. package/src/index.ts +218 -144
  18. package/src/lib/agent-files.ts +34 -0
  19. package/src/lib/agents.ts +390 -22
  20. package/src/lib/asset-hash.ts +50 -0
  21. package/src/lib/auth/client.ts +115 -0
  22. package/src/lib/auth/constants.ts +5 -0
  23. package/src/lib/auth/guard.ts +57 -0
  24. package/src/lib/auth/index.ts +18 -0
  25. package/src/lib/auth/store.ts +54 -0
  26. package/src/lib/binding-validator.ts +136 -0
  27. package/src/lib/build-helper.ts +211 -0
  28. package/src/lib/cloudflare-api.ts +24 -0
  29. package/src/lib/config.ts +5 -6
  30. package/src/lib/control-plane.ts +295 -0
  31. package/src/lib/debug.ts +3 -1
  32. package/src/lib/deploy-mode.ts +93 -0
  33. package/src/lib/deploy-upload.ts +92 -0
  34. package/src/lib/errors.ts +2 -0
  35. package/src/lib/github.ts +31 -1
  36. package/src/lib/hooks.ts +4 -12
  37. package/src/lib/intent.ts +88 -0
  38. package/src/lib/jsonc.ts +125 -0
  39. package/src/lib/local-paths.test.ts +902 -0
  40. package/src/lib/local-paths.ts +258 -0
  41. package/src/lib/managed-deploy.ts +175 -0
  42. package/src/lib/managed-down.ts +159 -0
  43. package/src/lib/mcp-config.ts +55 -34
  44. package/src/lib/names.ts +9 -29
  45. package/src/lib/project-operations.ts +676 -249
  46. package/src/lib/project-resolver.ts +476 -0
  47. package/src/lib/registry.ts +76 -37
  48. package/src/lib/resources.ts +196 -0
  49. package/src/lib/schema.ts +30 -1
  50. package/src/lib/storage/file-filter.ts +1 -0
  51. package/src/lib/storage/index.ts +5 -1
  52. package/src/lib/telemetry.ts +14 -0
  53. package/src/lib/tty.ts +15 -0
  54. package/src/lib/zip-packager.ts +255 -0
  55. package/src/mcp/resources/index.ts +8 -2
  56. package/src/mcp/server.ts +32 -4
  57. package/src/mcp/tools/index.ts +35 -13
  58. package/src/mcp/types.ts +6 -0
  59. package/src/mcp/utils.ts +1 -1
  60. package/src/templates/index.ts +42 -4
  61. package/src/templates/types.ts +13 -0
  62. package/templates/CLAUDE.md +166 -0
  63. package/templates/api/.jack.json +4 -0
  64. package/templates/api/bun.lock +1 -0
  65. package/templates/api/wrangler.jsonc +5 -0
  66. package/templates/hello/.jack.json +28 -0
  67. package/templates/hello/package.json +10 -0
  68. package/templates/hello/src/index.ts +11 -0
  69. package/templates/hello/tsconfig.json +11 -0
  70. package/templates/hello/wrangler.jsonc +5 -0
  71. package/templates/miniapp/.jack.json +15 -4
  72. package/templates/miniapp/bun.lock +135 -40
  73. package/templates/miniapp/index.html +1 -0
  74. package/templates/miniapp/package.json +3 -1
  75. package/templates/miniapp/public/.well-known/farcaster.json +7 -5
  76. package/templates/miniapp/public/icon.png +0 -0
  77. package/templates/miniapp/public/og.png +0 -0
  78. package/templates/miniapp/schema.sql +8 -0
  79. package/templates/miniapp/src/App.tsx +254 -3
  80. package/templates/miniapp/src/components/ShareSheet.tsx +147 -0
  81. package/templates/miniapp/src/hooks/useAI.ts +35 -0
  82. package/templates/miniapp/src/hooks/useGuestbook.ts +11 -1
  83. package/templates/miniapp/src/hooks/useShare.ts +76 -0
  84. package/templates/miniapp/src/index.css +15 -0
  85. package/templates/miniapp/src/lib/api.ts +2 -1
  86. package/templates/miniapp/src/worker.ts +515 -1
  87. package/templates/miniapp/wrangler.jsonc +15 -3
  88. package/LICENSE +0 -190
  89. package/README.md +0 -55
  90. package/src/commands/cloud.ts +0 -230
  91. 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
+ }
@@ -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
- cloudflare: {
35
+ status?: "created" | "build_failed" | "live";
36
+ cloudflare?: {
14
37
  accountId: string;
15
38
  workerId: string;
16
39
  };
17
- resources: {
18
- services: {
19
- db: string | null;
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: 1;
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: 1, projects: {} };
84
+ return { version: 2, projects: {} };
40
85
  }
86
+
41
87
  try {
42
- return await Bun.file(REGISTRY_PATH).json();
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: 1, projects: {} };
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
- }