@sanity/workbench-cli 1.4.0 → 1.6.0

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 (41) hide show
  1. package/dist/_exports/build.d.ts +46 -1
  2. package/dist/_exports/build.js +1 -0
  3. package/dist/_exports/build.js.map +1 -1
  4. package/dist/_exports/deploy.d.ts +49 -6
  5. package/dist/_exports/dev.d.ts +52 -9
  6. package/dist/_exports/index.d.ts +8 -1
  7. package/dist/_exports/preview.d.ts +169 -0
  8. package/dist/_exports/preview.js +3 -0
  9. package/dist/_exports/preview.js.map +1 -0
  10. package/dist/_exports/undeploy.d.ts +13 -1
  11. package/dist/actions/build/vite/optimize-deps.js +84 -0
  12. package/dist/actions/build/vite/optimize-deps.js.map +1 -0
  13. package/dist/actions/deploy/buildExposes.js +23 -10
  14. package/dist/actions/deploy/buildExposes.js.map +1 -1
  15. package/dist/actions/deploy/checkBuiltOutput.js +16 -2
  16. package/dist/actions/deploy/checkBuiltOutput.js.map +1 -1
  17. package/dist/actions/deploy/deployWorkbenchApp.js +19 -4
  18. package/dist/actions/deploy/deployWorkbenchApp.js.map +1 -1
  19. package/dist/actions/dev/deriveInterfaces.js +41 -25
  20. package/dist/actions/dev/deriveInterfaces.js.map +1 -1
  21. package/dist/actions/dev/registry.js +99 -19
  22. package/dist/actions/dev/registry.js.map +1 -1
  23. package/dist/actions/dev/startWorkbenchDev.js +6 -41
  24. package/dist/actions/dev/startWorkbenchDev.js.map +1 -1
  25. package/dist/actions/dev/startWorkbenchDevServer.js +7 -3
  26. package/dist/actions/dev/startWorkbenchDevServer.js.map +1 -1
  27. package/dist/actions/preview/serveBuiltApplication.js +53 -0
  28. package/dist/actions/preview/serveBuiltApplication.js.map +1 -0
  29. package/dist/actions/preview/startWorkbenchPreview.js +91 -0
  30. package/dist/actions/preview/startWorkbenchPreview.js.map +1 -0
  31. package/dist/contract.js +32 -0
  32. package/dist/contract.js.map +1 -1
  33. package/dist/defineApp.js +13 -6
  34. package/dist/defineApp.js.map +1 -1
  35. package/dist/resolveWorkbenchApp.js +3 -1
  36. package/dist/resolveWorkbenchApp.js.map +1 -1
  37. package/dist/services/applications.js +15 -1
  38. package/dist/services/applications.js.map +1 -1
  39. package/dist/util/serverOrchestration.js +75 -0
  40. package/dist/util/serverOrchestration.js.map +1 -0
  41. package/package.json +16 -12
@@ -1,27 +1,40 @@
1
+ import { interfaceModuleId } from '../../contract.js';
1
2
  /**
2
3
  * The interface records deploy sends: the app view (only when `exposesAppView`),
3
4
  * every view, and every service.
4
5
  * @internal
5
6
  */ export function buildExposes(exposes, { appName, appTitle, exposesAppView, version }) {
6
- const toRecord = (prefix, decl)=>({
7
- moduleId: `${prefix}/${decl.name}`,
8
- name: decl.name,
9
- title: decl.title ?? decl.name,
10
- type: decl.type,
11
- version
12
- });
13
7
  const records = [];
14
8
  if (exposesAppView) {
15
9
  records.push({
16
- moduleId: 'App',
10
+ metadata: null,
11
+ moduleId: interfaceModuleId('app', appName),
17
12
  name: appName,
18
13
  title: appTitle,
19
14
  type: 'app',
20
15
  version
21
16
  });
22
17
  }
23
- for (const view of exposes.views ?? [])records.push(toRecord('views', view));
24
- for (const service of exposes.services ?? [])records.push(toRecord('services', service));
18
+ for (const view of exposes.views ?? []){
19
+ records.push({
20
+ metadata: null,
21
+ moduleId: interfaceModuleId('panel', view.name),
22
+ name: view.name,
23
+ title: view.title ?? view.name,
24
+ type: 'panel',
25
+ version
26
+ });
27
+ }
28
+ for (const service of exposes.services ?? []){
29
+ records.push({
30
+ metadata: null,
31
+ moduleId: interfaceModuleId('worker', service.name),
32
+ name: service.name,
33
+ title: service.title ?? service.name,
34
+ type: 'worker',
35
+ version
36
+ });
37
+ }
25
38
  return records;
26
39
  }
27
40
  const label = (item)=>item.title === item.name ? item.name : `${item.title} (${item.name})`;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/actions/deploy/buildExposes.ts"],"sourcesContent":["import {type WorkbenchExposes} from '../../resolveWorkbenchApp.js'\nimport {type BrettInterface} from '../../services/applications.js'\n\ninterface BuildExposesContext {\n appName: string\n appTitle: string\n /** Whether the build exposes the app view (`./App`) — apps with an `entry`, and every studio. */\n exposesAppView: boolean\n version: string\n}\n\n/**\n * The interface records deploy sends: the app view (only when `exposesAppView`),\n * every view, and every service.\n * @internal\n */\nexport function buildExposes(\n exposes: WorkbenchExposes,\n {appName, appTitle, exposesAppView, version}: BuildExposesContext,\n): BrettInterface[] {\n const toRecord = (\n prefix: string,\n decl: {name: string; title?: string; type: string},\n ): BrettInterface => ({\n moduleId: `${prefix}/${decl.name}`,\n name: decl.name,\n title: decl.title ?? decl.name,\n type: decl.type,\n version,\n })\n\n const records: BrettInterface[] = []\n if (exposesAppView) {\n records.push({moduleId: 'App', name: appName, title: appTitle, type: 'app', version})\n }\n for (const view of exposes.views ?? []) records.push(toRecord('views', view))\n for (const service of exposes.services ?? []) records.push(toRecord('services', service))\n return records\n}\n\n/** A view or service as the deploy report and `--json` output surface it. */\nexport interface DeployedExpose {\n name: string\n src: string\n title: string\n type: string\n}\n\nconst label = (item: {name: string; title: string}) =>\n item.title === item.name ? item.name : `${item.title} (${item.name})`\n\n/**\n * One `Title (name): src` report line per declared entry point.\n * @internal\n */\nexport function summarizeExposeGroup(\n heading: string,\n items: readonly {name: string; src: string; title: string}[],\n): string {\n return `${heading}:\\n${items.map((item) => ` ${label(item)}: ${item.src}`).join('\\n')}`\n}\n\n/**\n * The deploy summary of an app's exposes: the structured records (for `--json`)\n * and one report line per non-empty group (for the human report).\n * @internal\n */\nexport function summarizeExposes({services, views}: WorkbenchExposes): {\n exposes: DeployedExpose[]\n lines: string[]\n} {\n const toExpose = (decl: {\n name: string\n src: string\n title?: string\n type: string\n }): DeployedExpose => ({\n name: decl.name,\n src: decl.src,\n title: decl.title ?? decl.name,\n type: decl.type,\n })\n const viewExposes = (views ?? []).map((view) => toExpose(view))\n const serviceExposes = (services ?? []).map((service) => toExpose(service))\n\n const lines: string[] = []\n if (viewExposes.length > 0) lines.push(summarizeExposeGroup('Views', viewExposes))\n if (serviceExposes.length > 0) lines.push(summarizeExposeGroup('Services', serviceExposes))\n return {exposes: [...viewExposes, ...serviceExposes], lines}\n}\n"],"names":["buildExposes","exposes","appName","appTitle","exposesAppView","version","toRecord","prefix","decl","moduleId","name","title","type","records","push","view","views","service","services","label","item","summarizeExposeGroup","heading","items","map","src","join","summarizeExposes","toExpose","viewExposes","serviceExposes","lines","length"],"mappings":"AAWA;;;;CAIC,GACD,OAAO,SAASA,aACdC,OAAyB,EACzB,EAACC,OAAO,EAAEC,QAAQ,EAAEC,cAAc,EAAEC,OAAO,EAAsB;IAEjE,MAAMC,WAAW,CACfC,QACAC,OACoB,CAAA;YACpBC,UAAU,GAAGF,OAAO,CAAC,EAAEC,KAAKE,IAAI,EAAE;YAClCA,MAAMF,KAAKE,IAAI;YACfC,OAAOH,KAAKG,KAAK,IAAIH,KAAKE,IAAI;YAC9BE,MAAMJ,KAAKI,IAAI;YACfP;QACF,CAAA;IAEA,MAAMQ,UAA4B,EAAE;IACpC,IAAIT,gBAAgB;QAClBS,QAAQC,IAAI,CAAC;YAACL,UAAU;YAAOC,MAAMR;YAASS,OAAOR;YAAUS,MAAM;YAAOP;QAAO;IACrF;IACA,KAAK,MAAMU,QAAQd,QAAQe,KAAK,IAAI,EAAE,CAAEH,QAAQC,IAAI,CAACR,SAAS,SAASS;IACvE,KAAK,MAAME,WAAWhB,QAAQiB,QAAQ,IAAI,EAAE,CAAEL,QAAQC,IAAI,CAACR,SAAS,YAAYW;IAChF,OAAOJ;AACT;AAUA,MAAMM,QAAQ,CAACC,OACbA,KAAKT,KAAK,KAAKS,KAAKV,IAAI,GAAGU,KAAKV,IAAI,GAAG,GAAGU,KAAKT,KAAK,CAAC,EAAE,EAAES,KAAKV,IAAI,CAAC,CAAC,CAAC;AAEvE;;;CAGC,GACD,OAAO,SAASW,qBACdC,OAAe,EACfC,KAA4D;IAE5D,OAAO,GAAGD,QAAQ,GAAG,EAAEC,MAAMC,GAAG,CAAC,CAACJ,OAAS,CAAC,EAAE,EAAED,MAAMC,MAAM,EAAE,EAAEA,KAAKK,GAAG,EAAE,EAAEC,IAAI,CAAC,OAAO;AAC1F;AAEA;;;;CAIC,GACD,OAAO,SAASC,iBAAiB,EAACT,QAAQ,EAAEF,KAAK,EAAmB;IAIlE,MAAMY,WAAW,CAACpB,OAKK,CAAA;YACrBE,MAAMF,KAAKE,IAAI;YACfe,KAAKjB,KAAKiB,GAAG;YACbd,OAAOH,KAAKG,KAAK,IAAIH,KAAKE,IAAI;YAC9BE,MAAMJ,KAAKI,IAAI;QACjB,CAAA;IACA,MAAMiB,cAAc,AAACb,CAAAA,SAAS,EAAE,AAAD,EAAGQ,GAAG,CAAC,CAACT,OAASa,SAASb;IACzD,MAAMe,iBAAiB,AAACZ,CAAAA,YAAY,EAAE,AAAD,EAAGM,GAAG,CAAC,CAACP,UAAYW,SAASX;IAElE,MAAMc,QAAkB,EAAE;IAC1B,IAAIF,YAAYG,MAAM,GAAG,GAAGD,MAAMjB,IAAI,CAACO,qBAAqB,SAASQ;IACrE,IAAIC,eAAeE,MAAM,GAAG,GAAGD,MAAMjB,IAAI,CAACO,qBAAqB,YAAYS;IAC3E,OAAO;QAAC7B,SAAS;eAAI4B;eAAgBC;SAAe;QAAEC;IAAK;AAC7D"}
