jamdesk 1.1.40 → 1.1.42

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 (76) 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.js +2 -2
  6. package/dist/__tests__/unit/dev-cache-cleanup.test.js.map +1 -1
  7. package/dist/__tests__/unit/dev-lock.test.d.ts +2 -0
  8. package/dist/__tests__/unit/dev-lock.test.d.ts.map +1 -0
  9. package/dist/__tests__/unit/dev-lock.test.js +70 -0
  10. package/dist/__tests__/unit/dev-lock.test.js.map +1 -0
  11. package/dist/__tests__/unit/dev-sync-vendored.test.d.ts +2 -0
  12. package/dist/__tests__/unit/dev-sync-vendored.test.d.ts.map +1 -0
  13. package/dist/__tests__/unit/dev-sync-vendored.test.js +58 -0
  14. package/dist/__tests__/unit/dev-sync-vendored.test.js.map +1 -0
  15. package/dist/__tests__/unit/run-build-script.test.d.ts +2 -0
  16. package/dist/__tests__/unit/run-build-script.test.d.ts.map +1 -0
  17. package/dist/__tests__/unit/run-build-script.test.js +58 -0
  18. package/dist/__tests__/unit/run-build-script.test.js.map +1 -0
  19. package/dist/__tests__/unit/vendored-sync.test.js +4 -0
  20. package/dist/__tests__/unit/vendored-sync.test.js.map +1 -1
  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 +10 -1
  30. package/dist/commands/dev.d.ts.map +1 -1
  31. package/dist/commands/dev.js +111 -141
  32. package/dist/commands/dev.js.map +1 -1
  33. package/dist/index.js +10 -11
  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
  56. package/vendored/app/[[...slug]]/page.tsx +41 -28
  57. package/vendored/app/api/isr-health/route.ts +6 -4
  58. package/vendored/components/mdx/StepSlugContext.tsx +57 -0
  59. package/vendored/components/mdx/Steps.tsx +2 -2
  60. package/vendored/components/navigation/TableOfContents.tsx +77 -5
  61. package/vendored/lib/cache-tags.ts +25 -0
  62. package/vendored/lib/cache-utils.ts +19 -0
  63. package/vendored/lib/heading-extractor.ts +25 -6
  64. package/vendored/lib/indexnow.ts +1 -1
  65. package/vendored/lib/navigation-resolver.ts +1 -1
  66. package/vendored/lib/openapi-isr.ts +13 -8
  67. package/vendored/lib/r2-cleanup.ts +70 -0
  68. package/vendored/lib/r2-content.ts +0 -24
  69. package/vendored/lib/r2-manifest.ts +13 -3
  70. package/vendored/lib/revalidation-helpers.ts +41 -11
  71. package/vendored/lib/revalidation-trigger.ts +104 -28
  72. package/vendored/lib/scanner-blocklist.ts +256 -0
  73. package/vendored/lib/snippet-compiler-isr.ts +5 -2
  74. package/vendored/scripts/validate-links.cjs +17 -6
  75. package/vendored/workspace-package-lock.json +9 -9
  76. package/vendored/lib/cache-keys.ts +0 -117
@@ -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,7 +27,16 @@ export interface DevOptions {
27
27
  webpack?: boolean;
28
28
  clean?: boolean;
29
29
  }
