specrails-desktop 2.9.1 → 2.10.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 (36) hide show
  1. package/client/dist/assets/{ActivityFeedPage-DpQzYMBz.js → ActivityFeedPage-qPnIozX9.js} +1 -1
  2. package/client/dist/assets/{AgentsPage-29fCY8qV.js → AgentsPage-B_8WZUUI.js} +1 -1
  3. package/client/dist/assets/{AnalyticsPage-BwGtS6Hf.js → AnalyticsPage-DCnFtl8E.js} +1 -1
  4. package/client/dist/assets/{BarChart-CTR97DVC.js → BarChart-Cr7__BAU.js} +1 -1
  5. package/client/dist/assets/{CodePage-yAAxKasA.js → CodePage-C7uJgpF8.js} +1 -1
  6. package/client/dist/assets/{DesktopAnalyticsPage-BdK_XpsD.js → DesktopAnalyticsPage-DqCEHy0T.js} +1 -1
  7. package/client/dist/assets/{DocsDialog-BaE0cLlL.js → DocsDialog-C_jIYef_.js} +1 -1
  8. package/client/dist/assets/{DocsPage-c1FgZX8_.js → DocsPage-CTifuwaa.js} +1 -1
  9. package/client/dist/assets/{ExportDropdown-lPv_yDen.js → ExportDropdown-BcmU6I16.js} +1 -1
  10. package/client/dist/assets/{IntegrationsPage-DOpxRe7G.js → IntegrationsPage-DZntBwnd.js} +1 -1
  11. package/client/dist/assets/{JobDetailPage-5ExzXY-F.js → JobDetailPage-4ncqbKxF.js} +1 -1
  12. package/client/dist/assets/{JobsPage-iW7WuPAc.js → JobsPage-CapLnvIs.js} +1 -1
  13. package/client/dist/assets/{dist-js-A8aSaLng.js → dist-js-DKGy2gD-.js} +1 -1
  14. package/client/dist/assets/{dist-js-CD_m3Xj5.js → dist-js-DvPNad3t.js} +1 -1
  15. package/client/dist/assets/index-Cc3LCzBq.css +2 -0
  16. package/client/dist/assets/{index-DRhFPNAv.js → index-IgMtikGD.js} +11 -11
  17. package/client/dist/assets/{lib-1vkTuLY7.js → lib-BouqFmKc.js} +1 -1
  18. package/client/dist/assets/{settings-BRaLLSVi.js → settings-BwMQ1lx5.js} +1 -1
  19. package/client/dist/assets/{settings-BI_cVCqN.js → settings-CewqtD9V.js} +1 -1
  20. package/client/dist/assets/{settings-BcqH0oea.js → settings-CqCY8gZq.js} +1 -1
  21. package/client/dist/assets/settings-Cx97tHs5.js +1 -0
  22. package/client/dist/assets/{settings-pT3MzfRu.js → settings-CyixAx0X.js} +1 -1
  23. package/client/dist/assets/{settings-GOBKOTGl.js → settings-DkB9Wh-f.js} +1 -1
  24. package/client/dist/assets/{settings-D6QMBlGQ.js → settings-EMaKBwE0.js} +1 -1
  25. package/client/dist/assets/{settings-u-16ISHt.js → settings-Puea5c8v.js} +1 -1
  26. package/client/dist/assets/{useProjectCache-CSi2xHri.js → useProjectCache-vvFIixVa.js} +1 -1
  27. package/client/dist/index.html +3 -3
  28. package/package.json +1 -1
  29. package/server/dist/core-update-manager.js +219 -0
  30. package/server/dist/desktop-router.js +43 -0
  31. package/server/dist/framework-manager.js +48 -10
  32. package/server/dist/path-resolver.js +21 -0
  33. package/server/dist/semver-lite.js +92 -0
  34. package/server/dist/setup-manager.js +8 -1
  35. package/client/dist/assets/index-D6BaYRRU.css +0 -2
  36. package/client/dist/assets/settings-C0-7Fpxg.js +0 -1