1
+ {"version":3,"sources":["../../../src/actions/deploy/buildExposes.ts"],"sourcesContent":["import {interfaceModuleId} from '../../contract.js'\nimport {type WorkbenchExposes} from '../../resolveWorkbenchApp.js'\nimport {type BrettInterface} from '../../services/applications.js'\n\ninterface BuildExposesContext {\n appName: string\n appTitle: string\n /** Whether the build exposes the app view (`./App`) — apps with an `entry`, and every studio. */\n exposesAppView: boolean\n version: string\n}\n\n/**\n * The interface records deploy sends: the app view (only when `exposesAppView`),\n * every view, and every service.\n * @internal\n */\nexport function buildExposes(\n exposes: WorkbenchExposes,\n {appName, appTitle, exposesAppView, version}: BuildExposesContext,\n): BrettInterface[] {\n const records: BrettInterface[] = []\n if (exposesAppView) {\n records.push({\n metadata: null,\n moduleId: interfaceModuleId('app', appName),\n name: appName,\n title: appTitle,\n type: 'app',\n version,\n })\n }\n for (const view of exposes.views ?? []) {\n records.push({\n metadata: null,\n moduleId: interfaceModuleId('panel', view.name),\n name: view.name,\n title: view.title ?? view.name,\n type: 'panel',\n version,\n })\n }\n for (const service of exposes.services ?? []) {\n records.push({\n metadata: null,\n moduleId: interfaceModuleId('worker', service.name),\n name: service.name,\n title: service.title ?? service.name,\n type: 'worker',\n version,\n })\n }\n return records\n}\n\n/** A view or service as the deploy report and `--json` output surface it. */\nexport interface DeployedExpose {\n name: string\n src: string\n title: string\n type: string\n}\n\nconst label = (item: {name: string; title: string}) =>\n item.title === item.name ? item.name : `${item.title} (${item.name})`\n\n/**\n * One `Title (name): src` report line per declared entry point.\n * @internal\n */\nexport function summarizeExposeGroup(\n heading: string,\n items: readonly {name: string; src: string; title: string}[],\n): string {\n return `${heading}:\\n${items.map((item) => ` ${label(item)}: ${item.src}`).join('\\n')}`\n}\n\n/**\n * The deploy summary of an app's exposes: the structured records (for `--json`)\n * and one report line per non-empty group (for the human report).\n * @internal\n */\nexport function summarizeExposes({services, views}: WorkbenchExposes): {\n exposes: DeployedExpose[]\n lines: string[]\n} {\n const toExpose = (decl: {\n name: string\n src: string\n title?: string\n type: string\n }): DeployedExpose => ({\n name: decl.name,\n src: decl.src,\n title: decl.title ?? decl.name,\n type: decl.type,\n })\n const viewExposes = (views ?? []).map((view) => toExpose(view))\n const serviceExposes = (services ?? []).map((service) => toExpose(service))\n\n const lines: string[] = []\n if (viewExposes.length > 0) lines.push(summarizeExposeGroup('Views', viewExposes))\n if (serviceExposes.length > 0) lines.push(summarizeExposeGroup('Services', serviceExposes))\n return {exposes: [...viewExposes, ...serviceExposes], lines}\n}\n"],"names":["interfaceModuleId","buildExposes","exposes","appName","appTitle","exposesAppView","version","records","push","metadata","moduleId","name","title","type","view","views","service","services","label","item","summarizeExposeGroup","heading","items","map","src","join","summarizeExposes","toExpose","decl","viewExposes","serviceExposes","lines","length"],"mappings":"AAAA,SAAQA,iBAAiB,QAAO,oBAAmB;AAYnD;;;;CAIC,GACD,OAAO,SAASC,aACdC,OAAyB,EACzB,EAACC,OAAO,EAAEC,QAAQ,EAAEC,cAAc,EAAEC,OAAO,EAAsB;IAEjE,MAAMC,UAA4B,EAAE;IACpC,IAAIF,gBAAgB;QAClBE,QAAQC,IAAI,CAAC;YACXC,UAAU;YACVC,UAAUV,kBAAkB,OAAOG;YACnCQ,MAAMR;YACNS,OAAOR;YACPS,MAAM;YACNP;QACF;IACF;IACA,KAAK,MAAMQ,QAAQZ,QAAQa,KAAK,IAAI,EAAE,CAAE;QACtCR,QAAQC,IAAI,CAAC;YACXC,UAAU;YACVC,UAAUV,kBAAkB,SAASc,KAAKH,IAAI;YAC9CA,MAAMG,KAAKH,IAAI;YACfC,OAAOE,KAAKF,KAAK,IAAIE,KAAKH,IAAI;YAC9BE,MAAM;YACNP;QACF;IACF;IACA,KAAK,MAAMU,WAAWd,QAAQe,QAAQ,IAAI,EAAE,CAAE;QAC5CV,QAAQC,IAAI,CAAC;YACXC,UAAU;YACVC,UAAUV,kBAAkB,UAAUgB,QAAQL,IAAI;YAClDA,MAAMK,QAAQL,IAAI;YAClBC,OAAOI,QAAQJ,KAAK,IAAII,QAAQL,IAAI;YACpCE,MAAM;YACNP;QACF;IACF;IACA,OAAOC;AACT;AAUA,MAAMW,QAAQ,CAACC,OACbA,KAAKP,KAAK,KAAKO,KAAKR,IAAI,GAAGQ,KAAKR,IAAI,GAAG,GAAGQ,KAAKP,KAAK,CAAC,EAAE,EAAEO,KAAKR,IAAI,CAAC,CAAC,CAAC;AAEvE;;;CAGC,GACD,OAAO,SAASS,qBACdC,OAAe,EACfC,KAA4D;IAE5D,OAAO,GAAGD,QAAQ,GAAG,EAAEC,MAAMC,GAAG,CAAC,CAACJ,OAAS,CAAC,EAAE,EAAED,MAAMC,MAAM,EAAE,EAAEA,KAAKK,GAAG,EAAE,EAAEC,IAAI,CAAC,OAAO;AAC1F;AAEA;;;;CAIC,GACD,OAAO,SAASC,iBAAiB,EAACT,QAAQ,EAAEF,KAAK,EAAmB;IAIlE,MAAMY,WAAW,CAACC,OAKK,CAAA;YACrBjB,MAAMiB,KAAKjB,IAAI;YACfa,KAAKI,KAAKJ,GAAG;YACbZ,OAAOgB,KAAKhB,KAAK,IAAIgB,KAAKjB,IAAI;YAC9BE,MAAMe,KAAKf,IAAI;QACjB,CAAA;IACA,MAAMgB,cAAc,AAACd,CAAAA,SAAS,EAAE,AAAD,EAAGQ,GAAG,CAAC,CAACT,OAASa,SAASb;IACzD,MAAMgB,iBAAiB,AAACb,CAAAA,YAAY,EAAE,AAAD,EAAGM,GAAG,CAAC,CAACP,UAAYW,SAASX;IAElE,MAAMe,QAAkB,EAAE;IAC1B,IAAIF,YAAYG,MAAM,GAAG,GAAGD,MAAMvB,IAAI,CAACY,qBAAqB,SAASS;IACrE,IAAIC,eAAeE,MAAM,GAAG,GAAGD,MAAMvB,IAAI,CAACY,qBAAqB,YAAYU;IAC3E,OAAO;QAAC5B,SAAS;eAAI2B;eAAgBC;SAAe;QAAEC;IAAK;AAC7D"}
@@ -1,5 +1,15 @@
1
1
  import { stat } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
