@getjack/jack 0.1.4 → 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/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
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for project-link.ts
|
|
3
|
+
*
|
|
4
|
+
* Tests the .jack/project.json linking system.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
ensureGitignored,
|
|
15
|
+
generateByoProjectId,
|
|
16
|
+
getDeployMode,
|
|
17
|
+
getJackDir,
|
|
18
|
+
getProjectId,
|
|
19
|
+
getProjectLinkPath,
|
|
20
|
+
getTemplatePath,
|
|
21
|
+
isLinked,
|
|
22
|
+
linkProject,
|
|
23
|
+
readProjectLink,
|
|
24
|
+
readTemplateMetadata,
|
|
25
|
+
unlinkProject,
|
|
26
|
+
updateProjectLink,
|
|
27
|
+
writeTemplateMetadata,
|
|
28
|
+
} from "./project-link.ts";
|
|
29
|
+
|
|
30
|
+
let testDir: string;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create a unique temp directory for each test
|
|
34
|
+
*/
|
|
35
|
+
async function createTestDir(): Promise<string> {
|
|
36
|
+
const timestamp = Date.now();
|
|
37
|
+
const random = Math.random().toString(36).substring(7);
|
|
38
|
+
const dir = join(tmpdir(), `jack-link-test-${timestamp}-${random}`);
|
|
39
|
+
await mkdir(dir, { recursive: true });
|
|
40
|
+
return dir;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("project-link", () => {
|
|
44
|
+
beforeEach(async () => {
|
|
45
|
+
testDir = await createTestDir();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
afterEach(async () => {
|
|
49
|
+
await rm(testDir, { recursive: true, force: true });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("path helpers", () => {
|
|
53
|
+
it("getJackDir returns correct path", () => {
|
|
54
|
+
const jackDir = getJackDir(testDir);
|
|
55
|
+
expect(jackDir).toBe(join(testDir, ".jack"));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("getProjectLinkPath returns correct path", () => {
|
|
59
|
+
const linkPath = getProjectLinkPath(testDir);
|
|
60
|
+
expect(linkPath).toBe(join(testDir, ".jack", "project.json"));
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("getTemplatePath returns correct path", () => {
|
|
64
|
+
const templatePath = getTemplatePath(testDir);
|
|
65
|
+
expect(templatePath).toBe(join(testDir, ".jack", "template.json"));
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("generateByoProjectId", () => {
|
|
70
|
+
it("generates unique IDs", () => {
|
|
71
|
+
const id1 = generateByoProjectId();
|
|
72
|
+
const id2 = generateByoProjectId();
|
|
73
|
+
|
|
74
|
+
expect(id1).not.toBe(id2);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("generates IDs with byo_ prefix", () => {
|
|
78
|
+
const id = generateByoProjectId();
|
|
79
|
+
expect(id.startsWith("byo_")).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("generates valid format", () => {
|
|
83
|
+
const id = generateByoProjectId();
|
|
84
|
+
// Format: byo_<uuid-like>
|
|
85
|
+
expect(id.length).toBeGreaterThan(10);
|
|
86
|
+
expect(id).toMatch(/^byo_[a-f0-9-]+$/);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("linkProject", () => {
|
|
91
|
+
it("creates .jack directory and project.json", async () => {
|
|
92
|
+
await linkProject(testDir, "proj_abc123", "managed");
|
|
93
|
+
|
|
94
|
+
const jackDir = getJackDir(testDir);
|
|
95
|
+
const linkPath = getProjectLinkPath(testDir);
|
|
96
|
+
|
|
97
|
+
expect(existsSync(jackDir)).toBe(true);
|
|
98
|
+
expect(existsSync(linkPath)).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("writes correct managed project data", async () => {
|
|
102
|
+
await linkProject(testDir, "proj_abc123", "managed");
|
|
103
|
+
|
|
104
|
+
const link = await readProjectLink(testDir);
|
|
105
|
+
|
|
106
|
+
expect(link).not.toBeNull();
|
|
107
|
+
expect(link?.version).toBe(1);
|
|
108
|
+
expect(link?.project_id).toBe("proj_abc123");
|
|
109
|
+
expect(link?.deploy_mode).toBe("managed");
|
|
110
|
+
expect(link?.linked_at).toBeDefined();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("writes correct BYO project data", async () => {
|
|
114
|
+
const byoId = generateByoProjectId();
|
|
115
|
+
await linkProject(testDir, byoId, "byo");
|
|
116
|
+
|
|
117
|
+
const link = await readProjectLink(testDir);
|
|
118
|
+
|
|
119
|
+
expect(link).not.toBeNull();
|
|
120
|
+
expect(link?.version).toBe(1);
|
|
121
|
+
expect(link?.project_id).toBe(byoId);
|
|
122
|
+
expect(link?.deploy_mode).toBe("byo");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("overwrites existing link", async () => {
|
|
126
|
+
await linkProject(testDir, "proj_old", "managed");
|
|
127
|
+
await linkProject(testDir, "proj_new", "byo");
|
|
128
|
+
|
|
129
|
+
const link = await readProjectLink(testDir);
|
|
130
|
+
|
|
131
|
+
expect(link?.project_id).toBe("proj_new");
|
|
132
|
+
expect(link?.deploy_mode).toBe("byo");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("auto-adds .jack/ to .gitignore", async () => {
|
|
136
|
+
await linkProject(testDir, "proj_abc123", "managed");
|
|
137
|
+
|
|
138
|
+
const gitignorePath = join(testDir, ".gitignore");
|
|
139
|
+
expect(existsSync(gitignorePath)).toBe(true);
|
|
140
|
+
|
|
141
|
+
const content = await readFile(gitignorePath, "utf-8");
|
|
142
|
+
expect(content).toContain(".jack/");
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("unlinkProject", () => {
|
|
147
|
+
it("removes .jack directory", async () => {
|
|
148
|
+
await linkProject(testDir, "proj_abc123", "managed");
|
|
149
|
+
expect(existsSync(getJackDir(testDir))).toBe(true);
|
|
150
|
+
|
|
151
|
+
await unlinkProject(testDir);
|
|
152
|
+
expect(existsSync(getJackDir(testDir))).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("handles non-existent .jack directory gracefully", async () => {
|
|
156
|
+
// Should not throw
|
|
157
|
+
await unlinkProject(testDir);
|
|
158
|
+
expect(existsSync(getJackDir(testDir))).toBe(false);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("removes template.json along with project.json", async () => {
|
|
162
|
+
await linkProject(testDir, "proj_abc123", "managed");
|
|
163
|
+
await writeTemplateMetadata(testDir, { type: "builtin", name: "miniapp" });
|
|
164
|
+
|
|
165
|
+
expect(existsSync(getTemplatePath(testDir))).toBe(true);
|
|
166
|
+
|
|
167
|
+
await unlinkProject(testDir);
|
|
168
|
+
expect(existsSync(getTemplatePath(testDir))).toBe(false);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("readProjectLink", () => {
|
|
173
|
+
it("returns null for non-existent directory", async () => {
|
|
174
|
+
const link = await readProjectLink(join(testDir, "nonexistent"));
|
|
175
|
+
expect(link).toBeNull();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("returns null for directory without .jack", async () => {
|
|
179
|
+
const link = await readProjectLink(testDir);
|
|
180
|
+
expect(link).toBeNull();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("returns null for invalid JSON", async () => {
|
|
184
|
+
const jackDir = getJackDir(testDir);
|
|
185
|
+
await mkdir(jackDir, { recursive: true });
|
|
186
|
+
await writeFile(getProjectLinkPath(testDir), "{ invalid json");
|
|
187
|
+
|
|
188
|
+
const link = await readProjectLink(testDir);
|
|
189
|
+
expect(link).toBeNull();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("returns null for missing required fields", async () => {
|
|
193
|
+
const jackDir = getJackDir(testDir);
|
|
194
|
+
await mkdir(jackDir, { recursive: true });
|
|
195
|
+
await writeFile(getProjectLinkPath(testDir), JSON.stringify({ version: 1 }));
|
|
196
|
+
|
|
197
|
+
const link = await readProjectLink(testDir);
|
|
198
|
+
expect(link).toBeNull();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("returns valid link data", async () => {
|
|
202
|
+
await linkProject(testDir, "proj_abc123", "managed");
|
|
203
|
+
|
|
204
|
+
const link = await readProjectLink(testDir);
|
|
205
|
+
|
|
206
|
+
expect(link).not.toBeNull();
|
|
207
|
+
expect(link?.project_id).toBe("proj_abc123");
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe("isLinked", () => {
|
|
212
|
+
it("returns false for unlinked directory", async () => {
|
|
213
|
+
const linked = await isLinked(testDir);
|
|
214
|
+
expect(linked).toBe(false);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("returns true for linked directory", async () => {
|
|
218
|
+
await linkProject(testDir, "proj_abc123", "managed");
|
|
219
|
+
|
|
220
|
+
const linked = await isLinked(testDir);
|
|
221
|
+
expect(linked).toBe(true);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("returns false for corrupted link", async () => {
|
|
225
|
+
const jackDir = getJackDir(testDir);
|
|
226
|
+
await mkdir(jackDir, { recursive: true });
|
|
227
|
+
await writeFile(getProjectLinkPath(testDir), "not json");
|
|
228
|
+
|
|
229
|
+
const linked = await isLinked(testDir);
|
|
230
|
+
expect(linked).toBe(false);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe("getProjectId", () => {
|
|
235
|
+
it("returns null for unlinked directory", async () => {
|
|
236
|
+
const id = await getProjectId(testDir);
|
|
237
|
+
expect(id).toBeNull();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("returns project ID for linked directory", async () => {
|
|
241
|
+
await linkProject(testDir, "proj_xyz789", "managed");
|
|
242
|
+
|
|
243
|
+
const id = await getProjectId(testDir);
|
|
244
|
+
expect(id).toBe("proj_xyz789");
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe("getDeployMode", () => {
|
|
249
|
+
it("returns 'byo' for unlinked directory (default)", async () => {
|
|
250
|
+
const mode = await getDeployMode(testDir);
|
|
251
|
+
expect(mode).toBe("byo");
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("returns 'managed' for managed project", async () => {
|
|
255
|
+
await linkProject(testDir, "proj_abc123", "managed");
|
|
256
|
+
|
|
257
|
+
const mode = await getDeployMode(testDir);
|
|
258
|
+
expect(mode).toBe("managed");
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("returns 'byo' for BYO project", async () => {
|
|
262
|
+
await linkProject(testDir, "byo_123", "byo");
|
|
263
|
+
|
|
264
|
+
const mode = await getDeployMode(testDir);
|
|
265
|
+
expect(mode).toBe("byo");
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe("ensureGitignored", () => {
|
|
270
|
+
it("creates .gitignore if not exists", async () => {
|
|
271
|
+
await ensureGitignored(testDir);
|
|
272
|
+
|
|
273
|
+
const gitignorePath = join(testDir, ".gitignore");
|
|
274
|
+
expect(existsSync(gitignorePath)).toBe(true);
|
|
275
|
+
|
|
276
|
+
const content = await readFile(gitignorePath, "utf-8");
|
|
277
|
+
expect(content).toContain(".jack/");
|
|
278
|
+
expect(content).toContain("# Jack project link");
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("appends to existing .gitignore", async () => {
|
|
282
|
+
const gitignorePath = join(testDir, ".gitignore");
|
|
283
|
+
await writeFile(gitignorePath, "node_modules/\n");
|
|
284
|
+
|
|
285
|
+
await ensureGitignored(testDir);
|
|
286
|
+
|
|
287
|
+
const content = await readFile(gitignorePath, "utf-8");
|
|
288
|
+
expect(content).toContain("node_modules/");
|
|
289
|
+
expect(content).toContain(".jack/");
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("does not duplicate .jack/ entry", async () => {
|
|
293
|
+
await ensureGitignored(testDir);
|
|
294
|
+
await ensureGitignored(testDir); // Call twice
|
|
295
|
+
|
|
296
|
+
const gitignorePath = join(testDir, ".gitignore");
|
|
297
|
+
const content = await readFile(gitignorePath, "utf-8");
|
|
298
|
+
|
|
299
|
+
// Count occurrences of .jack/
|
|
300
|
+
const matches = content.match(/\.jack\//g) || [];
|
|
301
|
+
expect(matches.length).toBe(1);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("recognizes .jack without trailing slash", async () => {
|
|
305
|
+
const gitignorePath = join(testDir, ".gitignore");
|
|
306
|
+
await writeFile(gitignorePath, ".jack\n");
|
|
307
|
+
|
|
308
|
+
await ensureGitignored(testDir);
|
|
309
|
+
|
|
310
|
+
const content = await readFile(gitignorePath, "utf-8");
|
|
311
|
+
// Should not add another entry since .jack is present
|
|
312
|
+
const matches = content.match(/\.jack/g) || [];
|
|
313
|
+
expect(matches.length).toBe(1);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("handles .gitignore without trailing newline", async () => {
|
|
317
|
+
const gitignorePath = join(testDir, ".gitignore");
|
|
318
|
+
await writeFile(gitignorePath, "node_modules/"); // No trailing newline
|
|
319
|
+
|
|
320
|
+
await ensureGitignored(testDir);
|
|
321
|
+
|
|
322
|
+
const content = await readFile(gitignorePath, "utf-8");
|
|
323
|
+
expect(content).toContain("node_modules/");
|
|
324
|
+
expect(content).toContain(".jack/");
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
describe("writeTemplateMetadata", () => {
|
|
329
|
+
it("creates template.json for builtin template", async () => {
|
|
330
|
+
await writeTemplateMetadata(testDir, { type: "builtin", name: "miniapp" });
|
|
331
|
+
|
|
332
|
+
const templatePath = getTemplatePath(testDir);
|
|
333
|
+
expect(existsSync(templatePath)).toBe(true);
|
|
334
|
+
|
|
335
|
+
const template = await readTemplateMetadata(testDir);
|
|
336
|
+
expect(template).toEqual({ type: "builtin", name: "miniapp" });
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("creates template.json for github template", async () => {
|
|
340
|
+
await writeTemplateMetadata(testDir, {
|
|
341
|
+
type: "github",
|
|
342
|
+
name: "user/repo",
|
|
343
|
+
ref: "main",
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
const template = await readTemplateMetadata(testDir);
|
|
347
|
+
expect(template).toEqual({ type: "github", name: "user/repo", ref: "main" });
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("creates .jack directory if not exists", async () => {
|
|
351
|
+
await writeTemplateMetadata(testDir, { type: "builtin", name: "api" });
|
|
352
|
+
|
|
353
|
+
expect(existsSync(getJackDir(testDir))).toBe(true);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
describe("readTemplateMetadata", () => {
|
|
358
|
+
it("returns null for non-existent template.json", async () => {
|
|
359
|
+
const template = await readTemplateMetadata(testDir);
|
|
360
|
+
expect(template).toBeNull();
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("returns null for invalid JSON", async () => {
|
|
364
|
+
await mkdir(getJackDir(testDir), { recursive: true });
|
|
365
|
+
await writeFile(getTemplatePath(testDir), "not json");
|
|
366
|
+
|
|
367
|
+
const template = await readTemplateMetadata(testDir);
|
|
368
|
+
expect(template).toBeNull();
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("returns null for missing required fields", async () => {
|
|
372
|
+
await mkdir(getJackDir(testDir), { recursive: true });
|
|
373
|
+
await writeFile(getTemplatePath(testDir), JSON.stringify({ type: "builtin" }));
|
|
374
|
+
|
|
375
|
+
const template = await readTemplateMetadata(testDir);
|
|
376
|
+
expect(template).toBeNull();
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("returns valid template data", async () => {
|
|
380
|
+
await writeTemplateMetadata(testDir, { type: "builtin", name: "hello" });
|
|
381
|
+
|
|
382
|
+
const template = await readTemplateMetadata(testDir);
|
|
383
|
+
expect(template).toEqual({ type: "builtin", name: "hello" });
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
describe("updateProjectLink", () => {
|
|
388
|
+
it("updates existing link", async () => {
|
|
389
|
+
await linkProject(testDir, "proj_abc123", "managed");
|
|
390
|
+
|
|
391
|
+
// Get original linked_at
|
|
392
|
+
const original = await readProjectLink(testDir);
|
|
393
|
+
expect(original).not.toBeNull();
|
|
394
|
+
|
|
395
|
+
// Update with new linked_at
|
|
396
|
+
await updateProjectLink(testDir, { linked_at: "2024-01-01T00:00:00.000Z" });
|
|
397
|
+
|
|
398
|
+
const updated = await readProjectLink(testDir);
|
|
399
|
+
expect(updated?.project_id).toBe("proj_abc123"); // Unchanged
|
|
400
|
+
expect(updated?.deploy_mode).toBe("managed"); // Unchanged
|
|
401
|
+
expect(updated?.linked_at).toBe("2024-01-01T00:00:00.000Z"); // Updated
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it("throws error for unlinked directory", async () => {
|
|
405
|
+
await expect(
|
|
406
|
+
updateProjectLink(testDir, { linked_at: "2024-01-01T00:00:00.000Z" }),
|
|
407
|
+
).rejects.toThrow("Project is not linked");
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it("can update project_id", async () => {
|
|
411
|
+
await linkProject(testDir, "proj_old", "managed");
|
|
412
|
+
await updateProjectLink(testDir, { project_id: "proj_new" });
|
|
413
|
+
|
|
414
|
+
const link = await readProjectLink(testDir);
|
|
415
|
+
expect(link?.project_id).toBe("proj_new");
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it("can update deploy_mode", async () => {
|
|
419
|
+
await linkProject(testDir, "proj_abc123", "managed");
|
|
420
|
+
await updateProjectLink(testDir, { deploy_mode: "byo" });
|
|
421
|
+
|
|
422
|
+
const link = await readProjectLink(testDir);
|
|
423
|
+
expect(link?.deploy_mode).toBe("byo");
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
describe("edge cases", () => {
|
|
428
|
+
it("handles paths with spaces", async () => {
|
|
429
|
+
const spacePath = join(testDir, "path with spaces");
|
|
430
|
+
await mkdir(spacePath, { recursive: true });
|
|
431
|
+
|
|
432
|
+
await linkProject(spacePath, "proj_spaces", "managed");
|
|
433
|
+
|
|
434
|
+
const linked = await isLinked(spacePath);
|
|
435
|
+
expect(linked).toBe(true);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it("handles nested directories", async () => {
|
|
439
|
+
const nestedPath = join(testDir, "a", "b", "c", "project");
|
|
440
|
+
await mkdir(nestedPath, { recursive: true });
|
|
441
|
+
|
|
442
|
+
await linkProject(nestedPath, "proj_nested", "managed");
|
|
443
|
+
|
|
444
|
+
const link = await readProjectLink(nestedPath);
|
|
445
|
+
expect(link?.project_id).toBe("proj_nested");
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it("preserves ISO date format in linked_at", async () => {
|
|
449
|
+
await linkProject(testDir, "proj_abc123", "managed");
|
|
450
|
+
|
|
451
|
+
const link = await readProjectLink(testDir);
|
|
452
|
+
expect(link).not.toBeNull();
|
|
453
|
+
// Should be valid ISO 8601 date
|
|
454
|
+
if (link) {
|
|
455
|
+
expect(new Date(link.linked_at).toISOString()).toBe(link.linked_at);
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
});
|