jamdesk 1.1.39 → 1.1.41

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 (55) hide show
  1. package/dist/__tests__/unit/clean.test.d.ts +2 -0
  2. package/dist/__tests__/unit/clean.test.d.ts.map +1 -0
  3. package/dist/__tests__/unit/clean.test.js +59 -0
  4. package/dist/__tests__/unit/clean.test.js.map +1 -0
  5. package/dist/__tests__/unit/dev-cache-cleanup.test.d.ts +2 -0
  6. package/dist/__tests__/unit/dev-cache-cleanup.test.d.ts.map +1 -0
  7. package/dist/__tests__/unit/dev-cache-cleanup.test.js +74 -0
  8. package/dist/__tests__/unit/dev-cache-cleanup.test.js.map +1 -0
  9. package/dist/__tests__/unit/dev-lock.test.d.ts +2 -0
  10. package/dist/__tests__/unit/dev-lock.test.d.ts.map +1 -0
  11. package/dist/__tests__/unit/dev-lock.test.js +70 -0
  12. package/dist/__tests__/unit/dev-lock.test.js.map +1 -0
  13. package/dist/__tests__/unit/dev-sync-vendored.test.d.ts +2 -0
  14. package/dist/__tests__/unit/dev-sync-vendored.test.d.ts.map +1 -0
  15. package/dist/__tests__/unit/dev-sync-vendored.test.js +58 -0
  16. package/dist/__tests__/unit/dev-sync-vendored.test.js.map +1 -0
  17. package/dist/__tests__/unit/run-build-script.test.d.ts +2 -0
  18. package/dist/__tests__/unit/run-build-script.test.d.ts.map +1 -0
  19. package/dist/__tests__/unit/run-build-script.test.js +58 -0
  20. package/dist/__tests__/unit/run-build-script.test.js.map +1 -0
  21. package/dist/__tests__/unit/workspace-paths.test.d.ts +2 -0
  22. package/dist/__tests__/unit/workspace-paths.test.d.ts.map +1 -0
  23. package/dist/__tests__/unit/workspace-paths.test.js +104 -0
  24. package/dist/__tests__/unit/workspace-paths.test.js.map +1 -0
  25. package/dist/commands/clean.d.ts +17 -2
  26. package/dist/commands/clean.d.ts.map +1 -1
  27. package/dist/commands/clean.js +47 -27
  28. package/dist/commands/clean.js.map +1 -1
  29. package/dist/commands/dev.d.ts +17 -0
  30. package/dist/commands/dev.d.ts.map +1 -1
  31. package/dist/commands/dev.js +116 -95
  32. package/dist/commands/dev.js.map +1 -1
  33. package/dist/index.js +9 -10
  34. package/dist/index.js.map +1 -1
  35. package/dist/lib/deps.d.ts +0 -4
  36. package/dist/lib/deps.d.ts.map +1 -1
  37. package/dist/lib/deps.js +0 -6
  38. package/dist/lib/deps.js.map +1 -1
  39. package/dist/lib/dev-lock.d.ts +15 -0
  40. package/dist/lib/dev-lock.d.ts.map +1 -0
  41. package/dist/lib/dev-lock.js +95 -0
  42. package/dist/lib/dev-lock.js.map +1 -0
  43. package/dist/lib/run-build-script.d.ts +13 -0
  44. package/dist/lib/run-build-script.d.ts.map +1 -0
  45. package/dist/lib/run-build-script.js +36 -0
  46. package/dist/lib/run-build-script.js.map +1 -0
  47. package/dist/lib/safe-fs.d.ts +18 -0
  48. package/dist/lib/safe-fs.d.ts.map +1 -0
  49. package/dist/lib/safe-fs.js +55 -0
  50. package/dist/lib/safe-fs.js.map +1 -0
  51. package/dist/lib/workspace-paths.d.ts +37 -0
  52. package/dist/lib/workspace-paths.d.ts.map +1 -0
  53. package/dist/lib/workspace-paths.js +64 -0
  54. package/dist/lib/workspace-paths.js.map +1 -0
  55. package/package.json +1 -1