@@ -1 +1 @@
1
- var e={title:`语言`,description:`Desktop 全局界面语言。即时生效,无需重启。`,selectLabel:`界面语言`,updateFailed:`无法保存语言偏好`},t={title:`项目设置`},n={saveFailed:`保存失败:{{message}}`},r={title:`流水线遥测`,description:`采集 token 用量、阶段耗时和子 Agent 活动以供诊断导出。默认关闭。`,toggleLabel:`启用流水线遥测`,toggleDescription:`开启后,流水线任务的 OTEL 数据将在本地采集。在任意任务卡片上使用<mono>导出诊断</mono>按钮即可下载。`,enabled:`已启用流水线遥测`,disabled:`已禁用流水线遥测`,saveFailed:`保存遥测设置失败`},i={title:`Rail 前置提示词`,description:`追加到 implement 与 batch-implement rail 任务中的项目专属附加指令,位于工单上下文之后、开始执行之前。`,label:`前置提示词`,placeholder:`示例:优先采用增量修改,保持迁移向后兼容,并为每次 rail 变更添加测试。`,helper:`用于应随每次 rail 实现运行附带的稳定项目指引。`,saveButton:`保存前置提示词`,cleared:`前置提示词已清除`,saved:`前置提示词已保存`,saveFailed:`保存前置提示词失败`},a={title:`Ultracode 前置提示词`,description:`在 Ultracode(仅限 Claude 的 rail)模式下发送给 Claude 的指令。Ultracode 跳过 OpenSpec 流水线——直接把该前置提示词加上 spec 文本交给 Claude,让其自主实现。留空则使用内置默认值。`,label:`Ultra 前置提示词`,placeholder:`留空以使用默认的 Ultracode 指令。`,helper:`spec 文本会自动追加在该前置提示词之后。留空 = 使用默认值。`,saveButton:`保存 Ultra 前置提示词`,resetToDefault:`Ultra 前置提示词已重置为默认值`,saved:`Ultra 前置提示词已保存`,saveFailed:`保存 Ultra 前置提示词失败`},o={title:`预算`,description:`为此项目设置每日支出上限。达到上限时队列自动暂停。`,dailyLabel:`每日预算(USD)`,dailyHelper:`留空表示禁用。支出按最近 24 小时计算。`,dailyPlaceholder:`如 5.00`,perJobLabel:`单任务成本告警(USD)`,perJobHelper:`当此项目中的单个任务超过此金额时告警。`,perJobPlaceholder:`如 0.50`,invalidNumber:`请输入正数,或留空以禁用`,dailyRemoved:`已移除每日预算`,dailySet:"每日预算已设为 ${{amount}}",perJobAlertDisabled:`已禁用单任务成本告警`,alertSet:"已设置任务超过 ${{amount}} 时告警",saveBudgetFailed:`保存预算失败`,saveThresholdFailed:`保存阈值失败`},s={title:`Desktop 设置`,description:`管理已注册的项目并查看 Desktop 信息。`,registeredProjects:`已注册项目`,noProjects:`尚未注册任何项目`,techUrlDescription:`specrails-tech API 的基础 URL(默认:http://localhost:3000)`,techUrlSaved:`specrails-tech URL 已保存`,techUrlSaveFailed:`保存 URL 失败`,budgetAlertsHeading:`预算与告警`,dailyBudgetLabel:`Desktop 每日预算(USD)`,dailyBudgetHelper:`所有项目的全局每日支出上限。超出后队列自动暂停。`,dailyBudgetPlaceholder:`如 10.00`,perJobHelper:`当单个任务超过此金额时告警。留空表示禁用。`,costAlertsDisabled:`已禁用成本告警`,dailyBudgetRemoved:`已移除 Desktop 每日预算`,dailyBudgetSet:"Desktop 每日预算已设为 ${{amount}}",dailyBudgetSaveFailed:`保存 Desktop 每日预算失败`,projectRemoved:`项目已移除`,projectRemoveFailed:`移除项目失败`,onboardingHeading:`新手引导`,platformTour:`平台导览`,platformTourDescription:`重播欢迎向导,回顾核心功能。`,replayTour:`重播导览`,terminalPanelHeading:`终端面板`,infoHeading:`Desktop 信息`,infoPort:`端口`,infoProjects:`项目`,infoDb:`Desktop 数据库`},c={heading:`系统通知`,description:`任务完成或失败时显示原生桌面通知。仅在标签页未聚焦时显示通知。`,enableLabel:`启用系统通知`,notifyOn:`通知时机:`,filterAll:`全部(完成与失败)`,filterCompleted:`仅完成`,filterFailed:`仅失败`,enabledToast:`已启用系统通知`,disabledToast:`已禁用系统通知`},l={heading:`出站 Webhook`,description:`在 Desktop 事件发生时通知外部工具(Slack、Zapier、CI/CD)。设置密钥后,请求会通过 <code>X-Specrails-Signature</code> 进行签名。`,eventJobCompleted:`任务完成`,eventJobFailed:`任务失败`,eventDailyBudgetExceeded:`超出每日预算`,statusOn:`开`,statusOff:`关`,disable:`禁用`,enable:`启用`,sendTestPing:`发送测试 ping`,addHeading:`添加 Webhook`,secretPlaceholder:`签名密钥(可选)`,addButton:`添加 Webhook`,urlRequired:`URL 为必填项`,selectEvent:`请至少选择一个事件`,added:`Webhook 已添加`,addFailed:`添加 Webhook 失败`,updateFailed:`更新 Webhook 失败`,removed:`Webhook 已移除`,removeFailed:`移除 Webhook 失败`,testPingSent:`测试 ping 已发送`,testPingFailed:`发送测试 ping 失败`},u={title:`终端面板`,desktopDescription:`Desktop 全局默认值,应用于所有项目,除非设置了项目级覆盖。`,projectDescription:`终端面板的项目级覆盖。保持字段不变即继承 Desktop 默认值。`,fontFamily:`字体`,fontSize:`字号({{min}}–{{max}})`,renderMode:`渲染模式`,copyOnSelect:`选中即复制`,shellIntegration:`Shell 集成(OSC 133 标记)`,notifyLongRunning:`长耗时命令通知`,longCommandThreshold:`长命令阈值(ms)`,imageRendering:`内联图片渲染(Sixel + iTerm2)`,browserShortcutUrl:`浏览器快捷方式 URL`,quickScript:`快捷脚本(粘贴到活动终端——需手动按 Enter)`,reset:`重置`,unsavedChanges:`有未保存的更改`,clearOverride:`清除覆盖`,clear:`清除`,inheritingDefault:`正在继承 Desktop 默认值`,nothingToSave:`没有可保存的内容`,saved:`终端设置已保存`},d={heading:`代码板块`,summaryLanguage:`摘要语言`,monthlyBudget:`每月预算(USD)`,budgetHelper:"每个自然月 `surface=file-summary` 支出的上限。用户手动发起的重新生成可超出此限制。"},f={taglines:{dracula:`经典原版 — 紫色调暗色主题,霓虹色点缀鲜明`,"aurora-light":`高级浅色 — 受 Linear 启发的靛蓝配暖白底色`,"obsidian-dark":`高级深色 — 近黑蓝调配电光色点缀`,matrix:`磷光终端 — 绿黑底色上的柔和薄荷绿`,specrails:`品牌主题 — 深海军靛蓝配饱和青色点缀`},heading:`外观`,themeGroupLabel:`主题`,currentlyActive:`当前使用中`,updateFailed:`更新主题失败`},p={heading:`移动伴侣`,description:`通过手机上的 SpecRails Companion 应用控制 Specrails,完全经由本地网络。默认关闭。`,accessOn:`移动访问已开启`,accessOff:`移动访问已关闭`,listeningOnPort:`正在监听端口 {{port}}`,notListening:`未在监听`,turnOn:`开启`,turnOff:`关闭`,pairWebDevice:`配对网页伴侣`,reset:`重置`,resetConfirm:`确定要重置移动身份吗?所有已配对设备都将被吊销,必须重新配对。`,identityReset:`移动身份已重置`,enableFailed:`无法启用移动访问:{{message}}`,disableFailed:`无法禁用移动访问:{{message}}`,windowsFirewall:`首次启用时,Windows 防火墙会询问是否允许 SpecRails 服务器——请选择“允许在专用网络上”。`,pairedDevices:`已配对设备`,noDevices:`尚无已配对设备。`,revokeDevice:`吊销 {{name}}`},m={title:`配对网页伴侣`,description:`在手机上打开 specrails.dev/companion-app,然后用它扫描此二维码。`,startFailed:`无法开始配对:{{error}}`,showThenScan:`手机扫描此码后,点按“扫描手机的二维码”,并将摄像头对准手机。`,scanAnswer:`扫描手机的二维码`,scanning:`请将摄像头对准手机上的二维码…`,cameraFailed:`摄像头不可用:{{message}}`,notAnswer:`该二维码不是配对应答。`,connecting:`正在连接…`,paired:`✓ 已配对`,pairedToast:`网页伴侣已配对`,answerRejected:`桌面端拒绝了该码。请重新生成一个并重试。`,copyCode:`复制配对码`,codeCopied:`配对码已复制`,cancel:`取消`},h={language:e,page:t,errors:n,telemetry:r,prePrompt:i,ultraPrePrompt:a,budget:o,desktop:s,notifications:c,webhooks:l,terminal:u,codeSection:d,appearance:f,mobile:p,pairWeb:m};export{f as appearance,o as budget,d as codeSection,h as default,s as desktop,n as errors,e as language,p as mobile,c as notifications,t as page,m as pairWeb,i as prePrompt,r as telemetry,u as terminal,a as ultraPrePrompt,l as webhooks};
1
+ var e={title:`语言`,description:`Desktop 全局界面语言。即时生效,无需重启。`,selectLabel:`界面语言`,updateFailed:`无法保存语言偏好`},t={title:`项目设置`},n={saveFailed:`保存失败:{{message}}`},r={title:`流水线遥测`,description:`采集 token 用量、阶段耗时和子 Agent 活动以供诊断导出。默认关闭。`,toggleLabel:`启用流水线遥测`,toggleDescription:`开启后,流水线任务的 OTEL 数据将在本地采集。在任意任务卡片上使用<mono>导出诊断</mono>按钮即可下载。`,enabled:`已启用流水线遥测`,disabled:`已禁用流水线遥测`,saveFailed:`保存遥测设置失败`},i={title:`Rail 前置提示词`,description:`追加到 implement 与 batch-implement rail 任务中的项目专属附加指令,位于工单上下文之后、开始执行之前。`,label:`前置提示词`,placeholder:`示例:优先采用增量修改,保持迁移向后兼容,并为每次 rail 变更添加测试。`,helper:`用于应随每次 rail 实现运行附带的稳定项目指引。`,saveButton:`保存前置提示词`,cleared:`前置提示词已清除`,saved:`前置提示词已保存`,saveFailed:`保存前置提示词失败`},a={title:`Ultracode 前置提示词`,description:`在 Ultracode(仅限 Claude 的 rail)模式下发送给 Claude 的指令。Ultracode 跳过 OpenSpec 流水线——直接把该前置提示词加上 spec 文本交给 Claude,让其自主实现。留空则使用内置默认值。`,label:`Ultra 前置提示词`,placeholder:`留空以使用默认的 Ultracode 指令。`,helper:`spec 文本会自动追加在该前置提示词之后。留空 = 使用默认值。`,saveButton:`保存 Ultra 前置提示词`,resetToDefault:`Ultra 前置提示词已重置为默认值`,saved:`Ultra 前置提示词已保存`,saveFailed:`保存 Ultra 前置提示词失败`},o={title:`预算`,description:`为此项目设置每日支出上限。达到上限时队列自动暂停。`,dailyLabel:`每日预算(USD)`,dailyHelper:`留空表示禁用。支出按最近 24 小时计算。`,dailyPlaceholder:`如 5.00`,perJobLabel:`单任务成本告警(USD)`,perJobHelper:`当此项目中的单个任务超过此金额时告警。`,perJobPlaceholder:`如 0.50`,invalidNumber:`请输入正数,或留空以禁用`,dailyRemoved:`已移除每日预算`,dailySet:"每日预算已设为 ${{amount}}",perJobAlertDisabled:`已禁用单任务成本告警`,alertSet:"已设置任务超过 ${{amount}} 时告警",saveBudgetFailed:`保存预算失败`,saveThresholdFailed:`保存阈值失败`},s={title:`Desktop 设置`,description:`管理已注册的项目并查看 Desktop 信息。`,registeredProjects:`已注册项目`,noProjects:`尚未注册任何项目`,techUrlDescription:`specrails-tech API 的基础 URL(默认:http://localhost:3000)`,techUrlSaved:`specrails-tech URL 已保存`,techUrlSaveFailed:`保存 URL 失败`,budgetAlertsHeading:`预算与告警`,dailyBudgetLabel:`Desktop 每日预算(USD)`,dailyBudgetHelper:`所有项目的全局每日支出上限。超出后队列自动暂停。`,dailyBudgetPlaceholder:`如 10.00`,perJobHelper:`当单个任务超过此金额时告警。留空表示禁用。`,costAlertsDisabled:`已禁用成本告警`,dailyBudgetRemoved:`已移除 Desktop 每日预算`,dailyBudgetSet:"Desktop 每日预算已设为 ${{amount}}",dailyBudgetSaveFailed:`保存 Desktop 每日预算失败`,projectRemoved:`项目已移除`,projectRemoveFailed:`移除项目失败`,onboardingHeading:`新手引导`,platformTour:`平台导览`,platformTourDescription:`重播欢迎向导,回顾核心功能。`,replayTour:`重播导览`,terminalPanelHeading:`终端面板`,infoHeading:`Desktop 信息`,infoPort:`端口`,infoProjects:`项目`,infoDb:`Desktop 数据库`},c={heading:`系统通知`,description:`任务完成或失败时显示原生桌面通知。仅在标签页未聚焦时显示通知。`,enableLabel:`启用系统通知`,notifyOn:`通知时机:`,filterAll:`全部(完成与失败)`,filterCompleted:`仅完成`,filterFailed:`仅失败`,enabledToast:`已启用系统通知`,disabledToast:`已禁用系统通知`},l={heading:`出站 Webhook`,description:`在 Desktop 事件发生时通知外部工具(Slack、Zapier、CI/CD)。设置密钥后,请求会通过 <code>X-Specrails-Signature</code> 进行签名。`,eventJobCompleted:`任务完成`,eventJobFailed:`任务失败`,eventDailyBudgetExceeded:`超出每日预算`,statusOn:`开`,statusOff:`关`,disable:`禁用`,enable:`启用`,sendTestPing:`发送测试 ping`,addHeading:`添加 Webhook`,secretPlaceholder:`签名密钥(可选)`,addButton:`添加 Webhook`,urlRequired:`URL 为必填项`,selectEvent:`请至少选择一个事件`,added:`Webhook 已添加`,addFailed:`添加 Webhook 失败`,updateFailed:`更新 Webhook 失败`,removed:`Webhook 已移除`,removeFailed:`移除 Webhook 失败`,testPingSent:`测试 ping 已发送`,testPingFailed:`发送测试 ping 失败`},u={title:`终端面板`,desktopDescription:`Desktop 全局默认值,应用于所有项目,除非设置了项目级覆盖。`,projectDescription:`终端面板的项目级覆盖。保持字段不变即继承 Desktop 默认值。`,fontFamily:`字体`,fontSize:`字号({{min}}–{{max}})`,renderMode:`渲染模式`,copyOnSelect:`选中即复制`,shellIntegration:`Shell 集成(OSC 133 标记)`,notifyLongRunning:`长耗时命令通知`,longCommandThreshold:`长命令阈值(ms)`,imageRendering:`内联图片渲染(Sixel + iTerm2)`,browserShortcutUrl:`浏览器快捷方式 URL`,quickScript:`快捷脚本(粘贴到活动终端——需手动按 Enter)`,reset:`重置`,unsavedChanges:`有未保存的更改`,clearOverride:`清除覆盖`,clear:`清除`,inheritingDefault:`正在继承 Desktop 默认值`,nothingToSave:`没有可保存的内容`,saved:`终端设置已保存`},d={heading:`代码板块`,summaryLanguage:`摘要语言`,monthlyBudget:`每月预算(USD)`,budgetHelper:"每个自然月 `surface=file-summary` 支出的上限。用户手动发起的重新生成可超出此限制。"},f={taglines:{dracula:`经典原版 — 紫色调暗色主题,霓虹色点缀鲜明`,"aurora-light":`高级浅色 — 受 Linear 启发的靛蓝配暖白底色`,"obsidian-dark":`高级深色 — 近黑蓝调配电光色点缀`,matrix:`磷光终端 — 绿黑底色上的柔和薄荷绿`,specrails:`品牌主题 — 深海军靛蓝配饱和青色点缀`},heading:`外观`,themeGroupLabel:`主题`,currentlyActive:`当前使用中`,updateFailed:`更新主题失败`},p={heading:`移动伴侣`,description:`通过手机上的 SpecRails Companion 应用控制 Specrails,完全经由本地网络。默认关闭。`,accessOn:`移动访问已开启`,accessOff:`移动访问已关闭`,listeningOnPort:`正在监听端口 {{port}}`,notListening:`未在监听`,turnOn:`开启`,turnOff:`关闭`,pairWebDevice:`配对网页伴侣`,reset:`重置`,resetConfirm:`确定要重置移动身份吗?所有已配对设备都将被吊销,必须重新配对。`,identityReset:`移动身份已重置`,enableFailed:`无法启用移动访问:{{message}}`,disableFailed:`无法禁用移动访问:{{message}}`,windowsFirewall:`首次启用时,Windows 防火墙会询问是否允许 SpecRails 服务器——请选择“允许在专用网络上”。`,pairedDevices:`已配对设备`,noDevices:`尚无已配对设备。`,revokeDevice:`吊销 {{name}}`},m={title:`配对网页伴侣`,description:`在手机上打开 specrails.dev/companion-app,然后用它扫描此二维码。`,startFailed:`无法开始配对:{{error}}`,showThenScan:`手机扫描此码后,点按“扫描手机的二维码”,并将摄像头对准手机。`,scanAnswer:`扫描手机的二维码`,scanning:`请将摄像头对准手机上的二维码…`,cameraFailed:`摄像头不可用:{{message}}`,notAnswer:`该二维码不是配对应答。`,connecting:`正在连接…`,paired:`✓ 已配对`,pairedToast:`网页伴侣已配对`,answerRejected:`桌面端拒绝了该码。请重新生成一个并重试。`,copyCode:`复制配对码`,codeCopied:`配对码已复制`,cancel:`取消`},h={heading:`Specrails Core`,installed:`已安装:`,updateAvailable:`有可用更新:{{version}}`,upToDate:`已是最新`,neverChecked:`尚未检查`,check:`检查更新`,checking:`检查中…`,update:`更新到 {{version}}`,downloading:`下载中…`,materializing:`安装中…`,affectsAll:`适用于所有项目 — 无需重启。`,unavailable:`此版本不支持核心更新。`,toastDone:`Specrails Core 已更新到 {{version}}`,toastError:`核心更新失败:{{message}}`,toastUpToDate:`Specrails Core 已是最新`,toastCheckFailed:`检查更新失败:{{message}}`},g={language:e,page:t,errors:n,telemetry:r,prePrompt:i,ultraPrePrompt:a,budget:o,desktop:s,notifications:c,webhooks:l,terminal:u,codeSection:d,appearance:f,mobile:p,pairWeb:m,coreUpdate:h};export{f as appearance,o as budget,d as codeSection,h as coreUpdate,g as default,s as desktop,n as errors,e as language,p as mobile,c as notifications,t as page,m as pairWeb,i as prePrompt,r as telemetry,u as terminal,a as ultraPrePrompt,l as webhooks};
@@ -1 +1 @@
1
- import{r as e}from"./chunk-CilyBKbf.js";import{sn as t}from"./index-DRhFPNAv.js";var n=e(t(),1),r=new Map;function i(e,t){return`${e}:${t}`}function a({namespace:e,projectId:t,initialValue:a,fetcher:o,pollInterval:s=0}){let c=t?i(t,e):null,[l,u]=(0,n.useState)(()=>c&&r.has(c)?r.get(c):a),[d,f]=(0,n.useState)(()=>c?!r.has(c):!0),[p,m]=(0,n.useState)(d),h=(0,n.useRef)(o);h.current=o;let g=(0,n.useRef)(c);return g.current=c,(0,n.useEffect)(()=>{if(!c)return;let e=r.get(c);e===void 0?(u(a),f(!0),m(!0)):(u(e),f(!1),m(!1));let t=!1;async function n(){try{let e=await h.current();if(t)return;r.set(c,e),u(e)}catch{}finally{t||(m(!1),f(!1))}}n();let i;return s>0&&(i=setInterval(n,s)),()=>{t=!0,i&&clearInterval(i)}},[c,s]),{data:l,isLoading:p,isFirstLoad:d,refresh:(0,n.useCallback)(()=>{if(!c)return;let e=c;h.current().then(t=>{r.set(e,t),g.current===e&&u(t)}).catch(()=>{})},[c])}}export{a as t};
1
+ import{r as e}from"./chunk-CilyBKbf.js";import{sn as t}from"./index-IgMtikGD.js";var n=e(t(),1),r=new Map;function i(e,t){return`${e}:${t}`}function a({namespace:e,projectId:t,initialValue:a,fetcher:o,pollInterval:s=0}){let c=t?i(t,e):null,[l,u]=(0,n.useState)(()=>c&&r.has(c)?r.get(c):a),[d,f]=(0,n.useState)(()=>c?!r.has(c):!0),[p,m]=(0,n.useState)(d),h=(0,n.useRef)(o);h.current=o;let g=(0,n.useRef)(c);return g.current=c,(0,n.useEffect)(()=>{if(!c)return;let e=r.get(c);e===void 0?(u(a),f(!0),m(!0)):(u(e),f(!1),m(!1));let t=!1;async function n(){try{let e=await h.current();if(t)return;r.set(c,e),u(e)}catch{}finally{t||(m(!1),f(!1))}}n();let i;return s>0&&(i=setInterval(n,s)),()=>{t=!0,i&&clearInterval(i)}},[c,s]),{data:l,isLoading:p,isFirstLoad:d,refresh:(0,n.useCallback)(()=>{if(!c)return;let e=c;h.current().then(t=>{r.set(e,t),g.current===e&&u(t)}).catch(()=>{})},[c])}}export{a as t};
@@ -191,7 +191,7 @@
191
191
  .specrails-splash__mark .pill { opacity: 1; }