30
- export declare function safeRemoveCache(dirPath: string): Promise<void>;
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
+ }>;
31
40
  /**
32
41
  * Validates the .next cache and cleans it if corrupted or stale.
33
42
  * Returns true if cache is valid and usable, false if it was cleared.
@@ -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;AAkPD,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgCpE;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,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
- import { spinner, stopSpinner } from '../lib/spinner.js';
13
- import { ensureDependencies, getWorkspaceDir, getJamdeskDir } from '../lib/deps.js';
13
+ import { spinner } from '../lib/spinner.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);
@@ -209,55 +220,6 @@ async function syncVendoredFiles(srcDir, destDir, filter) {
209
220
  await walkDest(destDir);
210
221
  return stats;
211
222
  }
212
- /**
213
- * Recursively removes a directory with retries.
214
- *
215
- * `fs-extra`'s legacy `remove` walks files then `rmdir`s the parent — if a
216
- * concurrent process (e.g. another `jamdesk dev` instance writing to its
217
- * Turbopack cache) repopulates the directory mid-walk, the final `rmdir`
218
- * fails with ENOTEMPTY/EBUSY and crashes the CLI as an unhandled rejection.
219
- *
220
- * `fs.rm` with `maxRetries` handles the race; on terminal failure we surface
221
- * a friendly message naming the most likely cause (another dev session
222
- * holding the workspace) and exit cleanly instead of stack-trace-crashing.
223
- *
224
- * Exported for unit testing.
225
- */
226
- // ENOTEMPTY/EBUSY are the documented APFS racy-rmdir codes; EPERM shows up
227
- // under macOS for files held open by a child process. EACCES is treated
228
- // separately — it almost always means a real permissions problem (e.g.
229
- // workspace owned by another user after a `sudo jamdesk dev` accident).
230
- const RACE_CODES = ['ENOTEMPTY', 'EBUSY', 'EPERM'];
231
- export async function safeRemoveCache(dirPath) {
232
- try {
233
- await fs.rm(dirPath, {
234
- recursive: true,
235
- force: true,
236
- maxRetries: 2,
237
- retryDelay: 200,
238
- });
239
- }
240
- catch (err) {
241
- const e = err;
242
- if (e.code && RACE_CODES.includes(e.code)) {
243
- stopSpinner();
244
- output.error(`Could not clear cache at ${dirPath} (${e.code}).\n` +
245
- ` Another \`jamdesk dev\` instance is likely still running and ` +
246
- `writing to this workspace.\n\n` +
247
- ` Fix: pkill -f "jamdesk dev|next dev" && jamdesk dev`);
248
- process.exit(1);
249
- }
250
- if (e.code === 'EACCES') {
251
- stopSpinner();
252
- output.error(`Permission denied clearing cache at ${dirPath} (EACCES).\n` +
253
- ` The workspace is likely owned by another user — check ownership ` +
254
- `with \`ls -ld ${dirPath}\` and \`chown\` it back to your user, ` +
255
- `or remove the directory manually.`);
256
- process.exit(1);
257
- }
258
- throw err;
259
- }
260
- }
261
223
  /**
262
224
  * Validates the .next cache and cleans it if corrupted or stale.
263
225
  * Returns true if cache is valid and usable, false if it was cleared.
@@ -402,7 +364,38 @@ export async function dev(options) {
402
364
  // Step 2: Prepare workspace
403
365
  spin = spinner('Preparing workspace...');
404
366
  const jamdeskDir = getJamdeskDir();
405
- 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
+ }
406
399
  const vendoredDir = path.join(__dirname, '../../vendored');
407
400
  // Check if vendored directory exists
408
401
  if (!fs.existsSync(vendoredDir)) {
@@ -414,58 +407,46 @@ export async function dev(options) {
414
407
  // Only remove and recopy vendored files, keeping the .next directory
415
408
  const nextCacheDir = path.join(workspaceDir, '.next');
416
409
  const cliVersionFile = path.join(nextCacheDir, '.jamdesk-cli-version');
417
- // Handle --clean flag: remove entire workspace to start fresh
418
- if (options.clean && fs.existsSync(workspaceDir)) {
419
- output.info('Clearing workspace cache...');
420
- await fs.remove(workspaceDir);
421
- }
422
- // Determine if we have usable cached compilation
423
- let hadCache = fs.existsSync(nextCacheDir);
424
- if (hadCache) {
425
- hadCache = await validateAndCleanCache(nextCacheDir, cliVersionFile, workspaceDir, verbose);
426
- }
410
+ const hadCache = fs.existsSync(nextCacheDir)
411
+ && await validateAndCleanCache(nextCacheDir, cliVersionFile, workspaceDir, verbose);
427
412
  // Ensure workspace directory exists
428
413
  await fs.ensureDir(workspaceDir);
429
414
  // Sync vendored files - only copy files that actually changed
430
415
  // This preserves timestamps for unchanged files, allowing Turbopack to reuse cache
431
416
  const syncStats = await syncVendoredFiles(vendoredDir, workspaceDir, (relativePath) => !relativePath.includes('node_modules') && !relativePath.includes('.next'));
432
- // Log sync stats in verbose mode only (internal detail)
433
417
  if (verbose && syncStats.copied > 0) {
434
418
  output.info(`Synced ${syncStats.copied} build files (${syncStats.skipped} unchanged)`);
435
419
  }
436
- // Use NODE_PATH instead of symlink for node_modules
437
- // Symlinks break Turbopack with "Symlink node_modules is invalid" error
438
- // NODE_PATH tells Node.js where to find modules without needing a symlink
439
- const jamdeskNodeModules = path.join(path.dirname(workspaceDir), 'node_modules');
440
- // 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.
441
422
  const workspaceNodeModules = path.join(workspaceDir, 'node_modules');
442
- if (fs.existsSync(workspaceNodeModules)) {
423
+ try {
443
424
  const stats = await fs.lstat(workspaceNodeModules);
444
- if (stats.isSymbolicLink()) {
425
+ if (stats.isSymbolicLink())
445
426
  await fs.remove(workspaceNodeModules);
446
- }
427
+ }
428
+ catch (err) {
429
+ if (err.code !== 'ENOENT')
430
+ throw err;
447
431
  }
448
432
  // Symlink public directory to parent so Turbopack can find static files
449
433
  // (outputFileTracingRoot and turbopack.root are set to parent directory)
450
434
  const parentPublic = path.join(jamdeskDir, 'public');
451
435
  const workspacePublic = path.join(workspaceDir, 'public');
452
436
  await fs.ensureDir(workspacePublic);
453
- // Only recreate symlink if target changed
437
+ // Only recreate symlink if target changed (preserves Turbopack cache mtimes)
454
438
  let needsPublicSymlink = true;
455
- if (fs.existsSync(parentPublic)) {
456
- try {
457
- const stats = await fs.lstat(parentPublic);
458
- if (stats.isSymbolicLink()) {
459
- const currentTarget = await fs.readlink(parentPublic);
460
- if (currentTarget === workspacePublic) {
461
- needsPublicSymlink = false;
462
- }
463
- else {
464
- await fs.remove(parentPublic);
465
- }
466
- }
439
+ try {
440
+ const stats = await fs.lstat(parentPublic);
441
+ if (stats.isSymbolicLink() && (await fs.readlink(parentPublic)) === workspacePublic) {
442
+ needsPublicSymlink = false;
467
443
  }
468
- catch {
444
+ else {
445
+ await fs.remove(parentPublic);
446
+ }
447
+ }
448
+ catch (err) {
449
+ if (err.code !== 'ENOENT') {
469
450
  await fs.remove(parentPublic);
470
451
  }
471
452
  }
@@ -478,28 +459,23 @@ export async function dev(options) {
478
459
  const workspaceProjectsDir = path.join(workspaceDir, 'projects');
479
460
  const workspaceProjectDir = path.join(workspaceProjectsDir, projectName);
480
461
  await fs.ensureDir(workspaceProjectsDir);
481
- // Only recreate symlink if target changed (preserves timestamps for Turbopack cache)
482
462
  let needsSymlink = true;
483
- if (fs.existsSync(workspaceProjectDir)) {
484
- try {
485
- const currentTarget = await fs.readlink(workspaceProjectDir);
486
- if (currentTarget === projectDir) {
487
- needsSymlink = false; // Symlink already points to correct location
488
- }
489
- else {
490
- await fs.remove(workspaceProjectDir);
491
- }
463
+ try {
464
+ if ((await fs.readlink(workspaceProjectDir)) === projectDir) {
465
+ needsSymlink = false;
492
466
  }
493
- catch {
494
- // 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') {
495
473
  await fs.remove(workspaceProjectDir);
496
474
  }
497
475
  }
498
476
  if (needsSymlink) {
499
477
  await fs.symlink(projectDir, workspaceProjectDir, 'junction');
500
478
  }
501
- // Copy project's docs.json to workspace/public/ (only if changed)
502
- await fs.ensureDir(path.join(workspaceDir, 'public'));
503
479
  await copyIfChanged(docsJsonPath, path.join(workspaceDir, 'public/docs.json'));
504
480
  spin.succeed('Workspace prepared');
505
481
  // Step 3: Copy project assets (images, etc.) - only copy changed files
@@ -551,30 +527,31 @@ export async function dev(options) {
551
527
  // Compute content hash to detect changes (based on file mtimes for speed)
552
528
  const crypto = await import('crypto');
553
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
+ };
554
539
  const computeContentHash = async () => {
555
540
  const hash = crypto.createHash('md5');
556
- // Hash docs.json content
557
541
  hash.update(await fs.readFile(docsJsonPath, 'utf-8'));
558
- // Glob only MDX/MD files instead of recursive readdir on entire project
559
- // (readdir walks node_modules, .git, images thousands of irrelevant entries)
560
- const mdxFiles = await glob('**/*.{mdx,md}', { cwd: projectDir, nodir: true });
561
- mdxFiles.sort(); // Deterministic order — glob walk order varies between runs
562
- for (const file of mdxFiles) {
563
- try {
564
- const stat = await fs.stat(path.join(projectDir, file));
565
- hash.update(`${file}:${stat.mtimeMs}`);
566
- }
567
- 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}`);
568
548
  }
569
- // Hash snippets mtimes if they exist
570
549
  if (hasSnippets) {
571
- const snippetFiles = await fs.readdir(snippetsDir);
572
- for (const file of snippetFiles) {
573
- try {
574
- const stat = await fs.stat(path.join(snippetsDir, file));
575
- hash.update(`snippets/${file}:${stat.mtimeMs}`);
576
- }
577
- 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}`);
578
555
  }