@@ -1,49 +1,69 @@
1
1
  /**
2
2
  * Clean Command
3
3
  *
4
- * Clears the ~/.jamdesk cache directory.
4
+ * Default: clears the current project's workspace
5
+ * (`<jamdeskDir>/workspaces/<slug>/`).
6
+ *
7
+ * `--all`: clears everything under `<jamdeskDir>/`, including shared
8
+ * `node_modules` (next dev will reinstall on first run).
9
+ *
10
+ * Both modes use safeRemoveCache so a still-running dev session in the
11
+ * target tree surfaces a clear "another dev is running" message rather
12
+ * than crashing ENOTEMPTY.
5
13
  */
6
14
  import fs from 'fs-extra';
7
- import path from 'path';
8
- import { homedir } from 'os';
9
15
  import { output } from '../lib/output.js';
10
16
  import { spinner } from '../lib/spinner.js';
11
- export async function clean() {
12
- const jamdeskDir = path.join(homedir(), '.jamdesk');
13
- if (!fs.existsSync(jamdeskDir)) {
14
- output.info('Nothing to clean - ~/.jamdesk does not exist');
17
+ import { getJamdeskDir } from '../lib/deps.js';
18
+ import { getProjectWorkspaceDir } from '../lib/workspace-paths.js';
19
+ import { safeRemoveCache } from '../lib/safe-fs.js';
20
+ export async function clean(options = {}, ctx = { jamdeskDir: getJamdeskDir(), projectDir: process.cwd() }) {
21
+ const { jamdeskDir, projectDir } = ctx;
22
+ if (options.all) {
23
+ if (!fs.existsSync(jamdeskDir)) {
24
+ output.info(`Nothing to clean — ${jamdeskDir} does not exist`);
25
+ return;
26
+ }
27
+ const spin = spinner('Calculating cache size...');
28
+ const size = await getDirectorySize(jamdeskDir);
29
+ spin.text = `Removing ${jamdeskDir} (${formatBytes(size)})...`;
30
+ await safeRemoveCache(jamdeskDir);
31
+ spin.succeed(`Cleared ${formatBytes(size)} of cache`);
32
+ console.log('\nCleared:');
33
+ console.log(` - ${jamdeskDir}/workspaces/ (all per-project caches)`);
34
+ console.log(` - ${jamdeskDir}/node_modules/ (installed dependencies)`);
35
+ console.log(` - ${jamdeskDir}/version-cache.json (update check cache)`);
36
+ console.log('\nNote: any currently-running `jamdesk dev` sessions will fail until you restart them.');
37
+ console.log('\nNext `jamdesk dev` will reinstall dependencies.');
38
+ return;
39
+ }
40
+ const workspaceDir = getProjectWorkspaceDir(jamdeskDir, projectDir);
41
+ if (!fs.existsSync(workspaceDir)) {
42
+ output.info(`Nothing to clean — no workspace at ${workspaceDir}`);
15
43
  return;
16
44
  }
17
- const spin = spinner('Calculating cache size...');
18
- // Calculate total size before cleaning
19
- const size = await getDirectorySize(jamdeskDir);
20
- spin.text = `Removing ~/.jamdesk (${formatBytes(size)})...`;
21
- // Remove the entire directory
22
- await fs.remove(jamdeskDir);
23
- spin.succeed(`Cleared ${formatBytes(size)} of cache`);
24
- console.log('\nCleared:');
25
- console.log(' - ~/.jamdesk/workspace/ (Build cache)');
26
- console.log(' - ~/.jamdesk/node_modules/ (installed dependencies)');
27
- console.log(' - ~/.jamdesk/version-cache.json (update check cache)');
28
- console.log('\nNext `jamdesk dev` will reinstall dependencies.');
45
+ const spin = spinner('Calculating workspace size...');
46
+ const size = await getDirectorySize(workspaceDir);
47
+ spin.text = `Removing ${workspaceDir} (${formatBytes(size)})...`;
48
+ await safeRemoveCache(workspaceDir);
49
+ spin.succeed(`Cleared ${formatBytes(size)} from this project's workspace`);
50
+ console.log(`\nNext \`jamdesk dev\` in this directory will recompile from scratch.`);
51
+ console.log(`Use \`jamdesk clean --all\` to also wipe shared node_modules.`);
29
52
  }
30
53
  async function getDirectorySize(dir) {
31
54
  let size = 0;
32
55
  try {
33
56
  const files = await fs.readdir(dir, { withFileTypes: true });
34
57
  for (const file of files) {
35
- const filePath = path.join(dir, file.name);
36
- if (file.isDirectory()) {
58
+ const filePath = `${dir}/${file.name}`;
59
+ if (file.isDirectory())
37
60
  size += await getDirectorySize(filePath);
38
- }
39
- else {
40
- const stats = await fs.stat(filePath);
41
- size += stats.size;
42
- }
61
+ else
62
+ size += (await fs.stat(filePath)).size;
43
63
  }
44
64
  }
45
65
  catch {
46
- // Ignore errors (permission issues, etc.)
66
+ // ignore (permissions, vanished entries)
47
67
  }
48
68
  return size;
49
69
  }
@@ -1 +1 @@
1
- {"version":3,"file":"clean.js","sourceRoot":"","sources":["../../src/commands/clean.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,MAAM,UAAU,CAAC;AAC1B,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAC7B,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAE5C,MAAM,CAAC,KAAK,UAAU,KAAK;IACzB,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,UAAU,CAAC,CAAC;IAEpD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/B,MAAM,CAAC,IAAI,CAAC,8CAA8C,CAAC,CAAC;QAC5D,OAAO;IACT,CAAC;IAED,MAAM,IAAI,GAAG,OAAO,CAAC,2BAA2B,CAAC,CAAC;IAElD,uCAAuC;IACvC,MAAM,IAAI,GAAG,MAAM,gBAAgB,CAAC,UAAU,CAAC,CAAC;IAEhD,IAAI,CAAC,IAAI,GAAG,wBAAwB,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC;IAE5D,8BAA8B;IAC9B,MAAM,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IAE5B,IAAI,CAAC,OAAO,CAAC,WAAW,WAAW,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IAEtD,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IAC1B,OAAO,CAAC,GAAG,CAAC,yCAAyC,CAAC,CAAC;IACvD,OAAO,CAAC,GAAG,CAAC,uDAAuD,CAAC,CAAC;IACrE,OAAO,CAAC,GAAG,CAAC,wDAAwD,CAAC,CAAC;IACtE,OAAO,CAAC,GAAG,CAAC,mDAAmD,CAAC,CAAC;AACnE,CAAC;AAED,KAAK,UAAU,gBAAgB,CAAC,GAAW;IACzC,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7D,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;YAC3C,IAAI,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;gBACvB,IAAI,IAAI,MAAM,gBAAgB,CAAC,QAAQ,CAAC,CAAC;YAC3C,CAAC;iBAAM,CAAC;gBACN,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;gBACtC,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC;YACrB,CAAC;QACH,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,0CAA0C;IAC5C,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,WAAW,CAAC,KAAa;IAChC,IAAI,KAAK,GAAG,IAAI;QAAE,OAAO,GAAG,KAAK,IAAI,CAAC;IACtC,IAAI,KAAK,GAAG,IAAI,GAAG,IAAI;QAAE,OAAO,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC;IAClE,IAAI,KAAK,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI;QAAE,OAAO,GAAG,CAAC,KAAK,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC;IAClF,OAAO,GAAG,CAAC,KAAK,GAAG,CAAC,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC;AAC3D,CAAC"}
1
+ {"version":3,"file":"clean.js","sourceRoot":"","sources":["../../src/commands/clean.ts"],"names":[],"mappings":"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"}
@@ -27,5 +27,22 @@ export interface DevOptions {
27
27
  webpack?: boolean;
28
28
  clean?: boolean;
29
29
  }
30
+ /**
31
+ * Sync vendored files to workspace, only copying files that changed.
32
+ * This preserves file timestamps for unchanged files, allowing Turbopack
33
+ * to reuse its cache and avoid 35s recompilation on every start.
34
+ */
35
+ export declare function syncVendoredFiles(srcDir: string, destDir: string, filter: (relativePath: string) => boolean): Promise<{
36
+ copied: number;
37
+ skipped: number;
38
+ removed: number;
39
+ }>;
40
+ /**
41
+ * Validates the .next cache and cleans it if corrupted or stale.
42
+ * Returns true if cache is valid and usable, false if it was cleared.
43
+ *
44
+ * Exported for unit testing.
45
+ */
46
+ export declare function validateAndCleanCache(nextCacheDir: string, cliVersionFile: string, workspaceDir: string, verbose: boolean): Promise<boolean>;
30
47
  export declare function dev(options: DevOptions): Promise<void>;
31
48
  //# sourceMappingURL=dev.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/commands/dev.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAqBH;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAO/E;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAsQD,wBAAsB,GAAG,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA2sB5D"}
1
+ {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/commands/dev.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAyBH;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAO/E;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAiJD;;;;GAIG;AAGH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,GACxC,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAuE/D;AAED;;;;;GAKG;AACH,wBAAsB,qBAAqB,CACzC,YAAY,EAAE,MAAM,EACpB,cAAc,EAAE,MAAM,EACtB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,OAAO,GACf,OAAO,CAAC,OAAO,CAAC,CA6BlB;AAED,wBAAsB,GAAG,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAqtB5D"}
@@ -5,17 +5,21 @@
5
5
  * Wraps the dev-project.cjs logic from build-service.
6
6
  */
7
7
  import { execSync, spawn } from 'child_process';
8
+ import { runBuildScript } from '../lib/run-build-script.js';
8
9
  import fs from 'fs-extra';
9
10
  import path from 'path';
10
11
  import { createRequire } from 'module';
11
12
  import { output } from '../lib/output.js';
12
13
  import { spinner } from '../lib/spinner.js';
13
- import { ensureDependencies, getWorkspaceDir, getJamdeskDir } from '../lib/deps.js';
14
+ import { ensureDependencies, getJamdeskDir } from '../lib/deps.js';
15
+ import { getProjectWorkspaceDir, migrateLegacyWorkspace } from '../lib/workspace-paths.js';
16
+ import { acquireDevLock, releaseDevLock } from '../lib/dev-lock.js';
14
17
  import { validateOpenApiSpec, formatErrorForCli, clearSpecCache } from '../lib/openapi.js';
15
18
  import { validateConfig } from '../lib/docs-config.js';
16
19
  import { validateBrandingAssets } from '../lib/validate-branding.js';
17
20
  import { findAvailablePort } from '../lib/port.js';
18
21
  import { createLoadingServer } from '../lib/dev-loading-server.js';
22
+ import { safeRemoveCache } from '../lib/safe-fs.js';
19
23
  import { fileURLToPath } from 'url';
20
24
  const __filename = fileURLToPath(import.meta.url);
21
25
  const __dirname = path.dirname(__filename);
@@ -151,7 +155,9 @@ async function syncDirectory(srcDir, destDir) {
151
155
  * This preserves file timestamps for unchanged files, allowing Turbopack
152
156
  * to reuse its cache and avoid 35s recompilation on every start.
153
157
  */
154
- async function syncVendoredFiles(srcDir, destDir, filter) {
158
+ // Exported for unit testing — verifies the skip list preserves CLI metadata
159
+ // like the dev lockfile, which acquireDevLock writes before this runs.
160
+ export async function syncVendoredFiles(srcDir, destDir, filter) {
155
161
  const stats = { copied: 0, skipped: 0, removed: 0 };
156
162
  // Build set of source files (relative paths)
157
163
  const sourceFiles = new Set();
@@ -188,9 +194,14 @@ async function syncVendoredFiles(srcDir, destDir, filter) {
188
194
  // - node_modules: managed separately via NODE_PATH
189
195
  // - projects: symlink to user's project
190
196
  // - public: contains project-specific files added after sync (docs.json, images)
197
+ // Plus any CLI-managed dotfile written before sync (e.g. the dev lockfile
198
+ // acquireDevLock writes at workspace root) — don't sweep our own metadata.
191
199
  if (entry.name === '.next' || entry.name === 'node_modules' || entry.name === 'projects' || entry.name === 'public') {
192
200
  continue;
193
201
  }
202
+ if (entry.name === '.jamdesk-dev.lock' || entry.name === '.content-hash') {
203
+ continue;
204
+ }
194
205
  const destPath = path.join(dir, entry.name);
195
206
  if (entry.isDirectory()) {
196
207
  await walkDest(destPath, relativePath);
@@ -212,8 +223,10 @@ async function syncVendoredFiles(srcDir, destDir, filter) {
212
223
  /**
213
224
  * Validates the .next cache and cleans it if corrupted or stale.
214
225
  * Returns true if cache is valid and usable, false if it was cleared.
226
+ *
227
+ * Exported for unit testing.
215
228
  */
216
- async function validateAndCleanCache(nextCacheDir, cliVersionFile, workspaceDir, verbose) {
229
+ export async function validateAndCleanCache(nextCacheDir, cliVersionFile, workspaceDir, verbose) {
217
230
  const cachedVersion = fs.existsSync(cliVersionFile)
218
231
  ? fs.readFileSync(cliVersionFile, 'utf-8').trim()
219
232
  : null;
@@ -222,7 +235,7 @@ async function validateAndCleanCache(nextCacheDir, cliVersionFile, workspaceDir,
222
235
  if (cachedVersion !== currentVersion) {
223
236
  if (verbose)
224
237
  output.info(`CLI version changed (${cachedVersion} → ${currentVersion}), clearing cache`);
225
- await fs.remove(nextCacheDir);
238
+ await safeRemoveCache(nextCacheDir);
226
239
  return false;
227
240
  }
228
241
  // Check for Turbopack cache corruption (workspace deleted while dev server running)
@@ -230,7 +243,7 @@ async function validateAndCleanCache(nextCacheDir, cliVersionFile, workspaceDir,
230
243
  const appDir = path.join(workspaceDir, 'app');
231
244
  if (fs.existsSync(turbopackCache) && !fs.existsSync(appDir)) {
232
245
  output.warn('Detected corrupted cache, clearing...');
233
- await fs.remove(nextCacheDir);
246
+ await safeRemoveCache(nextCacheDir);
234
247
  return false;
235
248
  }
236
249
  // Remove stale lock file from previous runs (prevents "another dev server" warning)
@@ -351,7 +364,38 @@ export async function dev(options) {
351
364
  // Step 2: Prepare workspace
352
365
  spin = spinner('Preparing workspace...');
353
366
  const jamdeskDir = getJamdeskDir();
354
- const workspaceDir = getWorkspaceDir();
367
+ // Drop the legacy single-workspace layout (CLI ≤1.1.40). safeRemoveCache
368
+ // surfaces a friendly message if a 1.1.40 dev session is still holding it.
369
+ if (await migrateLegacyWorkspace(jamdeskDir)) {
370
+ if (verbose)
371
+ output.info('Migrated legacy ~/.jamdesk/workspace/ to per-project layout');
372
+ }
373
+ const workspaceDir = getProjectWorkspaceDir(jamdeskDir, projectDir);
374
+ // Same-project guard: refuse to start if another dev is using this workspace.
375
+ // Acquired BEFORE --clean so a concurrent `dev --clean` can't wipe our
376
+ // lockfile out from under us.
377
+ const lock = acquireDevLock(workspaceDir);
378
+ if (!lock.acquired) {
379
+ spin.fail('Another `jamdesk dev` is already running for this project');
380
+ output.error(` PID ${lock.heldBy} holds the lock at ${path.join(workspaceDir, '.jamdesk-dev.lock')}.\n` +
381
+ ` Stop it first (Ctrl-C in that terminal, or kill ${lock.heldBy}), then retry.`);
382
+ process.exit(1);
383
+ }
384
+ // Release the lock on any normal exit path. SIGINT/SIGTERM additionally
385
+ // run `cleanup` further below, which calls release() before tree-killing
386
+ // the spawned next dev process group.
387
+ const release = () => releaseDevLock(workspaceDir);
388
+ process.once('exit', release);
389
+ // Handle --clean: wipe workspace contents but preserve the lockfile we just
390
+ // acquired. safeRemoveCache surfaces the friendly message if any path is
391
+ // still held open.
392
+ if (options.clean) {
393
+ output.info('Clearing workspace cache...');
394
+ const entries = await fs.readdir(workspaceDir);
395
+ await Promise.all(entries
396
+ .filter((name) => name !== '.jamdesk-dev.lock')
397
+ .map((name) => safeRemoveCache(path.join(workspaceDir, name))));
398
+ }
355
399
  const vendoredDir = path.join(__dirname, '../../vendored');
356
400
  // Check if vendored directory exists
357
401
  if (!fs.existsSync(vendoredDir)) {
@@ -363,58 +407,46 @@ export async function dev(options) {
363
407
  // Only remove and recopy vendored files, keeping the .next directory
364
408
  const nextCacheDir = path.join(workspaceDir, '.next');
365
409
  const cliVersionFile = path.join(nextCacheDir, '.jamdesk-cli-version');
366
- // Handle --clean flag: remove entire workspace to start fresh
367
- if (options.clean && fs.existsSync(workspaceDir)) {
368
- output.info('Clearing workspace cache...');
369
- await fs.remove(workspaceDir);
370
- }
371
- // Determine if we have usable cached compilation
372
- let hadCache = fs.existsSync(nextCacheDir);
373
- if (hadCache) {
374
- hadCache = await validateAndCleanCache(nextCacheDir, cliVersionFile, workspaceDir, verbose);
375
- }
410
+ const hadCache = fs.existsSync(nextCacheDir)
411
+ && await validateAndCleanCache(nextCacheDir, cliVersionFile, workspaceDir, verbose);
376
412
  // Ensure workspace directory exists
377
413
  await fs.ensureDir(workspaceDir);
378
414
  // Sync vendored files - only copy files that actually changed
379
415
  // This preserves timestamps for unchanged files, allowing Turbopack to reuse cache
380
416
  const syncStats = await syncVendoredFiles(vendoredDir, workspaceDir, (relativePath) => !relativePath.includes('node_modules') && !relativePath.includes('.next'));
381
- // Log sync stats in verbose mode only (internal detail)
382
417
  if (verbose && syncStats.copied > 0) {
383
418
  output.info(`Synced ${syncStats.copied} build files (${syncStats.skipped} unchanged)`);
384
419
  }
385
- // Use NODE_PATH instead of symlink for node_modules
386
- // Symlinks break Turbopack with "Symlink node_modules is invalid" error
387
- // NODE_PATH tells Node.js where to find modules without needing a symlink
388
- const jamdeskNodeModules = path.join(path.dirname(workspaceDir), 'node_modules');
389
- // Clean up any stale node_modules symlink (Turbopack doesn't allow external symlinks)
420
+ // Turbopack rejects external symlinks (`Symlink node_modules is invalid`),
421
+ // so use NODE_PATH=depsDir below instead and clear any stale symlink here.
390
422
  const workspaceNodeModules = path.join(workspaceDir, 'node_modules');
391
- if (fs.existsSync(workspaceNodeModules)) {
423
+ try {
392
424
  const stats = await fs.lstat(workspaceNodeModules);
393
- if (stats.isSymbolicLink()) {
425
+ if (stats.isSymbolicLink())
394
426
  await fs.remove(workspaceNodeModules);
395
- }
427
+ }
428
+ catch (err) {
429
+ if (err.code !== 'ENOENT')
430
+ throw err;
396
431
  }
397
432
  // Symlink public directory to parent so Turbopack can find static files
398
433
  // (outputFileTracingRoot and turbopack.root are set to parent directory)
399
434
  const parentPublic = path.join(jamdeskDir, 'public');
400
435
  const workspacePublic = path.join(workspaceDir, 'public');
401
436
  await fs.ensureDir(workspacePublic);
402
- // Only recreate symlink if target changed
437
+ // Only recreate symlink if target changed (preserves Turbopack cache mtimes)
403
438
  let needsPublicSymlink = true;
404
- if (fs.existsSync(parentPublic)) {
405
- try {
406
- const stats = await fs.lstat(parentPublic);
407
- if (stats.isSymbolicLink()) {
408
- const currentTarget = await fs.readlink(parentPublic);
409
- if (currentTarget === workspacePublic) {
410
- needsPublicSymlink = false;
411
- }
412
- else {
413
- await fs.remove(parentPublic);
414
- }
415
- }
439
+ try {
440
+ const stats = await fs.lstat(parentPublic);
441
+ if (stats.isSymbolicLink() && (await fs.readlink(parentPublic)) === workspacePublic) {
442
+ needsPublicSymlink = false;
416
443
  }
417
- catch {
444
+ else {
445
+ await fs.remove(parentPublic);
446
+ }
447
+ }
448
+ catch (err) {
449
+ if (err.code !== 'ENOENT') {
418
450
  await fs.remove(parentPublic);
419
451
  }
420
452
  }
@@ -427,28 +459,23 @@ export async function dev(options) {
427
459
  const workspaceProjectsDir = path.join(workspaceDir, 'projects');
428
460
  const workspaceProjectDir = path.join(workspaceProjectsDir, projectName);
429
461
  await fs.ensureDir(workspaceProjectsDir);
430
- // Only recreate symlink if target changed (preserves timestamps for Turbopack cache)
431
462
  let needsSymlink = true;
432
- if (fs.existsSync(workspaceProjectDir)) {
433
- try {
434
- const currentTarget = await fs.readlink(workspaceProjectDir);
435
- if (currentTarget === projectDir) {
436
- needsSymlink = false; // Symlink already points to correct location
437
- }
438
- else {
439
- await fs.remove(workspaceProjectDir);
440
- }
463
+ try {
464
+ if ((await fs.readlink(workspaceProjectDir)) === projectDir) {
465
+ needsSymlink = false;
441
466
  }
442
- catch {
443
- // Not a symlink or can't read - remove and recreate
467
+ else {
468
+ await fs.remove(workspaceProjectDir);
469
+ }
470
+ }
471
+ catch (err) {
472
+ if (err.code !== 'ENOENT') {
444
473
  await fs.remove(workspaceProjectDir);
445
474
  }
446
475
  }
447
476
  if (needsSymlink) {
448
477
  await fs.symlink(projectDir, workspaceProjectDir, 'junction');
449
478
  }
450
- // Copy project's docs.json to workspace/public/ (only if changed)
451
- await fs.ensureDir(path.join(workspaceDir, 'public'));
452
479
  await copyIfChanged(docsJsonPath, path.join(workspaceDir, 'public/docs.json'));
453
480
  spin.succeed('Workspace prepared');
454
481
  // Step 3: Copy project assets (images, etc.) - only copy changed files
@@ -500,30 +527,31 @@ export async function dev(options) {
500
527
  // Compute content hash to detect changes (based on file mtimes for speed)
501
528
  const crypto = await import('crypto');
502
529
  const { glob } = await import('glob');
530
+ const statMtime = async (file, root) => {
531
+ try {
532
+ const stat = await fs.stat(path.join(root, file));
533
+ return { key: file, mtimeMs: stat.mtimeMs };
534
+ }
535
+ catch {
536
+ return null;
537
+ }
538
+ };
503
539
  const computeContentHash = async () => {
504
540
  const hash = crypto.createHash('md5');
505
- // Hash docs.json content
506
541
  hash.update(await fs.readFile(docsJsonPath, 'utf-8'));
507
- // Glob only MDX/MD files instead of recursive readdir on entire project
508
- // (readdir walks node_modules, .git, images thousands of irrelevant entries)
509
- const mdxFiles = await glob('**/*.{mdx,md}', { cwd: projectDir, nodir: true });
510
- mdxFiles.sort(); // Deterministic order — glob walk order varies between runs
511
- for (const file of mdxFiles) {
512
- try {
513
- const stat = await fs.stat(path.join(projectDir, file));
514
- hash.update(`${file}:${stat.mtimeMs}`);
515
- }
516
- catch { /* skip inaccessible files */ }
542
+ // Glob only MDX/MD files recursive readdir walks node_modules/.git.
543
+ const mdxFiles = (await glob('**/*.{mdx,md}', { cwd: projectDir, nodir: true })).sort();
544
+ const mdxStats = await Promise.all(mdxFiles.map((f) => statMtime(f, projectDir)));
545
+ for (const s of mdxStats) {
546
+ if (s)
547
+ hash.update(`${s.key}:${s.mtimeMs}`);
517
548
  }
518
- // Hash snippets mtimes if they exist
519
549
  if (hasSnippets) {
520
- const snippetFiles = await fs.readdir(snippetsDir);
521
- for (const file of snippetFiles) {
522
- try {
523
- const stat = await fs.stat(path.join(snippetsDir, file));
524
- hash.update(`snippets/${file}:${stat.mtimeMs}`);
525
- }
526
- catch { /* skip */ }
550
+ const snippetFiles = (await fs.readdir(snippetsDir)).sort();
551
+ const snippetStats = await Promise.all(snippetFiles.map((f) => statMtime(f, snippetsDir)));
552
+ for (const s of snippetStats) {
553
+ if (s)
554
+ hash.update(`snippets/${s.key}:${s.mtimeMs}`);
527
555
  }
528
556
  }
529
557
  return hash.digest('hex');
@@ -544,34 +572,26 @@ export async function dev(options) {
544
572
  const contentChanged = currentHash !== cachedHash;
545
573
  if (contentChanged) {
546
574
  spin.text = 'Processing navigation, snippets, and search index...';
547
- // Helper to run a script (uses hardcoded script names, no user input)
548
- const runScript = (script) => {
549
- return new Promise((resolve) => {
550
- try {
551
- execSync(`node scripts/${script}`, {
552
- cwd: workspaceDir,
553
- stdio: verbose ? 'inherit' : 'pipe',
554
- env: { ...env, PROJECT_NAME: path.basename(projectDir) },
555
- });
556
- resolve({ script, success: true });
557
- }
558
- catch (error) {
559
- resolve({ script, success: false, error });
560
- }
561
- });
575
+ const scriptOpts = {
576
+ cwd: workspaceDir,
577
+ env: { ...env, PROJECT_NAME: path.basename(projectDir) },
578
+ verbose,
562
579
  };
563
- // Run all three scripts in parallel
564
580
  const [navResult, snippetsResult, searchResult] = await Promise.all([
565
- runScript('enhance-navigation.cjs'),
566
- runScript('compile-snippets.cjs'),
567
- runScript('build-search-index.cjs'),
581
+ runBuildScript('enhance-navigation.cjs', scriptOpts),
582
+ runBuildScript('compile-snippets.cjs', scriptOpts),
583
+ runBuildScript('build-search-index.cjs', scriptOpts),
568
584
  ]);
569
- // Check for failures
570
585
  const failures = [navResult, snippetsResult, searchResult].filter((r) => !r.success);
571
586
  if (failures.length > 0) {
572
587
  spin.fail('Build step failed');
573
588
  for (const f of failures) {
574
589
  output.error(`Failed: ${f.script}`);
590
+ // Surface child stderr (or stdout) on every failure so non-verbose users
591
+ // see the actual error, not just the script name.
592
+ const tail = (f.stderr || f.stdout || '').trim();
593
+ if (tail)
594
+ console.error(tail.split('\n').slice(-20).join('\n'));
575
595
  if (verbose && f.error)
576
596
  console.error(f.error);
577
597
  }
@@ -634,7 +654,7 @@ export async function dev(options) {
634
654
  console.log(' If it gets stuck at "Starting...", run `jamdesk clean` and restart.\n');
635
655
  // Show cold start warning if no cache
636
656
  if (!hadCache) {
637
- console.log(`\n ⏳ First run - full compile needed (this takes ~30-40s, then it's cached)`);
657
+ console.log(`\n ⏳ First run - full compile needed (this takes a minute, then it's cached)`);
638
658
  }
639
659
  console.log(`\n Access at: http://localhost:${proxyPort}${basePath}/${firstPage}\n`);
640
660
  // Use Turbopack by default (~5x faster than webpack)
@@ -857,6 +877,7 @@ export async function dev(options) {
857
877
  });
858
878
  // Handle Ctrl+C - be aggressive about cleanup
859
879
  function cleanup() {
880
+ release(); // Release the workspace lock first so a subsequent dev isn't blocked.
860
881
  if (compileSpinner) {
861
882
  clearInterval(compileSpinner);
862
883
  process.stdout.write('\r\x1b[K');
@@ -882,7 +903,7 @@ export async function dev(options) {
882
903
  }
883
904
  process.exit(0);
884
905
  }
885
- process.on('SIGINT', cleanup);
886
- process.on('SIGTERM', cleanup);
906
+ process.once('SIGINT', cleanup);
907
+ process.once('SIGTERM', cleanup);
887
908
  }
888
909
  //# sourceMappingURL=dev.js.map