+ /**
4
+ * A genuinely-missing-build error, named so callers can offer the "run sanity
5
+ * build" hint (the `preview` command keys off this name, matching how the studio
6
+ * path keys off a missing `index.html`). Only the missing cases get it — real
7
+ * I/O failures keep their own name so they surface as themselves.
8
+ */ function buildNotFound(message) {
9
+ const error = new Error(message);
10
+ error.name = 'BUILD_NOT_FOUND';
11
+ return error;
12
+ }
3
13
  /**
4
14
  * Throws unless `sourceDir` is a directory holding a federation build.
5
15
  * A workbench build always emits a module-federation remote, and may
@@ -13,13 +23,17 @@ import { join } from 'node:path';
13
23
  throw new Error(`"${sourceDir}" is not a directory`);
14
24
  }
15
25
  } catch (err) {
16
- throw err.code === 'ENOENT' ? new Error(`Directory "${sourceDir}" does not exist`) : err;
26
+ if (err.code === 'ENOENT') throw buildNotFound(`Directory "${sourceDir}" does not exist`);
27
+ throw err;
17
28
  }
18
29
  const manifestPath = join(sourceDir, 'mf-manifest.json');
19
30
  try {
20
31
  await stat(manifestPath);
21
32
  } catch (err) {
22
- throw err.code === 'ENOENT' ? new Error(`"${manifestPath}" does not exist. ` + 'The deploy directory must contain a federation build created with "sanity build".') : err;
33
+ if (err.code === 'ENOENT') {
34
+ throw buildNotFound(`"${manifestPath}" does not exist. ` + 'The deploy directory must contain a federation build created with "sanity build".');
35
+ }
36
+ throw err;
23
37
  }
24
38
  }
25
39
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/actions/deploy/checkBuiltOutput.ts"],"sourcesContent":["import {stat} from 'node:fs/promises'\nimport {join} from 'node:path'\n\n/**\n * Throws unless `sourceDir` is a directory holding a federation build.\n * A workbench build always emits a module-federation remote, and may\n * additionally emit a standalone `index.html` SPA (workbench remotes). Either\n * way `mf-manifest.json` is the reliable marker that `sanity build` produced a\n * federation build, so that — not `index.html` — is what we check for.\n */\nexport async function checkBuiltOutput(sourceDir: string): Promise<void> {\n try {\n const stats = await stat(sourceDir)\n if (!stats.isDirectory()) {\n throw new Error(`\"${sourceDir}\" is not a directory`)\n }\n } catch (err) {\n throw err.code === 'ENOENT' ? new Error(`Directory \"${sourceDir}\" does not exist`) : err\n }\n\n const manifestPath = join(sourceDir, 'mf-manifest.json')\n try {\n await stat(manifestPath)\n } catch (err) {\n throw err.code === 'ENOENT'\n ? new Error(\n `\"${manifestPath}\" does not exist. ` +\n 'The deploy directory must contain a federation build created with \"sanity build\".',\n )\n : err\n }\n}\n"],"names":["stat","join","checkBuiltOutput","sourceDir","stats","isDirectory","Error","err","code","manifestPath"],"mappings":"AAAA,SAAQA,IAAI,QAAO,mBAAkB;AACrC,SAAQC,IAAI,QAAO,YAAW;AAE9B;;;;;;CAMC,GACD,OAAO,eAAeC,iBAAiBC,SAAiB;IACtD,IAAI;QACF,MAAMC,QAAQ,MAAMJ,KAAKG;QACzB,IAAI,CAACC,MAAMC,WAAW,IAAI;YACxB,MAAM,IAAIC,MAAM,CAAC,CAAC,EAAEH,UAAU,oBAAoB,CAAC;QACrD;IACF,EAAE,OAAOI,KAAK;QACZ,MAAMA,IAAIC,IAAI,KAAK,WAAW,IAAIF,MAAM,CAAC,WAAW,EAAEH,UAAU,gBAAgB,CAAC,IAAII;IACvF;IAEA,MAAME,eAAeR,KAAKE,WAAW;IACrC,IAAI;QACF,MAAMH,KAAKS;IACb,EAAE,OAAOF,KAAK;QACZ,MAAMA,IAAIC,IAAI,KAAK,WACf,IAAIF,MACF,CAAC,CAAC,EAAEG,aAAa,kBAAkB,CAAC,GAClC,uFAEJF;IACN;AACF"}
1
+ {"version":3,"sources":["../../../src/actions/deploy/checkBuiltOutput.ts"],"sourcesContent":["import {stat} from 'node:fs/promises'\nimport {join} from 'node:path'\n\n/**\n * A genuinely-missing-build error, named so callers can offer the \"run sanity\n * build\" hint (the `preview` command keys off this name, matching how the studio\n * path keys off a missing `index.html`). Only the missing cases get it — real\n * I/O failures keep their own name so they surface as themselves.\n */\nfunction buildNotFound(message: string): Error {\n const error = new Error(message)\n error.name = 'BUILD_NOT_FOUND'\n return error\n}\n\n/**\n * Throws unless `sourceDir` is a directory holding a federation build.\n * A workbench build always emits a module-federation remote, and may\n * additionally emit a standalone `index.html` SPA (workbench remotes). Either\n * way `mf-manifest.json` is the reliable marker that `sanity build` produced a\n * federation build, so that — not `index.html` — is what we check for.\n */\nexport async function checkBuiltOutput(sourceDir: string): Promise<void> {\n try {\n const stats = await stat(sourceDir)\n if (!stats.isDirectory()) {\n throw new Error(`\"${sourceDir}\" is not a directory`)\n }\n } catch (err) {\n if (err.code === 'ENOENT') throw buildNotFound(`Directory \"${sourceDir}\" does not exist`)\n throw err\n }\n\n const manifestPath = join(sourceDir, 'mf-manifest.json')\n try {\n await stat(manifestPath)\n } catch (err) {\n if (err.code === 'ENOENT') {\n throw buildNotFound(\n `\"${manifestPath}\" does not exist. ` +\n 'The deploy directory must contain a federation build created with \"sanity build\".',\n )\n }\n throw err\n }\n}\n"],"names":["stat","join","buildNotFound","message","error","Error","name","checkBuiltOutput","sourceDir","stats","isDirectory","err","code","manifestPath"],"mappings":"AAAA,SAAQA,IAAI,QAAO,mBAAkB;AACrC,SAAQC,IAAI,QAAO,YAAW;AAE9B;;;;;CAKC,GACD,SAASC,cAAcC,OAAe;IACpC,MAAMC,QAAQ,IAAIC,MAAMF;IACxBC,MAAME,IAAI,GAAG;IACb,OAAOF;AACT;AAEA;;;;;;CAMC,GACD,OAAO,eAAeG,iBAAiBC,SAAiB;IACtD,IAAI;QACF,MAAMC,QAAQ,MAAMT,KAAKQ;QACzB,IAAI,CAACC,MAAMC,WAAW,IAAI;YACxB,MAAM,IAAIL,MAAM,CAAC,CAAC,EAAEG,UAAU,oBAAoB,CAAC;QACrD;IACF,EAAE,OAAOG,KAAK;QACZ,IAAIA,IAAIC,IAAI,KAAK,UAAU,MAAMV,cAAc,CAAC,WAAW,EAAEM,UAAU,gBAAgB,CAAC;QACxF,MAAMG;IACR;IAEA,MAAME,eAAeZ,KAAKO,WAAW;IACrC,IAAI;QACF,MAAMR,KAAKa;IACb,EAAE,OAAOF,KAAK;QACZ,IAAIA,IAAIC,IAAI,KAAK,UAAU;YACzB,MAAMV,cACJ,CAAC,CAAC,EAAEW,aAAa,kBAAkB,CAAC,GAClC;QAEN;QACA,MAAMF;IACR;AACF"}
@@ -3,14 +3,14 @@ import { createGzip } from 'node:zlib';
3
3
  import { exitCodes } from '@sanity/cli-core';
4
4
  import { spinner } from '@sanity/cli-core/ux';
5
5
  import { pack } from 'tar-fs';