192
192
  }
193
193
  </style>
194
- <script type="module" crossorigin src="/assets/index-DRhFPNAv.js"></script>
194
+ <script type="module" crossorigin src="/assets/index-IgMtikGD.js"></script>
195
195
  <link rel="modulepreload" crossorigin href="/assets/chunk-CilyBKbf.js">
196
196
  <link rel="modulepreload" crossorigin href="/assets/preload-helper-DSXbuxSR.js">
197
197
  <link rel="modulepreload" crossorigin href="/assets/clsx-CnH-HMk3.js">
@@ -213,12 +213,12 @@
213
213
  <link rel="modulepreload" crossorigin href="/assets/jira-C-ATCti0.js">
214
214
  <link rel="modulepreload" crossorigin href="/assets/jobs-Db3xrsp_.js">
215
215
  <link rel="modulepreload" crossorigin href="/assets/nav-BRInPX8a.js">
216
- <link rel="modulepreload" crossorigin href="/assets/settings-C0-7Fpxg.js">
216
+ <link rel="modulepreload" crossorigin href="/assets/settings-Cx97tHs5.js">
217
217
  <link rel="modulepreload" crossorigin href="/assets/setup-B6egeeTM.js">
218
218
  <link rel="modulepreload" crossorigin href="/assets/specs-D-Sb6dre.js">
