@getjack/jack 0.1.3 → 0.1.5
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/README.md +103 -0
- package/package.json +2 -6
- package/src/commands/agents.ts +9 -24
- package/src/commands/clone.ts +27 -0
- package/src/commands/down.ts +31 -57
- package/src/commands/feedback.ts +4 -5
- package/src/commands/link.ts +147 -0
- package/src/commands/logs.ts +8 -18
- package/src/commands/new.ts +7 -1
- package/src/commands/projects.ts +162 -105
- package/src/commands/secrets.ts +7 -6
- package/src/commands/services.ts +5 -4
- package/src/commands/tag.ts +282 -0
- package/src/commands/unlink.ts +30 -0
- package/src/index.ts +46 -1
- package/src/lib/auth/index.ts +2 -0
- package/src/lib/auth/store.ts +26 -2
- package/src/lib/binding-validator.ts +4 -13
- package/src/lib/build-helper.ts +93 -5
- package/src/lib/control-plane.ts +48 -0
- package/src/lib/deploy-mode.ts +1 -1
- package/src/lib/managed-deploy.ts +11 -1
- package/src/lib/managed-down.ts +7 -20
- package/src/lib/paths-index.test.ts +546 -0
- package/src/lib/paths-index.ts +310 -0
- package/src/lib/project-link.test.ts +459 -0
- package/src/lib/project-link.ts +279 -0
- package/src/lib/project-list.test.ts +581 -0
- package/src/lib/project-list.ts +445 -0
- package/src/lib/project-operations.ts +304 -183
- package/src/lib/project-resolver.ts +191 -211
- package/src/lib/tags.ts +389 -0
- package/src/lib/telemetry.ts +81 -168
- package/src/lib/zip-packager.ts +9 -0
- package/src/templates/index.ts +5 -3
- package/templates/api/.jack/template.json +4 -0
- package/templates/hello/.jack/template.json +4 -0
- package/templates/miniapp/.jack/template.json +4 -0
- package/templates/nextjs/.jack.json +28 -0
- package/templates/nextjs/app/globals.css +9 -0
- package/templates/nextjs/app/isr-test/page.tsx +22 -0
- package/templates/nextjs/app/layout.tsx +19 -0
- package/templates/nextjs/app/page.tsx +8 -0
- package/templates/nextjs/bun.lock +2232 -0
- package/templates/nextjs/cloudflare-env.d.ts +3 -0
- package/templates/nextjs/next-env.d.ts +6 -0
- package/templates/nextjs/next.config.ts +8 -0
- package/templates/nextjs/open-next.config.ts +6 -0
- package/templates/nextjs/package.json +24 -0
- package/templates/nextjs/public/_headers +2 -0
- package/templates/nextjs/tsconfig.json +44 -0
- package/templates/nextjs/wrangler.jsonc +17 -0
- package/src/lib/local-paths.test.ts +0 -902
- package/src/lib/local-paths.ts +0 -258
- package/src/lib/registry.ts +0 -181
|
@@ -4,29 +4,30 @@
|
|
|
4
4
|
* Users see "projects". We handle the plumbing.
|
|
5
5
|
*
|
|
6
6
|
* Resolution strategy:
|
|
7
|
-
* 1. Check
|
|
8
|
-
* 2.
|
|
9
|
-
* 3.
|
|
7
|
+
* 1. Check .jack/project.json (local link)
|
|
8
|
+
* 2. If managed: fetch from control plane (authoritative)
|
|
9
|
+
* 3. If BYO: use local link info only
|
|
10
10
|
*
|
|
11
11
|
* Control plane is authoritative for managed projects.
|
|
12
|
-
*
|
|
12
|
+
* .jack/project.json is the local link (not a cache).
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { isLoggedIn } from "./auth/index.ts";
|
|
15
|
+
import { type AuthState, getAuthState, isLoggedIn } from "./auth/index.ts";
|
|
16
16
|
import {
|
|
17
17
|
type ManagedProject,
|
|
18
18
|
fetchProjectResources,
|
|
19
19
|
findProjectBySlug,
|
|
20
20
|
listManagedProjects,
|
|
21
21
|
} from "./control-plane.ts";
|
|
22
|
-
import {
|
|
22
|
+
import { getAllPaths, unregisterPath } from "./paths-index.ts";
|
|
23
23
|
import {
|
|
24
|
-
type
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
type DeployMode,
|
|
25
|
+
type LocalProjectLink,
|
|
26
|
+
getDeployMode,
|
|
27
|
+
getProjectId,
|
|
28
|
+
readProjectLink,
|
|
29
|
+
unlinkProject,
|
|
30
|
+
} from "./project-link.ts";
|
|
30
31
|
import {
|
|
31
32
|
type ResolvedResources,
|
|
32
33
|
convertControlPlaneResources,
|
|
@@ -36,7 +37,7 @@ import {
|
|
|
36
37
|
/**
|
|
37
38
|
* User-facing project status
|
|
38
39
|
*/
|
|
39
|
-
export type ProjectStatus = "live" | "local-only" | "error" | "syncing";
|
|
40
|
+
export type ProjectStatus = "live" | "local-only" | "error" | "syncing" | "auth-expired";
|
|
40
41
|
|
|
41
42
|
/**
|
|
42
43
|
* Unified project representation
|
|
@@ -52,7 +53,6 @@ export interface ResolvedProject {
|
|
|
52
53
|
|
|
53
54
|
// Where we found it (internal, not shown to user)
|
|
54
55
|
sources: {
|
|
55
|
-
registry: boolean;
|
|
56
56
|
controlPlane: boolean;
|
|
57
57
|
filesystem: boolean;
|
|
58
58
|
};
|
|
@@ -64,48 +64,44 @@ export interface ResolvedProject {
|
|
|
64
64
|
orgId: string;
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
+
// Deploy mode
|
|
68
|
+
deployMode?: DeployMode;
|
|
69
|
+
|
|
67
70
|
// Metadata
|
|
68
71
|
createdAt: string;
|
|
69
72
|
updatedAt?: string;
|
|
70
73
|
|
|
71
74
|
// Resources (fetched on-demand)
|
|
72
75
|
resources?: ResolvedResources;
|
|
76
|
+
|
|
77
|
+
// Tags (from local project link)
|
|
78
|
+
tags?: string[];
|
|
73
79
|
}
|
|
74
80
|
|
|
75
81
|
/**
|
|
76
|
-
* Convert
|
|
82
|
+
* Convert a local project link to a resolved project
|
|
77
83
|
*/
|
|
78
|
-
function
|
|
79
|
-
|
|
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
|
-
}
|
|
84
|
+
function fromLocalLink(link: LocalProjectLink, localPath: string): ResolvedProject {
|
|
85
|
+
const isByo = link.deploy_mode === "byo";
|
|
89
86
|
|
|
90
87
|
return {
|
|
91
|
-
name,
|
|
92
|
-
slug:
|
|
93
|
-
status,
|
|
94
|
-
url: project.workerUrl || project.remote?.runjack_url || undefined,
|
|
88
|
+
name: localPath.split("/").pop() || "unknown",
|
|
89
|
+
slug: localPath.split("/").pop() || "unknown",
|
|
90
|
+
status: isByo ? "local-only" : "syncing", // BYO is local-only, managed needs control plane check
|
|
95
91
|
sources: {
|
|
96
|
-
registry: true,
|
|
97
92
|
controlPlane: false,
|
|
98
|
-
filesystem:
|
|
93
|
+
filesystem: true,
|
|
99
94
|
},
|
|
100
|
-
localPath
|
|
101
|
-
remote:
|
|
102
|
-
?
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
95
|
+
localPath,
|
|
96
|
+
remote: isByo
|
|
97
|
+
? undefined
|
|
98
|
+
: {
|
|
99
|
+
projectId: link.project_id,
|
|
100
|
+
orgId: "", // Will be filled from control plane
|
|
101
|
+
},
|
|
102
|
+
deployMode: link.deploy_mode,
|
|
103
|
+
createdAt: link.linked_at,
|
|
104
|
+
tags: link.tags,
|
|
109
105
|
};
|
|
110
106
|
}
|
|
111
107
|
|
|
@@ -115,6 +111,16 @@ function fromRegistryProject(name: string, project: RegistryProject): ResolvedPr
|
|
|
115
111
|
function fromManagedProject(managed: ManagedProject): ResolvedProject {
|
|
116
112
|
const status: ProjectStatus = managed.status === "active" ? "live" : "error";
|
|
117
113
|
|
|
114
|
+
// Parse tags from JSON string (e.g., '["backend", "api"]')
|
|
115
|
+
let tags: string[] | undefined;
|
|
116
|
+
if (managed.tags) {
|
|
117
|
+
try {
|
|
118
|
+
tags = JSON.parse(managed.tags);
|
|
119
|
+
} catch {
|
|
120
|
+
// Invalid JSON, ignore
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
118
124
|
return {
|
|
119
125
|
name: managed.name,
|
|
120
126
|
slug: managed.slug,
|
|
@@ -122,7 +128,6 @@ function fromManagedProject(managed: ManagedProject): ResolvedProject {
|
|
|
122
128
|
url: `https://${managed.slug}.runjack.xyz`,
|
|
123
129
|
errorMessage: managed.status === "error" ? "deployment failed" : undefined,
|
|
124
130
|
sources: {
|
|
125
|
-
registry: false,
|
|
126
131
|
controlPlane: true,
|
|
127
132
|
filesystem: false,
|
|
128
133
|
},
|
|
@@ -130,27 +135,33 @@ function fromManagedProject(managed: ManagedProject): ResolvedProject {
|
|
|
130
135
|
projectId: managed.id,
|
|
131
136
|
orgId: managed.org_id,
|
|
132
137
|
},
|
|
138
|
+
deployMode: "managed",
|
|
133
139
|
createdAt: managed.created_at,
|
|
134
140
|
updatedAt: managed.updated_at,
|
|
141
|
+
tags,
|
|
135
142
|
};
|
|
136
143
|
}
|
|
137
144
|
|
|
138
145
|
/**
|
|
139
|
-
* Merge
|
|
146
|
+
* Merge local and managed project data
|
|
140
147
|
*/
|
|
141
|
-
function mergeProjects(
|
|
148
|
+
function mergeProjects(local: ResolvedProject, managed: ResolvedProject): ResolvedProject {
|
|
142
149
|
return {
|
|
143
|
-
...
|
|
150
|
+
...local,
|
|
151
|
+
name: managed.name, // Control plane name is authoritative
|
|
152
|
+
slug: managed.slug,
|
|
144
153
|
status: managed.status, // Control plane is authoritative for status
|
|
145
|
-
url: managed.url ||
|
|
154
|
+
url: managed.url || local.url,
|
|
146
155
|
errorMessage: managed.errorMessage,
|
|
147
156
|
sources: {
|
|
148
|
-
registry: true,
|
|
149
157
|
controlPlane: true,
|
|
150
|
-
filesystem:
|
|
158
|
+
filesystem: local.sources.filesystem,
|
|
151
159
|
},
|
|
152
160
|
remote: managed.remote,
|
|
153
|
-
|
|
161
|
+
deployMode: "managed",
|
|
162
|
+
updatedAt: managed.updatedAt || local.updatedAt,
|
|
163
|
+
// Local tags take priority; fall back to remote if local has none
|
|
164
|
+
tags: local.tags?.length ? local.tags : managed.tags,
|
|
154
165
|
};
|
|
155
166
|
}
|
|
156
167
|
|
|
@@ -166,29 +177,6 @@ export interface ResolveProjectOptions {
|
|
|
166
177
|
matchByName?: boolean;
|
|
167
178
|
}
|
|
168
179
|
|
|
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
180
|
/**
|
|
193
181
|
* Resolve project resources based on deploy mode.
|
|
194
182
|
* For managed: fetch from control plane
|
|
@@ -222,9 +210,9 @@ export async function resolveProjectResources(
|
|
|
222
210
|
}
|
|
223
211
|
|
|
224
212
|
/**
|
|
225
|
-
* Resolve a project by name/slug
|
|
226
|
-
* Checks:
|
|
227
|
-
*
|
|
213
|
+
* Resolve a project by name/slug or from current directory
|
|
214
|
+
* Checks: .jack/project.json -> control plane
|
|
215
|
+
* No caching - fresh reads only
|
|
228
216
|
*/
|
|
229
217
|
export async function resolveProject(
|
|
230
218
|
name: string,
|
|
@@ -232,46 +220,48 @@ export async function resolveProject(
|
|
|
232
220
|
): Promise<ResolvedProject | null> {
|
|
233
221
|
let resolved: ResolvedProject | null = null;
|
|
234
222
|
const matchByName = options?.matchByName !== false;
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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);
|
|
223
|
+
const projectPath = options?.projectPath || process.cwd();
|
|
224
|
+
|
|
225
|
+
// First, check if we're resolving from a local path with .jack/project.json
|
|
226
|
+
const link = await readProjectLink(projectPath);
|
|
227
|
+
|
|
228
|
+
if (link) {
|
|
229
|
+
// We have a local link - start with that
|
|
230
|
+
resolved = fromLocalLink(link, projectPath);
|
|
252
231
|
|
|
253
|
-
|
|
254
|
-
|
|
232
|
+
if (link.deploy_mode === "byo") {
|
|
233
|
+
// BYO project - use local link info only, no control plane
|
|
255
234
|
// resolved stays as-is
|
|
256
|
-
} else if (
|
|
257
|
-
//
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
235
|
+
} else if (link.deploy_mode === "managed") {
|
|
236
|
+
// Managed project - check auth state and fetch from control plane
|
|
237
|
+
const authState = await getAuthState();
|
|
238
|
+
|
|
239
|
+
if (authState === "logged-in") {
|
|
240
|
+
try {
|
|
241
|
+
// Try to find by project ID first via listing
|
|
242
|
+
const managedProjects = await listManagedProjects();
|
|
243
|
+
const managed = managedProjects.find((p) => p.id === link.project_id);
|
|
244
|
+
|
|
245
|
+
if (managed) {
|
|
246
|
+
resolved = mergeProjects(resolved, fromManagedProject(managed));
|
|
247
|
+
} else {
|
|
248
|
+
// Project ID not found in control plane - might be deleted
|
|
249
|
+
resolved.status = "error";
|
|
250
|
+
resolved.errorMessage = "Project not found in jack cloud";
|
|
251
|
+
}
|
|
252
|
+
} catch {
|
|
253
|
+
// Control plane unavailable, use local data with syncing status
|
|
254
|
+
resolved.status = "syncing";
|
|
268
255
|
}
|
|
269
|
-
}
|
|
270
|
-
//
|
|
256
|
+
} else if (authState === "session-expired") {
|
|
257
|
+
// Session expired - show clear status
|
|
258
|
+
resolved.status = "auth-expired";
|
|
259
|
+
resolved.errorMessage = "Session expired. Run: jack login";
|
|
271
260
|
}
|
|
261
|
+
// If not-logged-in, keep "syncing" status from fromLocalLink
|
|
272
262
|
}
|
|
273
263
|
} else if (await isLoggedIn()) {
|
|
274
|
-
//
|
|
264
|
+
// No local link - check control plane by slug/name if logged in
|
|
275
265
|
try {
|
|
276
266
|
let managed = await findProjectBySlug(name);
|
|
277
267
|
if (!managed && matchByName) {
|
|
@@ -279,43 +269,14 @@ export async function resolveProject(
|
|
|
279
269
|
managed = managedProjects.find((project) => project.name === name) ?? null;
|
|
280
270
|
}
|
|
281
271
|
if (managed) {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
});
|
|
272
|
+
resolved = fromManagedProject(managed);
|
|
273
|
+
|
|
274
|
+
// Check if we have a local path for this project
|
|
275
|
+
const allPaths = await getAllPaths();
|
|
276
|
+
const localPaths = allPaths[managed.id];
|
|
277
|
+
if (localPaths && localPaths.length > 0) {
|
|
278
|
+
resolved.localPath = localPaths[0];
|
|
279
|
+
resolved.sources.filesystem = true;
|
|
319
280
|
}
|
|
320
281
|
}
|
|
321
282
|
} catch {
|
|
@@ -323,9 +284,28 @@ export async function resolveProject(
|
|
|
323
284
|
}
|
|
324
285
|
}
|
|
325
286
|
|
|
287
|
+
// If still not found, check paths index for local projects
|
|
288
|
+
if (!resolved) {
|
|
289
|
+
const allPaths = await getAllPaths();
|
|
290
|
+
for (const [projectId, paths] of Object.entries(allPaths)) {
|
|
291
|
+
for (const localPath of paths) {
|
|
292
|
+
const localLink = await readProjectLink(localPath);
|
|
293
|
+
if (localLink) {
|
|
294
|
+
const dirName = localPath.split("/").pop() || "";
|
|
295
|
+
// Match by directory name or project_id
|
|
296
|
+
if (dirName === name || projectId === name) {
|
|
297
|
+
resolved = fromLocalLink(localLink, localPath);
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (resolved) break;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
326
306
|
// Optionally fetch resources
|
|
327
307
|
if (resolved && options?.includeResources) {
|
|
328
|
-
const resources = await resolveProjectResources(resolved,
|
|
308
|
+
const resources = await resolveProjectResources(resolved, resolved.localPath || projectPath);
|
|
329
309
|
if (resources) {
|
|
330
310
|
resolved.resources = resources;
|
|
331
311
|
}
|
|
@@ -336,21 +316,30 @@ export async function resolveProject(
|
|
|
336
316
|
|
|
337
317
|
/**
|
|
338
318
|
* List ALL projects from all sources
|
|
339
|
-
* Merges and dedupes
|
|
319
|
+
* Merges and dedupes by project_id
|
|
340
320
|
*/
|
|
341
321
|
export async function listAllProjects(): Promise<ResolvedProject[]> {
|
|
342
322
|
const projectMap = new Map<string, ResolvedProject>();
|
|
343
323
|
|
|
344
|
-
// Get all
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
for (const [
|
|
348
|
-
|
|
349
|
-
|
|
324
|
+
// Get all local projects from paths index
|
|
325
|
+
const allPaths = await getAllPaths();
|
|
326
|
+
|
|
327
|
+
for (const [projectId, paths] of Object.entries(allPaths)) {
|
|
328
|
+
// Read the first valid path's link
|
|
329
|
+
for (const localPath of paths) {
|
|
330
|
+
const link = await readProjectLink(localPath);
|
|
331
|
+
if (link) {
|
|
332
|
+
const resolved = fromLocalLink(link, localPath);
|
|
333
|
+
projectMap.set(projectId, resolved);
|
|
334
|
+
break; // Use first valid path
|
|
335
|
+
}
|
|
336
|
+
}
|
|
350
337
|
}
|
|
351
338
|
|
|
352
|
-
// Get all managed projects
|
|
353
|
-
|
|
339
|
+
// Get all managed projects based on auth state
|
|
340
|
+
const authState = await getAuthState();
|
|
341
|
+
|
|
342
|
+
if (authState === "logged-in") {
|
|
354
343
|
try {
|
|
355
344
|
const managedProjects = await listManagedProjects();
|
|
356
345
|
|
|
@@ -358,61 +347,35 @@ export async function listAllProjects(): Promise<ResolvedProject[]> {
|
|
|
358
347
|
const activeProjects = managedProjects.filter((p) => p.status !== "deleted");
|
|
359
348
|
|
|
360
349
|
for (const managed of activeProjects) {
|
|
361
|
-
const existing = projectMap.get(managed.
|
|
350
|
+
const existing = projectMap.get(managed.id);
|
|
362
351
|
|
|
363
352
|
if (existing) {
|
|
364
|
-
// Merge with
|
|
365
|
-
projectMap.set(managed.
|
|
353
|
+
// Merge with local data - control plane is authoritative
|
|
354
|
+
projectMap.set(managed.id, mergeProjects(existing, fromManagedProject(managed)));
|
|
366
355
|
} else {
|
|
367
|
-
// New project not in
|
|
356
|
+
// New project not in local index
|
|
368
357
|
const resolved = fromManagedProject(managed);
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
});
|
|
358
|
+
|
|
359
|
+
// Check if we have a local path for this project
|
|
360
|
+
const localPaths = allPaths[managed.id];
|
|
361
|
+
if (localPaths && localPaths.length > 0) {
|
|
362
|
+
resolved.localPath = localPaths[0];
|
|
363
|
+
resolved.sources.filesystem = true;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
projectMap.set(managed.id, resolved);
|
|
389
367
|
}
|
|
390
368
|
}
|
|
391
369
|
} catch {
|
|
392
|
-
// Control plane unavailable, use
|
|
370
|
+
// Control plane unavailable, use local-only data
|
|
393
371
|
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
});
|
|
372
|
+
} else if (authState === "session-expired") {
|
|
373
|
+
// Mark all managed projects as auth-expired
|
|
374
|
+
for (const [projectId, project] of projectMap) {
|
|
375
|
+
if (project.deployMode === "managed" && project.status === "syncing") {
|
|
376
|
+
project.status = "auth-expired";
|
|
377
|
+
project.errorMessage = "Session expired. Run: jack login";
|
|
378
|
+
}
|
|
416
379
|
}
|
|
417
380
|
}
|
|
418
381
|
|
|
@@ -436,21 +399,28 @@ export async function checkAvailability(name: string): Promise<{
|
|
|
436
399
|
|
|
437
400
|
/**
|
|
438
401
|
* Remove a project from everywhere
|
|
439
|
-
* Handles:
|
|
402
|
+
* Handles: .jack/ cleanup, paths index, control plane deletion
|
|
440
403
|
*/
|
|
441
|
-
export async function removeProject(
|
|
404
|
+
export async function removeProject(
|
|
405
|
+
name: string,
|
|
406
|
+
projectPath?: string,
|
|
407
|
+
): Promise<{
|
|
442
408
|
removed: string[];
|
|
443
409
|
errors: string[];
|
|
444
410
|
}> {
|
|
445
411
|
const removed: string[] = [];
|
|
446
412
|
const errors: string[] = [];
|
|
413
|
+
const localPath = projectPath || process.cwd();
|
|
447
414
|
|
|
448
415
|
// Resolve project to find all locations
|
|
449
|
-
const project = await resolveProject(name);
|
|
416
|
+
const project = await resolveProject(name, { projectPath: localPath });
|
|
450
417
|
if (!project) {
|
|
451
418
|
return { removed, errors: [`Project "${name}" not found`] };
|
|
452
419
|
}
|
|
453
420
|
|
|
421
|
+
// Get project ID for paths index cleanup
|
|
422
|
+
const projectId = await getProjectId(localPath);
|
|
423
|
+
|
|
454
424
|
// Remove from control plane if managed
|
|
455
425
|
if (project.remote?.projectId) {
|
|
456
426
|
try {
|
|
@@ -462,13 +432,23 @@ export async function removeProject(name: string): Promise<{
|
|
|
462
432
|
}
|
|
463
433
|
}
|
|
464
434
|
|
|
465
|
-
// Remove
|
|
466
|
-
if (project.sources.
|
|
435
|
+
// Remove local .jack/ directory
|
|
436
|
+
if (project.sources.filesystem && project.localPath) {
|
|
437
|
+
try {
|
|
438
|
+
await unlinkProject(project.localPath);
|
|
439
|
+
removed.push("local link (.jack/)");
|
|
440
|
+
} catch (error) {
|
|
441
|
+
errors.push(`Failed to remove local link: ${error}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Remove from paths index
|
|
446
|
+
if (projectId) {
|
|
467
447
|
try {
|
|
468
|
-
await
|
|
469
|
-
removed.push("
|
|
448
|
+
await unregisterPath(projectId, localPath);
|
|
449
|
+
removed.push("paths index");
|
|
470
450
|
} catch (error) {
|
|
471
|
-
errors.push(`Failed to remove from
|
|
451
|
+
errors.push(`Failed to remove from paths index: ${error}`);
|
|
472
452
|
}
|
|
473
453
|
}
|
|
474
454
|
|