6
- import { createApplication, createDeployment } from '../../services/applications.js';
6
+ import { createApplication, createDeployment, updateApplication } from '../../services/applications.js';
7
7
  /**
8
8
  * Deploy a workbench coreApp through Brett: redeploy when `appId` is set,
9
9
  * otherwise create the application at `slug`. Returns the application id for the
10
10
  * shell to report.
11
11
  * @internal
12
12
  */ export async function deployCoreApp(options) {
13
- const { appId, interfaces, isAutoUpdating, isSingleton, organizationId, slug, sourceDir, title, version } = options;
13
+ const { appId, icon, interfaces, isAutoUpdating, isSingleton, organizationId, slug, sourceDir, title, version, visibility } = options;
14
14
  const tarball = pack(dirname(sourceDir), {
15
15
  entries: [
16
16
  basename(sourceDir)
@@ -26,12 +26,19 @@ import { createApplication, createDeployment } from '../../services/applications
26
26
  tarball,
27
27
  version
28
28
  });
29
+ await updateApplication(appId, {
30
+ title,
31
+ ...icon ? {
32
+ icon
33
+ } : {}
34
+ });
29
35
  spin.succeed();
30
36
  return {
31
37
  applicationId: appId
32
38
  };
33
39
  }
34
40
  const { id } = await createApplication({
41
+ icon,
35
42
  interfaces,
36
43
  isSingleton,
37
44
  organizationId,
@@ -39,7 +46,8 @@ import { createApplication, createDeployment } from '../../services/applications
39
46
  tarball,
40
47
  title,
41
48
  type: 'coreApp',
42
- version
49
+ version,
50
+ visibility
43
51
  });
44
52
  spin.succeed();
45
53
  return {
@@ -56,7 +64,7 @@ import { createApplication, createDeployment } from '../../services/applications
56
64
  * the shell to report; a missing `studioHost` on create is a usage error.
57
65
  * @internal
58
66
  */ export async function deployStudio(options) {
59
- const { appId, interfaces, isAutoUpdating, organizationId, output, projectId, sourceDir, studioHost, title, version, workspaces } = options;
67
+ const { appId, icon, interfaces, isAutoUpdating, organizationId, output, projectId, sourceDir, studioHost, title, version, workspaces } = options;
60
68
  const tarball = pack(dirname(sourceDir), {
61
69
  entries: [
62
70
  basename(sourceDir)
@@ -73,6 +81,12 @@ import { createApplication, createDeployment } from '../../services/applications
73
81
  version,
74
82
  workspaces
75
83
  });
84
+ await updateApplication(appId, {
85
+ title,
86
+ ...icon ? {
87
+ icon
88
+ } : {}
89
+ });
76
90
  spin.succeed();
77
91
  return {
78
92
  applicationId: appId
@@ -85,6 +99,7 @@ import { createApplication, createDeployment } from '../../services/applications
85
99
  });
86
100
  }
87
101
  const application = await createApplication({
102
+ icon,
88
103
  interfaces,
89
104
  organizationId,
90
105
  projectId,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/actions/deploy/deployWorkbenchApp.ts"],"sourcesContent":["import {basename, dirname} from 'node:path'\nimport {createGzip} from 'node:zlib'\n\nimport {exitCodes, type Output} from '@sanity/cli-core'\nimport {spinner} from '@sanity/cli-core/ux'\nimport {pack} from 'tar-fs'\n\nimport {\n type BrettInterface,\n type BrettWorkspace,\n createApplication,\n createDeployment,\n} from '../../services/applications.js'\n\n/**\n * Deploy a workbench coreApp through Brett: redeploy when `appId` is set,\n * otherwise create the application at `slug`. Returns the application id for the\n * shell to report.\n * @internal\n */\nexport async function deployCoreApp(options: {\n appId: string | undefined\n interfaces: readonly BrettInterface[]\n isAutoUpdating: boolean\n isSingleton?: boolean\n organizationId: string\n slug: string\n sourceDir: string\n title: string\n version: string\n}): Promise<{applicationId: string}> {\n const {\n appId,\n interfaces,\n isAutoUpdating,\n isSingleton,\n organizationId,\n slug,\n sourceDir,\n title,\n version,\n } = options\n const tarball = pack(dirname(sourceDir), {entries: [basename(sourceDir)]}).pipe(createGzip())\n\n const spin = spinner('Deploying...').start()\n try {\n if (appId) {\n await createDeployment({applicationId: appId, interfaces, isAutoUpdating, tarball, version})\n spin.succeed()\n return {applicationId: appId}\n }\n\n const {id} = await createApplication({\n interfaces,\n isSingleton,\n organizationId,\n slug,\n tarball,\n title,\n type: 'coreApp',\n version,\n })\n spin.succeed()\n return {applicationId: id}\n } catch (error) {\n spin.clear()\n throw error\n }\n}\n\n/**\n * Deploy a workbench studio through Brett: redeploy when `appId` is set,\n * otherwise create the studio at `studioHost`. Returns the application id for\n * the shell to report; a missing `studioHost` on create is a usage error.\n * @internal\n */\nexport async function deployStudio(options: {\n appId: string | undefined\n interfaces: readonly BrettInterface[]\n isAutoUpdating: boolean\n organizationId: string\n output: Output\n projectId: string | undefined\n sourceDir: string\n studioHost: string | undefined\n title: string\n version: string\n workspaces: readonly BrettWorkspace[]\n}): Promise<{applicationId: string}> {\n const {\n appId,\n interfaces,\n isAutoUpdating,\n organizationId,\n output,\n projectId,\n sourceDir,\n studioHost,\n title,\n version,\n workspaces,\n } = options\n const tarball = pack(dirname(sourceDir), {entries: [basename(sourceDir)]}).pipe(createGzip())\n\n const spin = spinner('Deploying to sanity.studio').start()\n try {\n if (appId) {\n await createDeployment({\n applicationId: appId,\n interfaces,\n isAutoUpdating,\n tarball,\n version,\n workspaces,\n })\n spin.succeed()\n return {applicationId: appId}\n }\n\n if (!studioHost) {\n spin.fail()\n return output.error(\n 'No studio hostname configured. Set `studioHost` in sanity.cli.ts to create a studio.',\n {exit: exitCodes.USAGE_ERROR},\n )\n }\n\n const application = await createApplication({\n interfaces,\n organizationId,\n projectId,\n slug: studioHost,\n tarball,\n title,\n type: 'studio',\n version,\n workspaces,\n })\n spin.succeed()\n return {applicationId: application.id}\n } catch (error) {\n spin.fail()\n throw error\n }\n}\n"],"names":["basename","dirname","createGzip","exitCodes","spinner","pack","createApplication","createDeployment","deployCoreApp","options","appId","interfaces","isAutoUpdating","isSingleton","organizationId","slug","sourceDir","title","version","tarball","entries","pipe","spin","start","applicationId","succeed","id","type","error","clear","deployStudio","output","projectId","studioHost","workspaces","fail","exit","USAGE_ERROR","application"],"mappings":"AAAA,SAAQA,QAAQ,EAAEC,OAAO,QAAO,YAAW;AAC3C,SAAQC,UAAU,QAAO,YAAW;AAEpC,SAAQC,SAAS,QAAoB,mBAAkB;AACvD,SAAQC,OAAO,QAAO,sBAAqB;AAC3C,SAAQC,IAAI,QAAO,SAAQ;AAE3B,SAGEC,iBAAiB,EACjBC,gBAAgB,QACX,iCAAgC;AAEvC;;;;;CAKC,GACD,OAAO,eAAeC,cAAcC,OAUnC;IACC,MAAM,EACJC,KAAK,EACLC,UAAU,EACVC,cAAc,EACdC,WAAW,EACXC,cAAc,EACdC,IAAI,EACJC,SAAS,EACTC,KAAK,EACLC,OAAO,EACR,GAAGT;IACJ,MAAMU,UAAUd,KAAKJ,QAAQe,YAAY;QAACI,SAAS;YAACpB,SAASgB;SAAW;IAAA,GAAGK,IAAI,CAACnB;IAEhF,MAAMoB,OAAOlB,QAAQ,gBAAgBmB,KAAK;IAC1C,IAAI;QACF,IAAIb,OAAO;YACT,MAAMH,iBAAiB;gBAACiB,eAAed;gBAAOC;gBAAYC;gBAAgBO;gBAASD;YAAO;YAC1FI,KAAKG,OAAO;YACZ,OAAO;gBAACD,eAAed;YAAK;QAC9B;QAEA,MAAM,EAACgB,EAAE,EAAC,GAAG,MAAMpB,kBAAkB;YACnCK;YACAE;YACAC;YACAC;YACAI;YACAF;YACAU,MAAM;YACNT;QACF;QACAI,KAAKG,OAAO;QACZ,OAAO;YAACD,eAAeE;QAAE;IAC3B,EAAE,OAAOE,OAAO;QACdN,KAAKO,KAAK;QACV,MAAMD;IACR;AACF;AAEA;;;;;CAKC,GACD,OAAO,eAAeE,aAAarB,OAYlC;IACC,MAAM,EACJC,KAAK,EACLC,UAAU,EACVC,cAAc,EACdE,cAAc,EACdiB,MAAM,EACNC,SAAS,EACThB,SAAS,EACTiB,UAAU,EACVhB,KAAK,EACLC,OAAO,EACPgB,UAAU,EACX,GAAGzB;IACJ,MAAMU,UAAUd,KAAKJ,QAAQe,YAAY;QAACI,SAAS;YAACpB,SAASgB;SAAW;IAAA,GAAGK,IAAI,CAACnB;IAEhF,MAAMoB,OAAOlB,QAAQ,8BAA8BmB,KAAK;IACxD,IAAI;QACF,IAAIb,OAAO;YACT,MAAMH,iBAAiB;gBACrBiB,eAAed;gBACfC;gBACAC;gBACAO;gBACAD;gBACAgB;YACF;YACAZ,KAAKG,OAAO;YACZ,OAAO;gBAACD,eAAed;YAAK;QAC9B;QAEA,IAAI,CAACuB,YAAY;YACfX,KAAKa,IAAI;YACT,OAAOJ,OAAOH,KAAK,CACjB,wFACA;gBAACQ,MAAMjC,UAAUkC,WAAW;YAAA;QAEhC;QAEA,MAAMC,cAAc,MAAMhC,kBAAkB;YAC1CK;YACAG;YACAkB;YACAjB,MAAMkB;YACNd;YACAF;YACAU,MAAM;YACNT;YACAgB;QACF;QACAZ,KAAKG,OAAO;QACZ,OAAO;YAACD,eAAec,YAAYZ,EAAE;QAAA;IACvC,EAAE,OAAOE,OAAO;QACdN,KAAKa,IAAI;QACT,MAAMP;IACR;AACF"}
1
+ {"version":3,"sources":["../../../src/actions/deploy/deployWorkbenchApp.ts"],"sourcesContent":["import {basename, dirname} from 'node:path'\nimport {createGzip} from 'node:zlib'\n\nimport {type AppVisibility, exitCodes, type Output} from '@sanity/cli-core'\nimport {spinner} from '@sanity/cli-core/ux'\nimport {pack} from 'tar-fs'\n\nimport {\n type BrettInterface,\n type BrettWorkspace,\n createApplication,\n createDeployment,\n updateApplication,\n} from '../../services/applications.js'\n\n/**\n * Deploy a workbench coreApp through Brett: redeploy when `appId` is set,\n * otherwise create the application at `slug`. Returns the application id for the\n * shell to report.\n * @internal\n */\nexport async function deployCoreApp(options: {\n appId: string | undefined\n icon?: string\n interfaces: readonly BrettInterface[]\n isAutoUpdating: boolean\n isSingleton?: boolean\n organizationId: string\n slug: string\n sourceDir: string\n title: string\n version: string\n visibility?: AppVisibility\n}): Promise<{applicationId: string}> {\n const {\n appId,\n icon,\n interfaces,\n isAutoUpdating,\n isSingleton,\n organizationId,\n slug,\n sourceDir,\n title,\n version,\n visibility,\n } = options\n const tarball = pack(dirname(sourceDir), {entries: [basename(sourceDir)]}).pipe(createGzip())\n\n const spin = spinner('Deploying...').start()\n try {\n if (appId) {\n await createDeployment({applicationId: appId, interfaces, isAutoUpdating, tarball, version})\n await updateApplication(appId, {title, ...(icon ? {icon} : {})})\n spin.succeed()\n return {applicationId: appId}\n }\n\n const {id} = await createApplication({\n icon,\n interfaces,\n isSingleton,\n organizationId,\n slug,\n tarball,\n title,\n type: 'coreApp',\n version,\n visibility,\n })\n spin.succeed()\n return {applicationId: id}\n } catch (error) {\n spin.clear()\n throw error\n }\n}\n\n/**\n * Deploy a workbench studio through Brett: redeploy when `appId` is set,\n * otherwise create the studio at `studioHost`. Returns the application id for\n * the shell to report; a missing `studioHost` on create is a usage error.\n * @internal\n */\nexport async function deployStudio(options: {\n appId: string | undefined\n icon?: string\n interfaces: readonly BrettInterface[]\n isAutoUpdating: boolean\n organizationId: string\n output: Output\n projectId: string | undefined\n sourceDir: string\n studioHost: string | undefined\n title: string\n version: string\n workspaces: readonly BrettWorkspace[]\n}): Promise<{applicationId: string}> {\n const {\n appId,\n icon,\n interfaces,\n isAutoUpdating,\n organizationId,\n output,\n projectId,\n sourceDir,\n studioHost,\n title,\n version,\n workspaces,\n } = options\n const tarball = pack(dirname(sourceDir), {entries: [basename(sourceDir)]}).pipe(createGzip())\n\n const spin = spinner('Deploying to sanity.studio').start()\n try {\n if (appId) {\n await createDeployment({\n applicationId: appId,\n interfaces,\n isAutoUpdating,\n tarball,\n version,\n workspaces,\n })\n await updateApplication(appId, {title, ...(icon ? {icon} : {})})\n spin.succeed()\n return {applicationId: appId}\n }\n\n if (!studioHost) {\n spin.fail()\n return output.error(\n 'No studio hostname configured. Set `studioHost` in sanity.cli.ts to create a studio.',\n {exit: exitCodes.USAGE_ERROR},\n )\n }\n\n const application = await createApplication({\n icon,\n interfaces,\n organizationId,\n projectId,\n slug: studioHost,\n tarball,\n title,\n type: 'studio',\n version,\n workspaces,\n })\n spin.succeed()\n return {applicationId: application.id}\n } catch (error) {\n spin.fail()\n throw error\n }\n}\n"],"names":["basename","dirname","createGzip","exitCodes","spinner","pack","createApplication","createDeployment","updateApplication","deployCoreApp","options","appId","icon","interfaces","isAutoUpdating","isSingleton","organizationId","slug","sourceDir","title","version","visibility","tarball","entries","pipe","spin","start","applicationId","succeed","id","type","error","clear","deployStudio","output","projectId","studioHost","workspaces","fail","exit","USAGE_ERROR","application"],"mappings":"AAAA,SAAQA,QAAQ,EAAEC,OAAO,QAAO,YAAW;AAC3C,SAAQC,UAAU,QAAO,YAAW;AAEpC,SAA4BC,SAAS,QAAoB,mBAAkB;AAC3E,SAAQC,OAAO,QAAO,sBAAqB;AAC3C,SAAQC,IAAI,QAAO,SAAQ;AAE3B,SAGEC,iBAAiB,EACjBC,gBAAgB,EAChBC,iBAAiB,QACZ,iCAAgC;AAEvC;;;;;CAKC,GACD,OAAO,eAAeC,cAAcC,OAYnC;IACC,MAAM,EACJC,KAAK,EACLC,IAAI,EACJC,UAAU,EACVC,cAAc,EACdC,WAAW,EACXC,cAAc,EACdC,IAAI,EACJC,SAAS,EACTC,KAAK,EACLC,OAAO,EACPC,UAAU,EACX,GAAGX;IACJ,MAAMY,UAAUjB,KAAKJ,QAAQiB,YAAY;QAACK,SAAS;YAACvB,SAASkB;SAAW;IAAA,GAAGM,IAAI,CAACtB;IAEhF,MAAMuB,OAAOrB,QAAQ,gBAAgBsB,KAAK;IAC1C,IAAI;QACF,IAAIf,OAAO;YACT,MAAMJ,iBAAiB;gBAACoB,eAAehB;gBAAOE;gBAAYC;gBAAgBQ;gBAASF;YAAO;YAC1F,MAAMZ,kBAAkBG,OAAO;gBAACQ;gBAAO,GAAIP,OAAO;oBAACA;gBAAI,IAAI,CAAC,CAAC;YAAC;YAC9Da,KAAKG,OAAO;YACZ,OAAO;gBAACD,eAAehB;YAAK;QAC9B;QAEA,MAAM,EAACkB,EAAE,EAAC,GAAG,MAAMvB,kBAAkB;YACnCM;YACAC;YACAE;YACAC;YACAC;YACAK;YACAH;YACAW,MAAM;YACNV;YACAC;QACF;QACAI,KAAKG,OAAO;QACZ,OAAO;YAACD,eAAeE;QAAE;IAC3B,EAAE,OAAOE,OAAO;QACdN,KAAKO,KAAK;QACV,MAAMD;IACR;AACF;AAEA;;;;;CAKC,GACD,OAAO,eAAeE,aAAavB,OAalC;IACC,MAAM,EACJC,KAAK,EACLC,IAAI,EACJC,UAAU,EACVC,cAAc,EACdE,cAAc,EACdkB,MAAM,EACNC,SAAS,EACTjB,SAAS,EACTkB,UAAU,EACVjB,KAAK,EACLC,OAAO,EACPiB,UAAU,EACX,GAAG3B;IACJ,MAAMY,UAAUjB,KAAKJ,QAAQiB,YAAY;QAACK,SAAS;YAACvB,SAASkB;SAAW;IAAA,GAAGM,IAAI,CAACtB;IAEhF,MAAMuB,OAAOrB,QAAQ,8BAA8BsB,KAAK;IACxD,IAAI;QACF,IAAIf,OAAO;YACT,MAAMJ,iBAAiB;gBACrBoB,eAAehB;gBACfE;gBACAC;gBACAQ;gBACAF;gBACAiB;YACF;YACA,MAAM7B,kBAAkBG,OAAO;gBAACQ;gBAAO,GAAIP,OAAO;oBAACA;gBAAI,IAAI,CAAC,CAAC;YAAC;YAC9Da,KAAKG,OAAO;YACZ,OAAO;gBAACD,eAAehB;YAAK;QAC9B;QAEA,IAAI,CAACyB,YAAY;YACfX,KAAKa,IAAI;YACT,OAAOJ,OAAOH,KAAK,CACjB,wFACA;gBAACQ,MAAMpC,UAAUqC,WAAW;YAAA;QAEhC;QAEA,MAAMC,cAAc,MAAMnC,kBAAkB;YAC1CM;YACAC;YACAG;YACAmB;YACAlB,MAAMmB;YACNd;YACAH;YACAW,MAAM;YACNV;YACAiB;QACF;QACAZ,KAAKG,OAAO;QACZ,OAAO;YAACD,eAAec,YAAYZ,EAAE;QAAA;IACvC,EAAE,OAAOE,OAAO;QACdN,KAAKa,IAAI;QACT,MAAMP;IACR;AACF"}
@@ -1,37 +1,53 @@
1
1
  import { hash } from 'node:crypto';
2
- import { MEDIA_LIBRARY_CONFIG_CONTRACT_VERSION, SERVICE_CONTRACT_VERSION, VIEW_CONTRACT_VERSION } from '../../contract.js';
2
+ import { interfaceModuleId, MEDIA_LIBRARY_CONFIG_CONTRACT_VERSION, SERVICE_CONTRACT_VERSION, VIEW_CONTRACT_VERSION } from '../../contract.js';
3
3
  import { isWorkbenchApp, readConfig } from '../../defineApp.js';
4
4
  /**
5
- * Map a workbench app's declarations to the interface records forwarded on its
6
- * registry entry: `views` → panels, `services` → workers, `entry` → the
7
- * navigable `app` view. `src` is the raw source, not a resolved URL. `undefined`
8
- * for a non-branded app; a studio that declares `entry` is rejected (studio app
9
- * views are not implemented yet). The config is not an interface — see
10
- * {@link deriveConfigs}. `version` is the module's contract version; the app
11
- * view has no versioned contract, so it carries none.
5
+ * Map a workbench app's declarations to its registry interface records:
6
+ * `views` → panels, `services` → workers, `entry` → the `app` view. Each mirrors
7
+ * a deployed record so the workbench loads a local interface like a deployed one.
8
+ * `undefined` for a non-branded app; a studio that declares `entry` is rejected
9
+ * (studio app views aren't implemented yet).
12
10
  */ export function deriveInterfaces(app, options) {
13
11
  if (!isWorkbenchApp(app)) return undefined;
14
12
  if (!options.isApp && app.entry !== undefined) {
15
13
  throw new Error('App views for studios are not implemented yet');
16
14
  }
17
- const toInterface = ({ name, src, title, type }, version)=>({
18
- name,
19
- src,
20
- title: title ?? name,
21
- type,
22
- version
23
- });
15
+ const interfaceId = (type, name)=>`${app.name}-${type}-${name}`;
16
+ const views = (app.views ?? []).map((view)=>({
17
+ id: interfaceId('panel', view.name),
18
+ metadata: null,
19
+ moduleId: interfaceModuleId('panel', view.name),
20
+ name: view.name,
21
+ src: view.src,
22
+ title: view.title ?? view.name,
23
+ type: 'panel',
24
+ version: String(VIEW_CONTRACT_VERSION)
25
+ }));
26
+ const services = (app.services ?? []).map((service)=>({
27
+ id: interfaceId('worker', service.name),
28
+ metadata: null,
29
+ moduleId: interfaceModuleId('worker', service.name),
30
+ name: service.name,
31
+ src: service.src,
32
+ title: service.title ?? service.name,
33
+ type: 'worker',
34
+ version: String(SERVICE_CONTRACT_VERSION)
35
+ }));
36
+ const appView = app.entry === undefined ? [] : [
37
+ {
38
+ id: interfaceId('app', app.name),
39
+ metadata: null,
40
+ moduleId: interfaceModuleId('app', app.name),
41
+ name: app.name,
42
+ src: app.entry,
43
+ title: app.title,
44
+ type: 'app'
45
+ }
46
+ ];
24
47
  return [
25
- ...(app.views ?? []).map((view)=>toInterface(view, VIEW_CONTRACT_VERSION)),
26
- ...(app.services ?? []).map((service)=>toInterface(service, SERVICE_CONTRACT_VERSION)),
27
- ...app.entry === undefined ? [] : [
28
- {
29
- name: app.name,
30
- src: app.entry,
31
- title: app.title,
32
- type: 'app'
33
- }
34
- ]
48
+ ...views,
49
+ ...services,
50
+ ...appView
35
51
  ];
36
52
  }
37
53
  /**
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/actions/dev/deriveInterfaces.ts"],"sourcesContent":["import {hash} from 'node:crypto'\n\nimport {type CliConfig} from '@sanity/cli-core'\n\nimport {\n MEDIA_LIBRARY_CONFIG_CONTRACT_VERSION,\n SERVICE_CONTRACT_VERSION,\n VIEW_CONTRACT_VERSION,\n} from '../../contract.js'\nimport {isWorkbenchApp, readConfig} from '../../defineApp.js'\nimport {type DevServerManifest} from './registry.js'\n\n/** One forwarded interface record on the dev-server registry entry. */\nexport type DevServerInterface = NonNullable<DevServerManifest['interfaces']>[number]\n\n/** One forwarded config on the dev-server registry entry. */\nexport type DevServerConfig = NonNullable<DevServerManifest['configs']>[number]\n\n/**\n * Map a workbench app's declarations to the interface records forwarded on its\n * registry entry: `views` → panels, `services` → workers, `entry` → the\n * navigable `app` view. `src` is the raw source, not a resolved URL. `undefined`\n * for a non-branded app; a studio that declares `entry` is rejected (studio app\n * views are not implemented yet). The config is not an interface — see\n * {@link deriveConfigs}. `version` is the module's contract version; the app\n * view has no versioned contract, so it carries none.\n */\nexport function deriveInterfaces(\n app: CliConfig['app'],\n options: {isApp: boolean},\n): DevServerInterface[] | undefined {\n if (!isWorkbenchApp(app)) return undefined\n\n if (!options.isApp && app.entry !== undefined) {\n throw new Error('App views for studios are not implemented yet')\n }\n\n const toInterface = (\n {name, src, title, type}: {name: string; src: string; title?: string; type: string},\n version: number,\n ): DevServerInterface => ({name, src, title: title ?? name, type, version})\n\n return [\n ...(app.views ?? []).map((view) => toInterface(view, VIEW_CONTRACT_VERSION)),\n ...(app.services ?? []).map((service) => toInterface(service, SERVICE_CONTRACT_VERSION)),\n ...(app.entry === undefined\n ? []\n : [{name: app.name, src: app.entry, title: app.title, type: 'app' as const}]),\n ]\n}\n\n/**\n * The named source files a config's generated module is built from, dispatched\n * per app type — the projection the exposes-set id keys on, so the generic HMR\n * tracker owns none of the per-type shape. Throws on an app type it can't\n * handle, so a new config family has to register its shape here.\n */\nexport function deriveConfigEntries(config: DevServerConfig): {name: string; src: string}[] {\n switch (config.appType) {\n case 'media-library': {\n return config.fields.map((field) => ({name: field.name, src: field.src}))\n }\n default: {\n throw new Error(`Cannot derive entries for unknown config appType: ${config.appType}`)\n }\n }\n}\n\n/**\n * The fields' schema *values* can't serialize — the workbench loads them from\n * the federation module. `src` stays on so the exposes-set id keys on it and a\n * repoint rebuilds. `appType` routes the config to the singleton (no app id to\n * key on). `id` is a content hash of the entry — it fills the\n * installation-config id slot deployed apps get from the applications API,\n * and the workbench keys change detection on it. `version` is the config\n * contract version the generated module exports, known before the module runs.\n */\nexport function deriveConfigs(app: CliConfig['app']): DevServerConfig[] {\n if (!isWorkbenchApp(app)) return []\n const config = readConfig(app)\n if (!config) return []\n const entry = {\n appType: config.appType,\n fields: config.fields.map((field) => ({\n name: field.name,\n public: field.public,\n src: field.src,\n title: field.title,\n })),\n moduleName: app.name,\n version: MEDIA_LIBRARY_CONFIG_CONTRACT_VERSION,\n }\n return [{...entry, id: hash('sha1', JSON.stringify(entry))}]\n}\n"],"names":["hash","MEDIA_LIBRARY_CONFIG_CONTRACT_VERSION","SERVICE_CONTRACT_VERSION","VIEW_CONTRACT_VERSION","isWorkbenchApp","readConfig","deriveInterfaces","app","options","undefined","isApp","entry","Error","toInterface","name","src","title","type","version","views","map","view","services","service","deriveConfigEntries","config","appType","fields","field","deriveConfigs","public","moduleName","id","JSON","stringify"],"mappings":"AAAA,SAAQA,IAAI,QAAO,cAAa;AAIhC,SACEC,qCAAqC,EACrCC,wBAAwB,EACxBC,qBAAqB,QAChB,oBAAmB;AAC1B,SAAQC,cAAc,EAAEC,UAAU,QAAO,qBAAoB;AAS7D;;;;;;;;CAQC,GACD,OAAO,SAASC,iBACdC,GAAqB,EACrBC,OAAyB;IAEzB,IAAI,CAACJ,eAAeG,MAAM,OAAOE;IAEjC,IAAI,CAACD,QAAQE,KAAK,IAAIH,IAAII,KAAK,KAAKF,WAAW;QAC7C,MAAM,IAAIG,MAAM;IAClB;IAEA,MAAMC,cAAc,CAClB,EAACC,IAAI,EAAEC,GAAG,EAAEC,KAAK,EAAEC,IAAI,EAA4D,EACnFC,UACwB,CAAA;YAACJ;YAAMC;YAAKC,OAAOA,SAASF;YAAMG;YAAMC;QAAO,CAAA;IAEzE,OAAO;WACF,AAACX,CAAAA,IAAIY,KAAK,IAAI,EAAE,AAAD,EAAGC,GAAG,CAAC,CAACC,OAASR,YAAYQ,MAAMlB;WAClD,AAACI,CAAAA,IAAIe,QAAQ,IAAI,EAAE,AAAD,EAAGF,GAAG,CAAC,CAACG,UAAYV,YAAYU,SAASrB;WAC1DK,IAAII,KAAK,KAAKF,YACd,EAAE,GACF;YAAC;gBAACK,MAAMP,IAAIO,IAAI;gBAAEC,KAAKR,IAAII,KAAK;gBAAEK,OAAOT,IAAIS,KAAK;gBAAEC,MAAM;YAAc;SAAE;KAC/E;AACH;AAEA;;;;;CAKC,GACD,OAAO,SAASO,oBAAoBC,MAAuB;IACzD,OAAQA,OAAOC,OAAO;QACpB,KAAK;YAAiB;gBACpB,OAAOD,OAAOE,MAAM,CAACP,GAAG,CAAC,CAACQ,QAAW,CAAA;wBAACd,MAAMc,MAAMd,IAAI;wBAAEC,KAAKa,MAAMb,GAAG;oBAAA,CAAA;YACxE;QACA;YAAS;gBACP,MAAM,IAAIH,MAAM,CAAC,kDAAkD,EAAEa,OAAOC,OAAO,EAAE;YACvF;IACF;AACF;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASG,cAActB,GAAqB;IACjD,IAAI,CAACH,eAAeG,MAAM,OAAO,EAAE;IACnC,MAAMkB,SAASpB,WAAWE;IAC1B,IAAI,CAACkB,QAAQ,OAAO,EAAE;IACtB,MAAMd,QAAQ;QACZe,SAASD,OAAOC,OAAO;QACvBC,QAAQF,OAAOE,MAAM,CAACP,GAAG,CAAC,CAACQ,QAAW,CAAA;gBACpCd,MAAMc,MAAMd,IAAI;gBAChBgB,QAAQF,MAAME,MAAM;gBACpBf,KAAKa,MAAMb,GAAG;gBACdC,OAAOY,MAAMZ,KAAK;YACpB,CAAA;QACAe,YAAYxB,IAAIO,IAAI;QACpBI,SAASjB;IACX;IACA,OAAO;QAAC;YAAC,GAAGU,KAAK;YAAEqB,IAAIhC,KAAK,QAAQiC,KAAKC,SAAS,CAACvB;QAAO;KAAE;AAC9D"}
1
+ {"version":3,"sources":["../../../src/actions/dev/deriveInterfaces.ts"],"sourcesContent":["import {hash} from 'node:crypto'\n\nimport {type CliConfig} from '@sanity/cli-core'\n\nimport {\n interfaceModuleId,\n MEDIA_LIBRARY_CONFIG_CONTRACT_VERSION,\n SERVICE_CONTRACT_VERSION,\n VIEW_CONTRACT_VERSION,\n} from '../../contract.js'\nimport {isWorkbenchApp, readConfig} from '../../defineApp.js'\nimport {type DevServerManifest} from './registry.js'\n\n/** One forwarded interface record on the dev-server registry entry. */\nexport type DevServerInterface = NonNullable<DevServerManifest['interfaces']>[number]\n\n/** One forwarded config on the dev-server registry entry. */\nexport type DevServerConfig = NonNullable<DevServerManifest['configs']>[number]\n\n/**\n * Map a workbench app's declarations to its registry interface records:\n * `views` → panels, `services` → workers, `entry` → the `app` view. Each mirrors\n * a deployed record so the workbench loads a local interface like a deployed one.\n * `undefined` for a non-branded app; a studio that declares `entry` is rejected\n * (studio app views aren't implemented yet).\n */\nexport function deriveInterfaces(\n app: CliConfig['app'],\n options: {isApp: boolean},\n): DevServerInterface[] | undefined {\n if (!isWorkbenchApp(app)) return undefined\n\n if (!options.isApp && app.entry !== undefined) {\n throw new Error('App views for studios are not implemented yet')\n }\n\n const interfaceId = (type: string, name: string): string => `${app.name}-${type}-${name}`\n\n const views = (app.views ?? []).map(\n (view): DevServerInterface => ({\n id: interfaceId('panel', view.name),\n metadata: null,\n moduleId: interfaceModuleId('panel', view.name),\n name: view.name,\n src: view.src,\n title: view.title ?? view.name,\n type: 'panel',\n version: String(VIEW_CONTRACT_VERSION),\n }),\n )\n\n const services = (app.services ?? []).map(\n (service): DevServerInterface => ({\n id: interfaceId('worker', service.name),\n metadata: null,\n moduleId: interfaceModuleId('worker', service.name),\n name: service.name,\n src: service.src,\n title: service.title ?? service.name,\n type: 'worker',\n version: String(SERVICE_CONTRACT_VERSION),\n }),\n )\n\n const appView: DevServerInterface[] =\n app.entry === undefined\n ? []\n : [\n {\n id: interfaceId('app', app.name),\n metadata: null,\n moduleId: interfaceModuleId('app', app.name),\n name: app.name,\n src: app.entry,\n title: app.title,\n type: 'app',\n },\n ]\n\n return [...views, ...services, ...appView]\n}\n\n/**\n * The named source files a config's generated module is built from, dispatched\n * per app type — the projection the exposes-set id keys on, so the generic HMR\n * tracker owns none of the per-type shape. Throws on an app type it can't\n * handle, so a new config family has to register its shape here.\n */\nexport function deriveConfigEntries(config: DevServerConfig): {name: string; src: string}[] {\n switch (config.appType) {\n case 'media-library': {\n return config.fields.map((field) => ({name: field.name, src: field.src}))\n }\n default: {\n throw new Error(`Cannot derive entries for unknown config appType: ${config.appType}`)\n }\n }\n}\n\n/**\n * The fields' schema *values* can't serialize — the workbench loads them from\n * the federation module. `src` stays on so the exposes-set id keys on it and a\n * repoint rebuilds. `appType` routes the config to the singleton (no app id to\n * key on). `id` is a content hash of the entry — it fills the\n * installation-config id slot deployed apps get from the applications API,\n * and the workbench keys change detection on it. `version` is the config\n * contract version the generated module exports, known before the module runs.\n */\nexport function deriveConfigs(app: CliConfig['app']): DevServerConfig[] {\n if (!isWorkbenchApp(app)) return []\n const config = readConfig(app)\n if (!config) return []\n const entry = {\n appType: config.appType,\n fields: config.fields.map((field) => ({\n name: field.name,\n public: field.public,\n src: field.src,\n title: field.title,\n })),\n moduleName: app.name,\n version: MEDIA_LIBRARY_CONFIG_CONTRACT_VERSION,\n }\n return [{...entry, id: hash('sha1', JSON.stringify(entry))}]\n}\n"],"names":["hash","interfaceModuleId","MEDIA_LIBRARY_CONFIG_CONTRACT_VERSION","SERVICE_CONTRACT_VERSION","VIEW_CONTRACT_VERSION","isWorkbenchApp","readConfig","deriveInterfaces","app","options","undefined","isApp","entry","Error","interfaceId","type","name","views","map","view","id","metadata","moduleId","src","title","version","String","services","service","appView","deriveConfigEntries","config","appType","fields","field","deriveConfigs","public","moduleName","JSON","stringify"],"mappings":"AAAA,SAAQA,IAAI,QAAO,cAAa;AAIhC,SACEC,iBAAiB,EACjBC,qCAAqC,EACrCC,wBAAwB,EACxBC,qBAAqB,QAChB,oBAAmB;AAC1B,SAAQC,cAAc,EAAEC,UAAU,QAAO,qBAAoB;AAS7D;;;;;;CAMC,GACD,OAAO,SAASC,iBACdC,GAAqB,EACrBC,OAAyB;IAEzB,IAAI,CAACJ,eAAeG,MAAM,OAAOE;IAEjC,IAAI,CAACD,QAAQE,KAAK,IAAIH,IAAII,KAAK,KAAKF,WAAW;QAC7C,MAAM,IAAIG,MAAM;IAClB;IAEA,MAAMC,cAAc,CAACC,MAAcC,OAAyB,GAAGR,IAAIQ,IAAI,CAAC,CAAC,EAAED,KAAK,CAAC,EAAEC,MAAM;IAEzF,MAAMC,QAAQ,AAACT,CAAAA,IAAIS,KAAK,IAAI,EAAE,AAAD,EAAGC,GAAG,CACjC,CAACC,OAA8B,CAAA;YAC7BC,IAAIN,YAAY,SAASK,KAAKH,IAAI;YAClCK,UAAU;YACVC,UAAUrB,kBAAkB,SAASkB,KAAKH,IAAI;YAC9CA,MAAMG,KAAKH,IAAI;YACfO,KAAKJ,KAAKI,GAAG;YACbC,OAAOL,KAAKK,KAAK,IAAIL,KAAKH,IAAI;YAC9BD,MAAM;YACNU,SAASC,OAAOtB;QAClB,CAAA;IAGF,MAAMuB,WAAW,AAACnB,CAAAA,IAAImB,QAAQ,IAAI,EAAE,AAAD,EAAGT,GAAG,CACvC,CAACU,UAAiC,CAAA;YAChCR,IAAIN,YAAY,UAAUc,QAAQZ,IAAI;YACtCK,UAAU;YACVC,UAAUrB,kBAAkB,UAAU2B,QAAQZ,IAAI;YAClDA,MAAMY,QAAQZ,IAAI;YAClBO,KAAKK,QAAQL,GAAG;YAChBC,OAAOI,QAAQJ,KAAK,IAAII,QAAQZ,IAAI;YACpCD,MAAM;YACNU,SAASC,OAAOvB;QAClB,CAAA;IAGF,MAAM0B,UACJrB,IAAII,KAAK,KAAKF,YACV,EAAE,GACF;QACE;YACEU,IAAIN,YAAY,OAAON,IAAIQ,IAAI;YAC/BK,UAAU;YACVC,UAAUrB,kBAAkB,OAAOO,IAAIQ,IAAI;YAC3CA,MAAMR,IAAIQ,IAAI;YACdO,KAAKf,IAAII,KAAK;YACdY,OAAOhB,IAAIgB,KAAK;YAChBT,MAAM;QACR;KACD;IAEP,OAAO;WAAIE;WAAUU;WAAaE;KAAQ;AAC5C;AAEA;;;;;CAKC,GACD,OAAO,SAASC,oBAAoBC,MAAuB;IACzD,OAAQA,OAAOC,OAAO;QACpB,KAAK;YAAiB;gBACpB,OAAOD,OAAOE,MAAM,CAACf,GAAG,CAAC,CAACgB,QAAW,CAAA;wBAAClB,MAAMkB,MAAMlB,IAAI;wBAAEO,KAAKW,MAAMX,GAAG;oBAAA,CAAA;YACxE;QACA;YAAS;gBACP,MAAM,IAAIV,MAAM,CAAC,kDAAkD,EAAEkB,OAAOC,OAAO,EAAE;YACvF;IACF;AACF;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASG,cAAc3B,GAAqB;IACjD,IAAI,CAACH,eAAeG,MAAM,OAAO,EAAE;IACnC,MAAMuB,SAASzB,WAAWE;IAC1B,IAAI,CAACuB,QAAQ,OAAO,EAAE;IACtB,MAAMnB,QAAQ;QACZoB,SAASD,OAAOC,OAAO;QACvBC,QAAQF,OAAOE,MAAM,CAACf,GAAG,CAAC,CAACgB,QAAW,CAAA;gBACpClB,MAAMkB,MAAMlB,IAAI;gBAChBoB,QAAQF,MAAME,MAAM;gBACpBb,KAAKW,MAAMX,GAAG;gBACdC,OAAOU,MAAMV,KAAK;YACpB,CAAA;QACAa,YAAY7B,IAAIQ,IAAI;QACpBS,SAASvB;IACX;IACA,OAAO;QAAC;YAAC,GAAGU,KAAK;YAAEQ,IAAIpB,KAAK,QAAQsC,KAAKC,SAAS,CAAC3B;QAAO;KAAE;AAC9D"}
@@ -2,9 +2,28 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, watch, wr
2
2
  import { join } from 'node:path';
3
3
  import { coreAppManifestSchema, getSanityDataDir, studioManifestSchema, subdebug } from '@sanity/cli-core';
4
4
  import { z } from 'zod/mini';
5
+ import { AppInterfaceMetadataSchema } from '../../contract.js';
5
6
  import { canonicalizeWatchDir } from './canonicalizeWatchDir.js';
6
7
  import { getProcessStartTime, isOurProcess } from './processLiveness.js';
7
- const devDebug = subdebug('dev');
8
+ /**
9
+ * The dev-server registry: how a running `sanity dev` / `sanity start` process
10
+ * advertises itself so the workbench on this machine can find and load it.
11
+ *
12
+ * Two kinds of file under `~/.sanity/dev-servers/` do the coordinating:
13
+ *
14
+ * - `<pid>.json` — one per running app/studio server, holding where it's served
15
+ * plus its inlined manifest and interfaces. The workbench reads these to
16
+ * discover and render local apps. Written by `registerDevServer`, watched by
17
+ * `watchRegistry`.
18
+ * - `workbench.lock` — a single machine-wide lock, so only one workbench shell
19
+ * runs at a time and later `dev`s register into it instead of starting their
20
+ * own. Managed by `acquireWorkbenchLock` / `readWorkbenchLock`.
21
+ *
22
+ * Both files belong to the process that created them and must not outlive it.
23
+ * Three things keep that true: an explicit `release()` on clean shutdown, an
24
+ * `exit` backstop for abrupt exits (`unlinkOnProcessExit`), and a dead-pid prune
25
+ * on read (`isOurProcess`) that clears whatever a crashed process left behind.
26
+ */ const devDebug = subdebug('dev');
8
27
  /** Bump when the manifest/lock shape changes in a breaking way. */ const REGISTRY_VERSION = 1;
9
28
  /**
10
29
  * The current process's start time as reported by the OS, for the `startedAt`
@@ -14,6 +33,34 @@ const devDebug = subdebug('dev');
14
33
  */ function ownStartedAt() {
15
34
  return (getProcessStartTime(process.pid) ?? new Date()).toISOString();
16
35
  }
36
+ const interfaceBaseFields = {
37
+ /** CLI-minted for a local interface; a deployed one gets its id from Brett. */ id: z.string(),
38
+ moduleId: z.string(),
39
+ name: z.string(),
40
+ /** Raw source vite serves; a deployed interface carries only the `moduleId`. */ src: z.string(),
41
+ title: z.string(),
42
+ version: z.optional(z.string())
43
+ };
44
+ /**
45
+ * A forwarded interface, discriminated on `type`. Kept outside the manifest so
46
+ * the workbench renders local panels and runs workers without a deploy.
47
+ */ const devServerInterfaceSchema = z.discriminatedUnion('type', [
48
+ z.object({
49
+ ...interfaceBaseFields,
50
+ metadata: z.nullable(AppInterfaceMetadataSchema),
51
+ type: z.literal('app')
52
+ }),
53
+ z.object({
54
+ ...interfaceBaseFields,
55
+ metadata: z.null(),
56
+ type: z.literal('panel')
57
+ }),
58
+ z.object({
59
+ ...interfaceBaseFields,
60
+ metadata: z.null(),
61
+ type: z.literal('worker')
62
+ })
63
+ ]);
17
64
  const devServerManifestSchema = z.object({
18
65
  /**
19
66
  * Field schema *values* load from the federation module; each field's `src`
@@ -40,24 +87,7 @@ const devServerManifestSchema = z.object({
40
87
  }))),
41
88
  host: z.string(),
42
89
  id: z.optional(z.string()),
43
- /**
44
- * Interfaces the app exposes, mapped from the declared `views` (dock panels,
45
- * `type: "panel"`) and `services` (background workers,
46
- * `type: "worker"`). A service is just an interface, so both live
47
- * in this one list. Carried separately from the manifest — interfaces live in
48
- * the application service, not the manifest — so the workbench can render
49
- * local panels and run local workers without a deploy. `src` is the
50
- * declared source file; `title` defaults to `name`. Lenient by design; the
51
- * workbench is the authority on the interface shape.
52
- */ interfaces: z.optional(z.array(z.object({
53
- name: z.string(),
54
- src: z.string(),
55
- title: z.string(),
56
- type: z.string(),
57
- // Contract version the interface's generated module exports; the app
58
- // view has no versioned contract and carries none.
59
- version: z.optional(z.number())
60
- }))),
90
+ interfaces: z.optional(z.array(devServerInterfaceSchema)),
61
91
  /**
62
92
  * Inlined manifest — either a {@link StudioManifest} or {@link CoreAppManifest},
63
93
  * validated against the shared cli-core schemas. The registry stores and
@@ -88,6 +118,39 @@ const devServerManifestSchema = z.object({
88
118
  */ function getRegistryDir() {
89
119
  return join(getSanityDataDir(), 'dev-servers');
90
120
  }
121
+ // One shared `exit` listener drives every registered cleanup, so N locks/entries
122
+ // don't each add a listener and trip Node's MaxListeners warning.
123
+ const exitCleanups = new Set();
124
+ let exitListenerInstalled = false;
125
+ function runExitCleanups() {
126
+ for (const cleanup of exitCleanups)cleanup();
127
+ }
128
+ /** Exercise the exit backstop in tests without terminating the process; not part
129
+ * of the package's public surface. */ export const runRegistryExitCleanupForTesting = runExitCleanups;
130
+ /**
131
+ * Delete a registry file synchronously on process exit, as a backstop for abrupt
132
+ * termination. Vite installs its own SIGTERM handler that calls `process.exit()`,
133
+ * which can outrun the async server teardown and leave the lock or registry entry
134
+ * behind — a stray dev-server that lingers until the dead-pid prune clears it. The
135
+ * `exit` event only runs synchronous work, hence `unlinkSync`. `ownedByUs` guards
136
+ * the shared lock so a successor that reacquired it isn't wiped. Returns a
137
+ * detacher to call after a clean release.
138
+ */ function unlinkOnProcessExit(filePath, ownedByUs) {
139
+ const cleanup = ()=>{
140
+ if (!ownedByUs()) return;
141
+ try {
142
+ unlinkSync(filePath);
143
+ } catch {
144
+ // The file may already have been removed during shutdown.
145
+ }
146
+ };
147
+ exitCleanups.add(cleanup);
148
+ if (!exitListenerInstalled) {
149
+ exitListenerInstalled = true;
150
+ process.once('exit', runExitCleanups);
151
+ }
152
+ return ()=>exitCleanups.delete(cleanup);
153
+ }
91
154
  /**
92
155
  * Write a manifest file for the current process and return a handle with a
93
156
  * `release` function that removes it plus an `update` function for patching
@@ -110,9 +173,12 @@ const devServerManifestSchema = z.object({
110
173
  // manifest extraction) landing after `release()` has deleted the file —
111
174
  // without this, the update would re-create the registry entry and leak.
112
175
  let released = false;
176
+ // The file is pid-named, so it's always ours to remove on exit.
177
+ const detachExitCleanup = unlinkOnProcessExit(filePath, ()=>!released);
113
178
  return {
114
179
  release () {
115
180
  released = true;
181
+ detachExitCleanup();
116
182
  try {
117
183
  unlinkSync(filePath);
118
184
  } catch {
@@ -273,8 +339,22 @@ function pruneWorkbenchLock(lockPath) {
273
339
  flag: 'wx'
274
340
  });
275
341
  devDebug('Workbench lock acquired');
342
+ let released = false;
343
+ // Only wipe the lock on exit if it's still ours — a successor that reacquired
344
+ // it after our own release must not be clobbered.
345
+ const detachExitCleanup = unlinkOnProcessExit(lockPath, ()=>{
346
+ if (released) return false;
347
+ try {
348
+ const disk = parseLockContents(readFileSync(lockPath, 'utf8'));
349
+ return disk?.pid === process.pid && disk.startedAt === startedAt;
350
+ } catch {
351
+ return false;
352
+ }
353
+ });
276
354
  return {
277
355
  release () {
356
+ released = true;
357
+ detachExitCleanup();
278
358
  try {
279
359
  unlinkSync(lockPath);
280
360
  } catch {