219
219
  <link rel="modulepreload" crossorigin href="/assets/terminal-C0xx0SjA.js">
220
220
  <link rel="modulepreload" crossorigin href="/assets/tickets-D5MSAPe_.js">
221
- <link rel="stylesheet" crossorigin href="/assets/index-D6BaYRRU.css">
221
+ <link rel="stylesheet" crossorigin href="/assets/index-Cc3LCzBq.css">
222
222
  </head>
223
223
  <body>
224
224
  <div id="specrails-splash" aria-hidden="false" role="status" aria-label="Loading Specrails">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specrails-desktop",
3
- "version": "2.9.1",
3
+ "version": "2.10.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,219 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.CoreUpdateManager = exports.CORE_PACKAGE = void 0;
7
+ const child_process_1 = require("child_process");
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const os_1 = __importDefault(require("os"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const framework_manager_1 = require("./framework-manager");
12
+ const semver_lite_1 = require("./semver-lite");
13
+ /**
14
+ * CoreUpdateManager — the voluntary, app-global specrails-core update channel.
15
+ *
16
+ * The bundled framework (see FrameworkManager) is pinned to the core version
17
+ * shipped inside the app build. This manager lets a user UPDATE specrails-core
18
+ * independently of the desktop app: it queries npm for the latest published
19
+ * version, and on an explicit request npm-installs that newer core into a temp
20
+ * staging dir, materializes its framework into `~/.specrails/framework/<version>`
21
+ * and atomically swaps `framework/current` to it — every project workspace that
22
+ * symlinks `current/...` jumps to the new core with NO restart and NO per-project
23
+ * `npx`. The previous `framework/<version>` dir is left intact (non-destructive).
24
+ *
25
+ * Detection is deliberately simple (KISS): npm `latest` strictly greater than the
26
+ * installed `framework/current` version ⇒ an update is available. There is no
27
+ * compatibility-range gate; the floor is the app-bundled core (the update only
28
+ * ever moves the pointer UP — FrameworkManager.versionCheck has an anti-downgrade
29
+ * guard so the next startup never reverts a manual update).
30
+ *
31
+ * The whole feature is gated on a bundled core being present (desktop mode); in
32
+ * dev / non-desktop mode `isAvailable()` is false and projects use the legacy npx
33
+ * path, so a core "update" would have no effect and the UI shows "unavailable".
34
+ */
35
+ exports.CORE_PACKAGE = 'specrails-core';
36
+ const REGISTRY_URL = `https://registry.npmjs.org/${exports.CORE_PACKAGE}/latest`;
37
+ const CHECK_TIMEOUT_MS = 10_000;
38
+ const INSTALL_TIMEOUT_MS = 180_000;
39
+ class CoreUpdateManager {
40
+ home;
41
+ broadcast;
42
+ providersFn;
43
+ fetchLatestFn;
44
+ npmInstallFn;
45
+ makeFrameworkFn;
46
+ latestVersion = null;
47
+ lastCheckedAt = null;
48
+ updating = false;
49
+ constructor(opts = {}) {
50
+ this.home = opts.home;
51
+ this.broadcast = opts.broadcast;
52
+ this.providersFn = opts.providers ?? (() => ['claude']);
53
+ this.fetchLatestFn = opts.fetchLatest ?? (() => fetchLatestFromRegistry());
54
+ this.npmInstallFn = opts.npmInstall ?? defaultNpmInstall;
55
+ this.makeFrameworkFn =
56
+ opts.makeFramework ??
57
+ ((coreRoot) => new framework_manager_1.FrameworkManager({ home: this.home, broadcast: this.broadcast, coreRoot }));
58
+ }
59
+ /** A read-only FrameworkManager pointed at the bundled core. */
60
+ bundledFramework() {
61
+ return new framework_manager_1.FrameworkManager({ home: this.home });
62
+ }
63
+ /** True when the bundled-framework system is active (a bundled core is present). */
64
+ isAvailable() {
65
+ return this.bundledFramework().isAvailable();
66
+ }
67
+ /** Current status — no network (uses the cached latest from the last check). */
68
+ getStatus() {
69
+ const fm = this.bundledFramework();
70
+ const bundledVersion = fm.bundledVersion();
71
+ const currentVersion = (0, framework_manager_1.readCurrentFrameworkVersion)(this.home) ?? bundledVersion;
72
+ const updateAvailable = this.latestVersion != null &&
73
+ currentVersion != null &&
74
+ (0, semver_lite_1.isNewer)(this.latestVersion, currentVersion);
75
+ return {
76
+ available: fm.isAvailable(),
77
+ currentVersion,
78
+ bundledVersion,
79
+ latestVersion: this.latestVersion,
80
+ updateAvailable,
81
+ updating: this.updating,
82
+ lastCheckedAt: this.lastCheckedAt,
83
+ };
84
+ }
85
+ /** Hit npm for the latest published version, cache it, return refreshed status. */
86
+ async checkForUpdate() {
87
+ const latest = await this.fetchLatestFn();
88
+ if (!(0, semver_lite_1.isValidVersion)(latest)) {
89
+ throw new Error(`core-update: npm returned an unparseable version "${latest}"`);
90
+ }
91
+ this.latestVersion = latest.trim().replace(/^[v=]+/, '');
92
+ this.lastCheckedAt = Date.now();
93
+ return this.getStatus();
94
+ }
95
+ /**
96
+ * Materialize + swap to `targetVersion` (default: the cached latest). Streams
97
+ * `core_update.progress` WS events and broadcasts `framework.updated` on
98
+ * success. Returns the outcome; never throws (errors become a failed result +
99
+ * an `error` progress event).
100
+ */
101
+ async update(targetVersion) {
102
+ if (this.updating) {
103
+ return { ok: false, error: 'An update is already in progress.' };
104
+ }
105
+ if (!this.isAvailable()) {
106
+ return { ok: false, error: 'Core updates are unavailable in this build.' };
107
+ }
108
+ const requested = (targetVersion ?? this.latestVersion ?? '').trim().replace(/^[v=]+/, '');
109
+ if (!requested || !(0, semver_lite_1.isValidVersion)(requested)) {
110
+ return { ok: false, error: 'No valid target version to update to. Check for updates first.' };
111
+ }
112
+ const current = (0, framework_manager_1.readCurrentFrameworkVersion)(this.home);
113
+ if (current && !(0, semver_lite_1.isNewer)(requested, current)) {
114
+ return { ok: false, error: `Already on ${current}; ${requested} is not newer.` };
115
+ }
116
+ this.updating = true;
117
+ this.emit('downloading', { version: requested });
118
+ const tmp = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'core-update-'));
119
+ try {
120
+ this.npmInstallFn(`${exports.CORE_PACKAGE}@${requested}`, tmp);
121
+ const coreRoot = path_1.default.join(tmp, 'node_modules', exports.CORE_PACKAGE);
122
+ const cli = path_1.default.join(coreRoot, 'dist', 'installer', 'cli.js');
123
+ if (!fs_1.default.existsSync(cli)) {
124
+ throw new Error('downloaded core is missing dist/installer/cli.js');
125
+ }
126
+ const fm = this.makeFrameworkFn(coreRoot);
127
+ // The version actually installed (npm may resolve a dist-tag/range).
128
+ const installed = fm.bundledVersion() ?? requested;
129
+ this.emit('materializing', { version: installed });
130
+ const providers = uniqueProviders(this.providersFn());
131
+ const mat = fm.materialize(installed, providers);
132
+ if (!mat.ran) {
133
+ throw new Error('framework materialize did not run (no usable core)');
134
+ }
135
+ if (mat.errors.length > 0) {
136
+ throw new Error(`framework materialize failed: ${mat.errors.map((e) => `${e.provider}: ${e.message}`).join('; ')}`);
137
+ }
138
+ const swapped = fm.swapCurrent(installed);
139
+ if (!swapped) {
140
+ throw new Error('framework swap-current failed');
141
+ }
142
+ this.latestVersion = installed;
143
+ this.lastCheckedAt = Date.now();
144
+ this.emit('done', { version: installed });
145
+ // Reuse the existing app-level event so any listener that reacts to a
146
+ // framework version bump (e.g. core-version banners) refreshes too.
147
+ this.safeBroadcast({ type: 'framework.updated', version: installed });
148
+ return { ok: true, version: installed };
149
+ }
150
+ catch (err) {
151
+ const message = err instanceof Error ? err.message : String(err);
152
+ this.emit('error', { message });
153
+ return { ok: false, error: message };
154
+ }
155
+ finally {
156
+ this.updating = false;
157
+ try {
158
+ fs_1.default.rmSync(tmp, { recursive: true, force: true });
159
+ }
160
+ catch {
161
+ /* best-effort temp cleanup */
162
+ }
163
+ }
164
+ }
165
+ emit(phase, extra) {
166
+ this.safeBroadcast({ type: 'core_update.progress', phase, ...extra });
167
+ }
168
+ safeBroadcast(msg) {
169
+ try {
170
+ this.broadcast?.(msg);
171
+ }
172
+ catch {
173
+ /* broadcast is best-effort */
174
+ }
175
+ }
176
+ }
177
+ exports.CoreUpdateManager = CoreUpdateManager;
178
+ function uniqueProviders(values) {
179
+ const out = Array.from(new Set(values.filter((v) => typeof v === 'string' && v.length > 0)));
180
+ return out.length > 0 ? out : ['claude'];
181
+ }
182
+ /** GET the latest published version from the npm registry (no npm binary needed). */
183
+ async function fetchLatestFromRegistry() {
184
+ const controller = new AbortController();
185
+ const timer = setTimeout(() => controller.abort(), CHECK_TIMEOUT_MS);
186
+ try {
187
+ const res = await fetch(REGISTRY_URL, {
188
+ signal: controller.signal,
189
+ headers: { accept: 'application/json' },
190
+ });
191
+ if (!res.ok) {
192
+ throw new Error(`npm registry returned ${res.status}`);
193
+ }
194
+ const json = (await res.json());
195
+ if (typeof json.version !== 'string' || json.version.length === 0) {
196
+ throw new Error('npm registry response has no version field');
197
+ }
198
+ return json.version;
199
+ }
200
+ finally {
201
+ clearTimeout(timer);
202
+ }
203
+ }
204
+ /**
205
+ * Install `spec` into `cwd` with a minimal isolated package.json — mirrors
206
+ * `scripts/assemble-bundled-core.mjs`. On Windows npm is `npm.cmd`; Node 20.12+
207
+ * (CVE-2024-27980) refuses to spawn a `.cmd` without a shell, so run through the
208
+ * shell there. POSIX spawns directly.
209
+ */
210
+ function defaultNpmInstall(spec, cwd) {
211
+ fs_1.default.writeFileSync(path_1.default.join(cwd, 'package.json'), JSON.stringify({ name: 'core-update-stage', private: true, version: '0.0.0' }));
212
+ (0, child_process_1.execFileSync)('npm', ['install', spec, '--no-audit', '--no-fund', '--no-save', '--ignore-scripts', '--silent'], {
213
+ cwd,
214
+ encoding: 'utf8',
215
+ stdio: ['ignore', 'inherit', 'inherit'],
216
+ timeout: INSTALL_TIMEOUT_MS,
217
+ shell: process.platform === 'win32',
218
+ });
219
+ }
@@ -11,6 +11,7 @@ const fs_1 = __importDefault(require("fs"));
11
11
  const net_1 = __importDefault(require("net"));