579
556
  }
580
557
  return hash.digest('hex');
@@ -595,34 +572,26 @@ export async function dev(options) {
595
572
  const contentChanged = currentHash !== cachedHash;
596
573
  if (contentChanged) {
597
574
  spin.text = 'Processing navigation, snippets, and search index...';
598
- // Helper to run a script (uses hardcoded script names, no user input)
599
- const runScript = (script) => {
600
- return new Promise((resolve) => {
601
- try {
602
- execSync(`node scripts/${script}`, {
603
- cwd: workspaceDir,
604
- stdio: verbose ? 'inherit' : 'pipe',
605
- env: { ...env, PROJECT_NAME: path.basename(projectDir) },
606
- });
607
- resolve({ script, success: true });
608
- }
609
- catch (error) {
610
- resolve({ script, success: false, error });
611
- }
612
- });
575
+ const scriptOpts = {
576
+ cwd: workspaceDir,
577
+ env: { ...env, PROJECT_NAME: path.basename(projectDir) },
578
+ verbose,
613
579
  };
614
- // Run all three scripts in parallel
615
580
  const [navResult, snippetsResult, searchResult] = await Promise.all([
616
- runScript('enhance-navigation.cjs'),
617
- runScript('compile-snippets.cjs'),
618
- runScript('build-search-index.cjs'),
581
+ runBuildScript('enhance-navigation.cjs', scriptOpts),
582
+ runBuildScript('compile-snippets.cjs', scriptOpts),
583
+ runBuildScript('build-search-index.cjs', scriptOpts),
619
584
  ]);
620
- // Check for failures
621
585
  const failures = [navResult, snippetsResult, searchResult].filter((r) => !r.success);
622
586
  if (failures.length > 0) {
623
587
  spin.fail('Build step failed');
624
588
  for (const f of failures) {
625
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'));
626
595
  if (verbose && f.error)
627
596
  console.error(f.error);
628
597
  }
@@ -908,6 +877,7 @@ export async function dev(options) {
908
877
  });
909
878
  // Handle Ctrl+C - be aggressive about cleanup
910
879
  function cleanup() {
880
+ release(); // Release the workspace lock first so a subsequent dev isn't blocked.
911
881
  if (compileSpinner) {
912
882
  clearInterval(compileSpinner);
913
883
  process.stdout.write('\r\x1b[K');
@@ -933,7 +903,7 @@ export async function dev(options) {
933
903
  }
934
904
  process.exit(0);
935
905
  }
936
- process.on('SIGINT', cleanup);
937
- process.on('SIGTERM', cleanup);
906
+ process.once('SIGINT', cleanup);
907
+ process.once('SIGTERM', cleanup);
938
908
  }
939
909
  //# sourceMappingURL=dev.js.map