12
12
  const desktop_db_1 = require("./desktop-db");
13
13
  const webhook_manager_1 = require("./webhook-manager");
14
+ const core_update_manager_1 = require("./core-update-manager");
14
15
  const specrails_tech_client_1 = require("./specrails-tech-client");
15
16
  const core_compat_1 = require("./core-compat");
16
17
  const providers_1 = require("./providers");
@@ -775,5 +776,47 @@ function createDesktopRouter(registry, broadcast) {
775
776
  const monthlyBudgetUsd = Number.isFinite(parsed) && parsed >= 0 ? parsed : 5.0;
776
777
  res.json({ language, monthlyBudgetUsd });
777
778
  });
779
+ // ─── specrails-core update channel (app-global) ─────────────────────────────
780
+ // Detect + apply a specrails-core framework update independently of the desktop
781
+ // app update. See server/core-update-manager.ts. A single manager instance
782
+ // persists for the router lifetime (holds the cached latest + in-progress flag).
783
+ const coreUpdate = new core_update_manager_1.CoreUpdateManager({
784
+ // `core_update.progress` / `framework.updated` are app-level (no projectId)
785
+ // and not members of the WsMessage union; cast at this single boundary.
786
+ broadcast: (msg) => broadcast(msg),
787
+ providers: () => registry.installedProvidersUnion(),
788
+ });
789
+ // GET /api/core-update/status — current/bundled/latest versions, no network.
790
+ router.get('/core-update/status', (_req, res) => {
791
+ res.json(coreUpdate.getStatus());
792
+ });
793
+ // POST /api/core-update/check — hit npm for the latest version, refresh status.
794
+ router.post('/core-update/check', (_req, res) => {
795
+ void coreUpdate
796
+ .checkForUpdate()
797
+ .then((status) => res.json(status))
798
+ .catch((err) => {
799
+ const message = err instanceof Error ? err.message : 'check failed';
800
+ res.status(502).json({ error: 'check_failed', message });
801
+ });
802
+ });
803
+ // POST /api/core-update/update — materialize + swap to the target (default latest).
804
+ // 202 + async progress over the `core_update.progress` WS event.
805
+ router.post('/core-update/update', (req, res) => {
806
+ if (!coreUpdate.isAvailable()) {
807
+ res.status(409).json({ error: 'unavailable', message: 'Core updates are unavailable in this build.' });
808
+ return;
809
+ }
810
+ const status = coreUpdate.getStatus();
811
+ if (status.updating) {
812
+ res.status(409).json({ error: 'in_progress', message: 'An update is already in progress.' });
813
+ return;
814
+ }
815
+ const version = req.body?.version;
816
+ const target = typeof version === 'string' ? version : undefined;
817
+ res.status(202).json({ accepted: true });
818
+ // Fire-and-forget; outcome is delivered over WS (`core_update.progress`).
819
+ void coreUpdate.update(target);
820
+ });
778
821
  return router;
779
822
  }
@@ -12,6 +12,7 @@ const path_1 = __importDefault(require("path"));
12
12
  const bundled_core_1 = require("./bundled-core");
13
13
  const artifact_registry_1 = require("./artifact-registry");
14
14
  Object.defineProperty(exports, "atomicWrite", { enumerable: true, get: function () { return artifact_registry_1.atomicWrite; } });
15
+ const semver_lite_1 = require("./semver-lite");
15
16
  /** `~/.specrails/framework` — same home as the registry. */
16
17
  function frameworkRoot(home) {
17
18
  return path_1.default.join((0, artifact_registry_1.resolveHome)(home), '.specrails', 'framework');
@@ -50,16 +51,43 @@ function readCurrentFrameworkVersion(home) {
50
51
  class FrameworkManager {
51
52
  home;
52
53
  broadcast;
54
+ coreRoot;
53
55
  constructor(opts = {}) {
54
56
  this.home = opts.home;
55
57
  this.broadcast = opts.broadcast;
58
+ this.coreRoot = opts.coreRoot;
56
59
  }
57
- /** True when a usable bundled core is present (else all methods no-op). */
60
+ /**
61
+ * Resolve the core CLI to shell out to: the override core root (update channel)
62
+ * when set, else the bundled core. Returns null when neither is present.
63
+ */
64
+ resolveCli() {
65
+ if (this.coreRoot) {
66
+ const cli = path_1.default.join(this.coreRoot, 'dist', 'installer', 'cli.js');
67
+ return fs_1.default.existsSync(cli) ? cli : null;
68
+ }
69
+ return (0, bundled_core_1.getBundledCoreCli)();
70
+ }
71
+ /** True when a usable core (override or bundled) is present (else methods no-op). */
58
72
  isAvailable() {
59
- return (0, bundled_core_1.getBundledCoreCli)() !== null;
73
+ return this.resolveCli() !== null;
60
74
  }
61
- /** The bundled core version (the version the app should materialize), or null. */
75
+ /**
76
+ * The version the app should materialize: the override core's version (update
77
+ * channel) when a `coreRoot` is set, else the bundled core version. Null when
78
+ * neither is present / unreadable.
79
+ */
62
80
  bundledVersion() {
81
+ if (this.coreRoot) {
82
+ try {
83
+ const pkg = JSON.parse(fs_1.default.readFileSync(path_1.default.join(this.coreRoot, 'package.json'), 'utf8'));
84
+ const v = pkg.version?.trim();
85
+ return v && v.length > 0 ? v : null;
86
+ }
87
+ catch {
88
+ return null;
89
+ }
90
+ }
63
91
  return (0, bundled_core_1.getBundledCoreVersion)();
64
92
  }
65
93
  /**
@@ -73,7 +101,7 @@ class FrameworkManager {
73
101
  * (ran:false) when no bundled core is present.
74
102
  */
75
103
  materialize(version, providers = ['claude']) {
76
- const cli = (0, bundled_core_1.getBundledCoreCli)();
104
+ const cli = this.resolveCli();
77
105
  if (!cli) {
78
106
  return { ran: false, version: null, providers: [], errors: [] };
79
107
  }
@@ -119,7 +147,7 @@ class FrameworkManager {
119
147
  * `swap-current` only moves the version pointer (provider-invariant).
120
148
  */
121
149
  swapCurrent(version, _provider = 'claude') {
122
- const cli = (0, bundled_core_1.getBundledCoreCli)();
150
+ const cli = this.resolveCli();
123
151
  if (!cli)
124
152
  return false;
125
153
  if (readCurrentFrameworkVersion(this.home) === version)
@@ -138,13 +166,22 @@ class FrameworkManager {
138
166
  */
139
167
  versionCheck(providers = ['claude']) {
140
168
  const bundled = this.bundledVersion();
141
- if (!(0, bundled_core_1.getBundledCoreCli)() || !bundled) {
169
+ if (!this.resolveCli() || !bundled) {
142
170
  return { swapped: false, version: readCurrentFrameworkVersion(this.home) };
143
171
  }
144
172
  const current = readCurrentFrameworkVersion(this.home);
145
173
  if (current === bundled) {
146
174
  return { swapped: false, version: current };
147
175
  }
176
+ // Anti-downgrade guard: NEVER swap `current` down to the bundled version when
177
+ // `current` is already NEWER. The core update channel lets a user voluntarily
178
+ // materialize a core newer than the one shipped in this app build; without
179
+ // this guard the next startup versionCheck would revert that manual update
180
+ // back to the (older) bundled version. Only first-run (current null) or a
181
+ // genuinely newer bundled core (a fresh app update) proceeds.
182
+ if (current && !(0, semver_lite_1.isNewer)(bundled, current)) {
183
+ return { swapped: false, version: current };
184
+ }
148
185
  // Materialize EVERY requested provider first (each with `--no-swap`), then
149
186
  // swap `current` ONCE — and ONLY when every provider materialized cleanly.
150
187
  // A partial multi-provider install must NEVER flip `current`: doing so would
@@ -201,7 +238,7 @@ class FrameworkManager {
201
238
  * when no bundled core is present — caller falls back to legacy npx assembly.
202
239
  */
203
240
  assembleWorkspace(input) {
204
- const cli = (0, bundled_core_1.getBundledCoreCli)();
241
+ const cli = this.resolveCli();
205
242
  if (!cli)
206
243
  return { ran: false };
207
244
  const ver = input.version ?? this.bundledVersion();
@@ -234,9 +271,10 @@ class FrameworkManager {
234
271
  if (this.home) {
235
272
  env.SPECRAILS_REGISTRY_HOME = this.home;
236
273
  }
237
- // Point the bundled core CLI's scriptDir at the bundled package so its
238
- // template/command sources resolve from the bundle, not a global install.
239
- const coreRoot = process.env.SPECRAILS_BUNDLED_CORE_PATH;
274
+ // Point the core CLI's scriptDir at the package whose framework we are
275
+ // materializing so its template/command sources resolve from there: the
276
+ // OVERRIDE core (update channel) when set, else the bundled package.
277
+ const coreRoot = this.coreRoot ?? process.env.SPECRAILS_BUNDLED_CORE_PATH;
240
278
  if (coreRoot)
241
279
  env.SPECRAILS_CORE_SCRIPT_DIR = coreRoot;
242
280
  return env;
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.resolveBundledRuntimePath = resolveBundledRuntimePath;
7
+ exports.resolveBundledNodeExe = resolveBundledNodeExe;
7
8
  exports.resolveStartupPath = resolveStartupPath;
8
9
  exports.parseLoginShellOutput = parseLoginShellOutput;
9
10
  exports.augmentPathFromLoginShell = augmentPathFromLoginShell;
@@ -94,6 +95,26 @@ function resolveBundledRuntimePath() {
94
95
  }
95
96
  return p;
96
97
  }
98
+ /**
99
+ * Absolute path to the bundled REAL Node executable (`runtimes/node/bin/node` on
100
+ * POSIX, `runtimes/node/node.exe` on Windows), or `null` when no bundled runtimes
101
+ * are present (non-desktop mode, a runtimes-less build, or a partial extraction).
102
+ *
103
+ * This is the node that must run bundled node CLIs (e.g. the openspec ESM CLI).
104
+ * It is deliberately NOT `process.execPath`: in the packaged app `process.execPath`
105
+ * is the `specrails-server` pkg binary, which cannot run an external ESM CLI —
106
+ * passing it as `SPECRAILS_OPENSPEC_NODE` made `openspec init` exit with code -1.
107
+ * Existence-gated so a stale/partial bundle degrades to the PATH `node` instead.
108
+ */
109
+ function resolveBundledNodeExe() {
110
+ const runtimesPath = process.env.SPECRAILS_BUNDLED_RUNTIMES_PATH;
111
+ if (!runtimesPath || runtimesPath.length === 0)
112
+ return null;
113
+ const exe = process.platform === 'win32'
114
+ ? path_1.default.join(runtimesPath, 'node', 'node.exe')
115
+ : path_1.default.join(runtimesPath, 'node', 'bin', 'node');
116
+ return fileExists(exe) ? exe : null;
117
+ }
97
118
  /**
98
119
  * Synchronously prepend well-known package-manager bin directories to
99
120
  * `process.env.PATH` if they are missing. No-op on Windows.
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ /**
3
+ * Minimal semver comparison — the repo has no `semver` dependency and the core
4
+ * update channel only needs "is A strictly newer than B" plus a total order.
5
+ *
6
+ * Supports `MAJOR.MINOR.PATCH` with an optional `-prerelease` suffix. A version
7
+ * WITH a prerelease tag sorts BELOW the same version without one (`4.9.0-rc.1` <
8
+ * `4.9.0`), matching semver §11. Prerelease identifiers are compared
9
+ * left-to-right: numeric identifiers numerically, others lexically, numeric <
10
+ * non-numeric, and a shorter prerelease set sorts lower when all prior
11
+ * identifiers are equal. Build metadata (`+…`) is ignored. Leading `v`/`=` and
12
+ * surrounding whitespace are tolerated. Unparseable input sorts as `0.0.0`.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.compareVersions = compareVersions;
16
+ exports.isNewer = isNewer;
17
+ exports.isValidVersion = isValidVersion;
18
+ function parse(raw) {
19
+ const cleaned = String(raw ?? '')
20
+ .trim()
21
+ .replace(/^[v=]+/, '')
22
+ .split('+', 1)[0]; // drop build metadata
23
+ const [core, pre] = cleaned.split('-', 2);
24
+ const parts = core.split('.');
25
+ const num = (s) => {
26
+ const n = Number.parseInt(s ?? '', 10);
27
+ return Number.isFinite(n) && n >= 0 ? n : 0;
28
+ };
29
+ return {
30
+ major: num(parts[0]),
31
+ minor: num(parts[1]),
32
+ patch: num(parts[2]),
33
+ prerelease: pre && pre.length > 0 ? pre.split('.') : [],
34
+ };
35
+ }
36
+ function comparePrerelease(a, b) {
37
+ // No prerelease outranks any prerelease (1.0.0 > 1.0.0-rc).
38
+ if (a.length === 0 && b.length === 0)
39
+ return 0;
40
+ if (a.length === 0)
41
+ return 1;
42
+ if (b.length === 0)
43
+ return -1;
44
+ const len = Math.max(a.length, b.length);
45
+ for (let i = 0; i < len; i++) {
46
+ const ai = a[i];
47
+ const bi = b[i];
48
+ if (ai === undefined)
49
+ return -1; // shorter set sorts lower
50
+ if (bi === undefined)
51
+ return 1;
52
+ if (ai === bi)
53
+ continue;
54
+ const an = /^\d+$/.test(ai);
55
+ const bn = /^\d+$/.test(bi);
56
+ if (an && bn) {
57
+ const d = Number.parseInt(ai, 10) - Number.parseInt(bi, 10);
58
+ if (d !== 0)
59
+ return d < 0 ? -1 : 1;
60
+ }
61
+ else if (an) {
62
+ return -1; // numeric identifiers have lower precedence than non-numeric
63
+ }
64
+ else if (bn) {
65
+ return 1;
66
+ }
67
+ else {
68
+ return ai < bi ? -1 : 1;
69
+ }
70
+ }
71
+ return 0;
72
+ }
73
+ /** -1 if a < b, 0 if equal, 1 if a > b. */
74
+ function compareVersions(a, b) {
75
+ const pa = parse(a);
76
+ const pb = parse(b);
77
+ if (pa.major !== pb.major)
78
+ return pa.major < pb.major ? -1 : 1;
79
+ if (pa.minor !== pb.minor)
80
+ return pa.minor < pb.minor ? -1 : 1;
81
+ if (pa.patch !== pb.patch)
82
+ return pa.patch < pb.patch ? -1 : 1;
83
+ return comparePrerelease(pa.prerelease, pb.prerelease);
84
+ }
85
+ /** True when `candidate` is strictly newer than `base`. */
86
+ function isNewer(candidate, base) {
87
+ return compareVersions(candidate, base) > 0;
88
+ }
89
+ /** True when `raw` looks like a parseable `MAJOR.MINOR.PATCH[-pre]` version. */
90
+ function isValidVersion(raw) {
91
+ return /^[v=]?\d+\.\d+\.\d+(?:[-+].*)?$/.test(String(raw ?? '').trim());
92
+ }
@@ -24,6 +24,7 @@ const artifact_registry_1 = require("./artifact-registry");
24
24
  const install_config_path_1 = require("./install-config-path");
25
25
  const bundled_core_1 = require("./bundled-core");
26
26
  const bundled_openspec_1 = require("./bundled-openspec");
27
+ const path_resolver_1 = require("./path-resolver");
27
28
  const framework_manager_1 = require("./framework-manager");
28
29
  /**
29
30
  * specrails-core's installer (Node-native from v4.2.0 onward, bash
@@ -109,7 +110,13 @@ function spawnBundledCoreInit(args, cwd) {
109
110
  const openspecCli = (0, bundled_openspec_1.getBundledOpenspecCli)();
110
111
  if (openspecCli) {
111
112
  env.SPECRAILS_OPENSPEC_BIN = openspecCli;
112
- env.SPECRAILS_OPENSPEC_NODE = process.execPath;
113
+ // Core runs openspec as `<node> <cli> init …`. SPECRAILS_OPENSPEC_NODE MUST be
114
+ // a REAL node executable — NOT process.execPath (the packaged `specrails-server`
115
+ // pkg binary), which cannot run openspec's ESM CLI and made `openspec init`
116
+ // exit with code -1 (→ core throws InstallerError code 50, setup fails). Prefer
117
+ // the bundled Node; fall back to `node` on PATH (resolveStartupPath prepends the
118
+ // bundled node bin in desktop mode, so PATH `node` is the bundled node too).
119
+ env.SPECRAILS_OPENSPEC_NODE = (0, path_resolver_1.resolveBundledNodeExe)() ?? 'node';
113
120
  }
114
121
  console.log(`[SetupManager] spawning BUNDLED core: ${process.execPath} ${fullArgs.join(' ')} (cwd=${cwd})` +
115
122
  (openspecCli ? ' [bundled openspec: offline]' : ' [openspec: npx fallback]'));