@visulima/vis 1.0.0-alpha.10 → 1.0.0-alpha.11

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 (120) hide show
  1. package/CHANGELOG.md +95 -42
  2. package/LICENSE.md +213 -0
  3. package/README.md +8 -4
  4. package/dist/bin.js +9 -1
  5. package/dist/config/index.d.ts +1818 -0
  6. package/dist/config/index.js +2 -0
  7. package/dist/generate/index.d.ts +1 -1
  8. package/dist/generate/index.js +3 -1
  9. package/dist/packem_chunks/applyDefaults.js +336 -0
  10. package/dist/packem_chunks/bin.js +9554 -64
  11. package/dist/packem_chunks/doctor-probe.js +112 -0
  12. package/dist/packem_chunks/fix.js +229 -48
  13. package/dist/packem_chunks/handler.js +99 -1
  14. package/dist/packem_chunks/handler10.js +53 -1
  15. package/dist/packem_chunks/handler11.js +32 -1
  16. package/dist/packem_chunks/handler12.js +100 -2
  17. package/dist/packem_chunks/handler13.js +25 -1
  18. package/dist/packem_chunks/handler14.js +916 -5
  19. package/dist/packem_chunks/handler15.js +206 -1
  20. package/dist/packem_chunks/handler16.js +122 -18
  21. package/dist/packem_chunks/handler17.js +13 -1
  22. package/dist/packem_chunks/handler18.js +106 -1
  23. package/dist/packem_chunks/handler19.js +19 -1
  24. package/dist/packem_chunks/handler2.js +75 -1
  25. package/dist/packem_chunks/handler20.js +29 -1
  26. package/dist/packem_chunks/handler21.js +222 -1
  27. package/dist/packem_chunks/handler22.js +237 -5
  28. package/dist/packem_chunks/handler23.js +101 -1
  29. package/dist/packem_chunks/handler24.js +110 -1
  30. package/dist/packem_chunks/handler25.js +402 -5
  31. package/dist/packem_chunks/handler26.js +13 -1
  32. package/dist/packem_chunks/handler27.js +63 -3
  33. package/dist/packem_chunks/handler28.js +34 -1
  34. package/dist/packem_chunks/handler29.js +458 -7
  35. package/dist/packem_chunks/handler3.js +95 -2
  36. package/dist/packem_chunks/handler30.js +168 -21
  37. package/dist/packem_chunks/handler31.js +530 -3
  38. package/dist/packem_chunks/handler32.js +214 -2
  39. package/dist/packem_chunks/handler33.js +119 -24
  40. package/dist/packem_chunks/handler34.js +630 -2
  41. package/dist/packem_chunks/handler35.js +283 -19
  42. package/dist/packem_chunks/handler36.js +521 -407
  43. package/dist/packem_chunks/handler37.js +762 -22
  44. package/dist/packem_chunks/handler38.js +989 -22
  45. package/dist/packem_chunks/handler39.js +574 -22
  46. package/dist/packem_chunks/handler4.js +90 -4
  47. package/dist/packem_chunks/handler40.js +1685 -3
  48. package/dist/packem_chunks/handler41.js +1088 -10
  49. package/dist/packem_chunks/handler42.js +785 -141
  50. package/dist/packem_chunks/handler43.js +2658 -42
  51. package/dist/packem_chunks/handler44.js +3886 -3
  52. package/dist/packem_chunks/handler45.js +2568 -21
  53. package/dist/packem_chunks/handler46.js +3769 -0
  54. package/dist/packem_chunks/handler47.js +1491 -0
  55. package/dist/packem_chunks/handler5.js +174 -2
  56. package/dist/packem_chunks/handler6.js +95 -13
  57. package/dist/packem_chunks/handler7.js +115 -8
  58. package/dist/packem_chunks/handler8.js +12 -1
  59. package/dist/packem_chunks/handler9.js +29 -1
  60. package/dist/packem_chunks/heal-accept.js +522 -0
  61. package/dist/packem_chunks/heal.js +673 -0
  62. package/dist/packem_chunks/index.js +873 -7
  63. package/dist/packem_chunks/loader.js +23 -1
  64. package/dist/packem_shared/VisUpdateApp-D-Yz_wvg.js +1316 -0
  65. package/dist/packem_shared/_commonjsHelpers-BqLXS_qQ.js +5 -0
  66. package/dist/packem_shared/ai-analysis-CHeB1joD.js +367 -0
  67. package/dist/packem_shared/ai-cache-Be_jexe4.js +142 -0
  68. package/dist/packem_shared/ai-fix-B9iQVcD2.js +379 -0
  69. package/dist/packem_shared/cache-directory-2qvs4goY.js +98 -0
  70. package/dist/packem_shared/catalog-BJTtyi-O.js +1371 -0
  71. package/dist/packem_shared/dependency-scan-A0KSklpG.js +188 -0
  72. package/dist/packem_shared/docker-2iZzc280.js +181 -0
  73. package/dist/packem_shared/failure-log-Cz3Z4SKL.js +100 -0
  74. package/dist/packem_shared/flakiness-goTxXuCX.js +180 -0
  75. package/dist/packem_shared/otel-DCvqCTz_.js +158 -0
  76. package/dist/packem_shared/otelPlugin-DFaLDvJf.js +3 -0
  77. package/dist/packem_shared/registry-CbqXI0rc.js +272 -0
  78. package/dist/packem_shared/run-summary-utils-PVMl4aIh.js +130 -0
  79. package/dist/packem_shared/runtime-check-Cobi3p6l.js +127 -0
  80. package/dist/packem_shared/selectors-SM69TfqC.js +194 -0
  81. package/dist/packem_shared/symbols-Ta7g2nU-.js +14 -0
  82. package/dist/packem_shared/toolchain-BdZd9eBi.js +975 -0
  83. package/dist/packem_shared/typosquats-C-bCh3PX.js +1210 -0
  84. package/dist/packem_shared/use-measured-height-CNP0vT4M.js +20 -0
  85. package/dist/packem_shared/utils-CthVdBPS.js +40 -0
  86. package/dist/packem_shared/xxh3-Ck8mXNg1.js +239 -0
  87. package/index.js +727 -555
  88. package/package.json +35 -17
  89. package/schemas/project.schema.json +8 -10
  90. package/schemas/vis-config.schema.json +132 -8
  91. package/skills/vis/SKILL.md +96 -0
  92. package/templates/buildkite-ci/.buildkite/pipeline.yml.tera +85 -0
  93. package/templates/buildkite-ci/template.yml +20 -0
  94. package/dist/errors/index.d.ts +0 -26
  95. package/dist/errors/index.js +0 -1
  96. package/dist/packem_chunks/config.js +0 -2
  97. package/dist/packem_shared/VisConfigCycleError-CAYNC7d-.js +0 -1
  98. package/dist/packem_shared/VisConfigError-B5LP1zRf.js +0 -1
  99. package/dist/packem_shared/VisConfigLoadError-CeqBSd2Z.js +0 -2
  100. package/dist/packem_shared/VisConfigNotFoundError-DZ9KC527.js +0 -5
  101. package/dist/packem_shared/VisUpdateApp-D-L4_-Iu.js +0 -1
  102. package/dist/packem_shared/_commonjsHelpers-D6W6KoPK.js +0 -1
  103. package/dist/packem_shared/ai-analysis-CGuy7dfE.js +0 -67
  104. package/dist/packem_shared/ai-cache-Bynt6Y9x.js +0 -1
  105. package/dist/packem_shared/cache-directory-D72ZEag2.js +0 -1
  106. package/dist/packem_shared/catalog-BVPerCwG.js +0 -12
  107. package/dist/packem_shared/dependency-scan-Du0tBu64.js +0 -2
  108. package/dist/packem_shared/docker-BcfqH4Av.js +0 -2
  109. package/dist/packem_shared/failure-log-DqYen0LC.js +0 -2
  110. package/dist/packem_shared/flakiness-DSIHZGBT.js +0 -1
  111. package/dist/packem_shared/run-summary-utils-C24Aaf9E.js +0 -1
  112. package/dist/packem_shared/runtime-check-CGHal8SO.js +0 -1
  113. package/dist/packem_shared/selectors-CfH9ZY08.js +0 -3
  114. package/dist/packem_shared/symbols-CQmER5MT.js +0 -1
  115. package/dist/packem_shared/target-merge-DNa-6eWu.js +0 -1
  116. package/dist/packem_shared/toolchain-DQfTQY8E.js +0 -5
  117. package/dist/packem_shared/typosquats-DOR8izpX.js +0 -1
  118. package/dist/packem_shared/use-measured-height-DjYgUOKk.js +0 -1
  119. package/dist/packem_shared/utils-DrNg0XTR.js +0 -1
  120. package/dist/packem_shared/xxh3-DrAUNq4n.js +0 -1
@@ -1,27 +1,2574 @@
1
- var Ee=Object.defineProperty;var f=(e,t)=>Ee(e,"name",{value:t,configurable:!0});import{createRequire as Ae}from"node:module";import{isAccessibleSync as re,isFsCaseSensitive as je}from"@visulima/fs";import{isAbsolute as Fe,join as X,relative as D,basename as ve}from"@visulima/path";import{execa as ye}from"execa";import ke from"zeptomatch";import{jsx as k,jsxs as x}from"react/jsx-runtime";import{Spinner as Ne,Text as S,Box as A,render as Ge}from"@visulima/tui";import{C as be,D as B,T as Te}from"../packem_shared/symbols-CQmER5MT.js";import{dim as C,green as _e,yellow as se,cyan as K,red as Y}from"@visulima/colorize";const Me=Ae(import.meta.url),F=typeof globalThis<"u"&&typeof globalThis.process<"u"?globalThis.process:process,we=f(e=>{if(typeof F<"u"&&F.versions&&F.versions.node){const[t,r]=F.versions.node.split(".").map(Number);if(t>22||t===22&&r>=3||t===20&&r>=16)return F.getBuiltinModule(e)}return Me(e)},"__cjs_getBuiltinModule"),{readFileSync:Oe,writeFileSync:Pe,unlinkSync:Re}=we("node:fs"),{availableParallelism:ne}=we("node:os");var De=Object.defineProperty,He=f((e,t)=>De(e,"name",{value:t,configurable:!0}),"s$4");class O extends Error{static{f(this,"StagedError")}static{He(this,"StagedError")}constructor(t,r){super(t,r),this.name=this.constructor.name}}var Le=Object.defineProperty,Be=f((e,t)=>Le(e,"name",{value:t,configurable:!0}),"o$6");class Ue extends O{static{f(this,"ApplyEmptyCommitError")}static{Be(this,"ApplyEmptyCommitError")}}var qe=Object.defineProperty,Ve=f((e,t)=>qe(e,"name",{value:t,configurable:!0}),"o$5");class b extends O{static{f(this,"ConfigError")}static{Ve(this,"ConfigError")}}var ze=Object.defineProperty,We=f((e,t)=>ze(e,"name",{value:t,configurable:!0}),"t$1");class ae extends O{static{f(this,"GetBackupStashError")}static{We(this,"GetBackupStashError")}}var Ke=Object.defineProperty,Ye=f((e,t)=>Ke(e,"name",{value:t,configurable:!0}),"s$3");class _ extends O{static{f(this,"GitError")}static{Ye(this,"GitError")}stderr;constructor(t,r,n){super(t,n),this.stderr=r}}var Je=Object.defineProperty,Qe=f((e,t)=>Je(e,"name",{value:t,configurable:!0}),"e");class ie extends O{static{f(this,"RestoreOriginalStateError")}static{Qe(this,"RestoreOriginalStateError")}}var Xe=Object.defineProperty,Ze=f((e,t)=>Xe(e,"name",{value:t,configurable:!0}),"o$2");class M extends O{static{f(this,"TaskError")}static{Ze(this,"TaskError")}commandTitle;constructor(t,r,n){super(r,n),this.commandTitle=t}}var et=Object.defineProperty,q=f((e,t)=>et(e,"name",{value:t,configurable:!0}),"i");const tt=q(async e=>{if(e.config!==void 0)return e.config;throw new b(`No staged config provided. Add \`staged\` to your vis.config.ts:
1
+ import { createRequire as __cjs_createRequire } from "node:module";
2
2
 
3
- import { defineConfig } from "@visulima/vis/config";
3
+ const __cjs_require = __cjs_createRequire(import.meta.url);
4
4
 
5
- export default defineConfig({
6
- staged: { "*.ts": "eslint --fix" },
7
- });
5
+ const __cjs_getProcess = typeof globalThis !== "undefined" && typeof globalThis.process !== "undefined" ? globalThis.process : process;
8
6
 
9
- Coming from lint-staged or nano-staged? Run \`vis migrate lint-staged\` (or \`vis migrate nano-staged\`) to move the config in and remove the legacy files.`)},"resolveConfig"),oe=q(e=>{if(typeof e!="object"||e===null)throw new b("Staged config must be an object mapping glob patterns to tasks.");const t=Object.entries(e);if(t.length===0)throw new b("Staged config is empty — at least one glob pattern is required.");for(const[r,n]of t){if(!r||r.trim()==="")throw new b("Staged config keys must be non-empty glob patterns.");Se(r,n)}return e},"validateConfig"),Se=q((e,t)=>{if(typeof t=="string"){if(t.trim()==="")throw new b(`Task for "${e}" is an empty string.`);return}if(Array.isArray(t)){if(t.length===0)throw new b(`Task array for "${e}" is empty.`);for(const r of t)Se(e,r);return}if(typeof t!="function"&&!rt(t))throw new b(`Invalid task for "${e}" — expected string, string[], function, or { title, task } object.`)},"validateTask"),rt=q(e=>typeof e=="object"&&e!==null&&typeof e.title=="string"&&typeof e.task=="function","isCustomTask");var st=Object.defineProperty,$=f((e,t)=>st(e,"name",{value:t,configurable:!0}),"n$1");const ce=2048,g=$(async(e,t)=>{const r=await ye("git",[...e],{cwd:t.cwd,env:t.env?{...process.env,...t.env}:void 0,input:t.input,reject:!1,stderr:"pipe",stdin:t.input===void 0?"ignore":"pipe",stdout:"pipe"}),n=typeof r.exitCode=="number"?r.exitCode:1;if(n!==0&&!t.lenient){const s=typeof r.stderr=="string"?r.stderr:"",i=s.length>ce?`${s.slice(0,ce)}…`:s;throw new _(`git ${e.join(" ")} failed with exit code ${n}: ${i.trim()}`,s)}return{exitCode:n,stderr:typeof r.stderr=="string"?r.stderr:"",stdout:typeof r.stdout=="string"?r.stdout:""}},"git"),R=$(async(e,t)=>{const{stdout:r}=await g(e,t);return r.trim()},"gitOut"),nt=$(async e=>{const t=await g(["rev-parse","--is-inside-work-tree"],{cwd:e,lenient:!0});return t.exitCode===0&&t.stdout.trim()==="true"},"isGitRepo"),at=$(async e=>R(["rev-parse","--absolute-git-dir"],{cwd:e}),"getGitDirectory"),$e=$(async e=>R(["rev-parse","--show-toplevel"],{cwd:e}),"getWorkTree"),de=$(async e=>R(["write-tree"],{cwd:e}),"writeIndexTree"),it=$(async e=>{const t=await g(["rev-parse","HEAD^{tree}"],{cwd:e,lenient:!0});return t.exitCode===0?t.stdout.trim():""},"headTreeSha"),N={major:2,minor:32},ot=$(e=>{const t=/git version (\d+)\.(\d+)/.exec(e);if(!t)return null;const r=Number.parseInt(t[1]??"",10),n=Number.parseInt(t[2]??"",10);return Number.isNaN(r)||Number.isNaN(n)?null:{major:r,minor:n}},"parseGitVersion"),ct=$(async e=>{const t=ot(await R(["--version"],{cwd:e}));if(t!==null&&(t.major<N.major||t.major===N.major&&t.minor<N.minor))throw new _(`Git ${N.major}.${N.minor} or newer is required; found ${t.major}.${t.minor}.`)},"assertGitVersion");var dt=Object.defineProperty,I=f((e,t)=>dt(e,"name",{value:t,configurable:!0}),"a$2");const lt="ACMR",le=500,ut=I(async e=>{const{stdout:t}=await g(["diff-files","--raw","-z"],{cwd:e}),r=t.split("\0").filter(s=>s.length>0),n=[];for(let s=0;s<r.length;s+=1){const i=r[s];if(!i?.startsWith(":"))continue;const o=i.slice(1).split(" "),a=o[3],c=o[4],l=r[s+1];s+=1,c==="A"&&a!==void 0&&/^0+$/.test(a)&&l!==void 0&&n.push(l)}return n},"getIntentToAddPaths"),ue=I(async e=>{const{stdout:t}=await g(["ls-files","--others","--exclude-standard","-z"],{cwd:e});return t.split("\0").filter(r=>r.length>0)},"getUntrackedFiles"),ft=I(async(e,t)=>{if(e.length===0)return;const r=`${e.join("\0")}\0`;await g(["rm","--cached","--quiet","--pathspec-from-file=-","--pathspec-file-nul","--"],{cwd:t.cwd,input:r})},"removeFromIndex"),ht=I(async e=>{const t=e.diffFilter??lt,r=e.diff===void 0?["diff","--name-only","-z",`--diff-filter=${t}`,"--staged"]:["diff","--name-only","-z",`--diff-filter=${t}`,...e.diff.split(/\s+/).filter(Boolean)],{stdout:n}=await g(r,{cwd:e.cwd}),s=n.split("\0").filter(o=>o.length>0);if(s.length===0)return[];const i=e.workTree??await $e(e.cwd);return s.map(o=>Fe(o)?o:X(i,o))},"getFiles"),pt=I(async(e,t)=>{if(e.length===0)return null;const r=[];for(let i=0;i<e.length;i+=le){const o=e.slice(i,i+le),{stdout:a}=await g(["diff","--binary","--unified=0","--no-color","--no-ext-diff","--src-prefix=a/","--dst-prefix=b/","--patch","--submodule=short","--",...o],{cwd:t.cwd});a.length>0&&r.push(a)}if(r.length===0)return null;const n=r.join(""),s=n.endsWith(`
10
- `)?n:`${n}
11
- `;return Buffer.from(s,"utf8")},"capturePatch"),gt=I(async e=>{const{stdout:t}=await g(["status","--porcelain=v1","-z"],{cwd:e}),r=t.split("\0"),n=[];for(let s=0;s<r.length;s+=1){const i=r[s];if(i===void 0||i.length<4)continue;const o=i.charAt(0),a=i.charAt(1),c=i.slice(3),l=o==="R"||o==="C"||a==="R"||a==="C";o!==" "&&o!=="?"&&a!==" "&&a!=="?"&&n.push(c),l&&(s+=1)}return n},"getPartiallyStagedFiles"),mt=I(async(e,t)=>{if(e.length===0)return;const r=`${e.join("\0")}\0`;await g(["checkout","--force","--pathspec-from-file=-","--pathspec-file-nul","--"],{cwd:t.cwd,input:r})},"checkoutPaths"),wt=I(async(e,t)=>{if(await g(["update-index","--again"],{cwd:t.cwd,lenient:!0}),e.length===0)return;const r=`${e.join("\0")}\0`;await g(["add","-u","--pathspec-from-file=-","--pathspec-file-nul","--"],{cwd:t.cwd,input:r})},"updateIndexAgain");var vt=Object.defineProperty,P=f((e,t)=>vt(e,"name",{value:t,configurable:!0}),"r$1");const yt="vis_staged_automatic_backup",xe=P(()=>`${yt}-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2,8)}`,"buildMessage"),kt=P(async e=>{const t=await R(["stash","create"],{cwd:e});return t.length===0?null:(await g(["stash","store","-m",xe(),t],{cwd:e}),t)},"createBackupStash"),bt=P(async e=>{const t=xe(),{exitCode:r,stdout:n}=await g(["stash","push","--keep-index","--include-untracked","--quiet","-m",t],{cwd:e,lenient:!0});if(r!==0)return null;const s=n.trim();return s.length>0&&/no local changes/i.test(s)?null:R(["rev-parse","stash@{0}"],{cwd:e})},"createHideAllStash"),ee=P(async(e,t)=>{const{exitCode:r,stdout:n}=await g(["reflog","--format=%H %gd","refs/stash"],{cwd:e,lenient:!0});if(r!==0)return null;for(const s of n.split(/\r?\n/)){const[i,o]=s.split(/\s+/,2);if(i===t&&o!==void 0)return o}return null},"findStashRefBySha"),fe=P(async(e,t)=>{if(t===null)return;const r=await ee(e,t);r!==null&&await g(["stash","drop","--quiet",r],{cwd:e})},"dropBackupStash"),Tt=P(async(e,t)=>{if(t===null)throw new ae("Backup stash was not found — can't revert working tree.");const r=await ee(e,t);if(r===null)throw new ae(`Backup stash ${t} is no longer reachable — can't revert working tree.`);await g(["reset","--hard","HEAD"],{cwd:e}),await g(["stash","apply","--index","--quiet",r],{cwd:e})},"applyBackupStash"),St=P(async(e,t)=>{if(t===null)return;const r=await ee(e,t);r!==null&&await g(["stash","pop","--quiet",r],{cwd:e})},"popHideAllStash");var $t=Object.defineProperty,xt=f((e,t)=>$t(e,"name",{value:t,configurable:!0}),"l");class Ct{static{f(this,"GitWorkflow")}static{xt(this,"GitWorkflow")}stagedFiles=[];partiallyStaged=[];workTree="";gitDir="";preTaskIndexTree="";postTaskIndexTree="";headTree="";revertApplied=!1;warnings=[];cwd;options;patch=null;backupStashSha=null;merge=[];shouldStash;shouldHidePartial;shouldHideUnstaged;shouldHideAll;hideAllStashSha=null;intentToAddPaths=[];preTaskUntracked=new Set;constructor(t){this.cwd=t.cwd??process.cwd(),this.options=t,this.shouldStash=t.stash!==!1&&t.diff===void 0,this.shouldHidePartial=t.hidePartiallyStaged!==!1,this.shouldHideUnstaged=t.hideUnstaged===!0,this.shouldHideAll=t.hideAll===!0}async prepare(){if(!await nt(this.cwd))throw new _(`Not a git repository: ${this.cwd}`);await ct(this.cwd),this.workTree=await $e(this.cwd),this.gitDir=await at(this.cwd),this.stagedFiles=await ht({cwd:this.cwd,diff:this.options.diff,diffFilter:this.options.diffFilter,workTree:this.workTree}),this.partiallyStaged=this.stagedFiles.length===0?[]:await gt(this.cwd),this.snapshotMergeState(),this.intentToAddPaths=await ut(this.workTree),this.intentToAddPaths.length>0&&await ft(this.intentToAddPaths,{cwd:this.workTree}),this.shouldStash?this.backupStashSha=await kt(this.workTree):this.partiallyStaged.length>0&&this.warnings.push("Running with --no-stash on partially-staged files — unstaged deltas will be captured to a patch, but if re-applying the patch fails after tasks run the changes cannot be recovered."),this.shouldHideAll?this.hideAllStashSha=await bt(this.workTree):await this.hideUnstagedChanges(),this.preTaskIndexTree=this.stagedFiles.length===0?"":await de(this.workTree),this.postTaskIndexTree=this.preTaskIndexTree,this.headTree=await it(this.workTree),this.preTaskUntracked=new Set(await ue(this.workTree))}async applyModifications({autoStage:t=!1}={}){if(this.stagedFiles.length===0)return;const r=this.stagedFiles.map(n=>D(this.workTree,n));if(await wt(r,{cwd:this.workTree}),t){const n=(await ue(this.workTree)).filter(s=>!this.preTaskUntracked.has(s));if(n.length>0){const s=`${n.join("\0")}\0`;await g(["add","--pathspec-from-file=-","--pathspec-file-nul","--"],{cwd:this.workTree,input:s})}}if(this.intentToAddPaths.length>0)try{await g(["add","--intent-to-add","--",...this.intentToAddPaths],{cwd:this.workTree})}catch{}this.postTaskIndexTree=await de(this.workTree)}indexTreeChanged(){return this.preTaskIndexTree.length>0&&this.postTaskIndexTree.length>0&&this.preTaskIndexTree!==this.postTaskIndexTree}postTaskIndexMatchesHead(){return this.postTaskIndexTree.length>0&&this.headTree.length>0&&this.postTaskIndexTree===this.headTree}async restoreUnstagedChanges(){if(this.revertApplied||this.patch===null||this.shouldHideAll)return;const t=["apply","--whitespace=nowarn","--recount","--unidiff-zero"];let r;try{await g(t,{cwd:this.workTree,input:this.patch});return}catch(n){r=n instanceof _?n.stderr:String(n)}try{await g([...t,"--3way"],{cwd:this.workTree,input:this.patch})}catch(n){const s=n instanceof _&&n.stderr?n.stderr:String(n);throw new ie(`Failed to re-apply unstaged changes after running tasks. Original changes remain in the backup stash — recover with \`git stash list\` and \`git stash apply\`.
12
- First attempt: ${r??"(no stderr)"}
13
- Second attempt: ${s}`,{cause:n})}}async revert(){if(this.backupStashSha!==null){try{await Tt(this.workTree,this.backupStashSha)}catch(t){throw new ie("Revert failed while restoring the backup stash. Use `git stash list` to recover manually.",{cause:t})}if(await fe(this.workTree,this.backupStashSha),this.intentToAddPaths.length>0)try{await g(["add","--intent-to-add","--",...this.intentToAddPaths],{cwd:this.workTree})}catch{}this.revertApplied=!0}}async cleanup(t){if(this.restoreMergeState(),this.hideAllStashSha!==null)try{await St(this.workTree,this.hideAllStashSha)}catch{}t&&this.backupStashSha!==null&&!this.revertApplied&&await fe(this.workTree,this.backupStashSha)}recoveryHint(){return this.backupStashSha===null?null:`Backup stash is preserved (sha ${this.backupStashSha.slice(0,7)}) — restore with: git stash apply --index ${this.backupStashSha}`}async hideUnstagedChanges(){const t=new Set(this.stagedFiles.map(n=>D(this.workTree,n))),r=this.shouldHideUnstaged?[...t]:this.shouldHidePartial?this.partiallyStaged.filter(n=>t.has(n)):[];r.length!==0&&(this.patch=await pt(r,{cwd:this.workTree}),this.patch!==null&&await mt(r,{cwd:this.workTree}))}snapshotMergeState(){this.gitDir.length!==0&&(this.merge=["MERGE_HEAD","MERGE_MODE","MERGE_MSG"].map(t=>{const r=X(this.gitDir,t);return re(r)?{body:Oe(r),existed:!0,name:t}:{body:null,existed:!1,name:t}}))}restoreMergeState(){if(!(this.gitDir.length===0||this.merge.length===0))for(const t of this.merge){const r=X(this.gitDir,t.name);try{t.existed&&t.body!==null?Pe(r,t.body):re(r)&&Re(r)}catch{}}}}var It=Object.defineProperty,V=f((e,t)=>It(e,"name",{value:t,configurable:!0}),"r");const Ce=V(e=>e.includes("/"),"isPathStyle"),U=V((e,t)=>t?e.toLowerCase():e,"normalizeForMatch"),Et=V((e,t,r,n={})=>{const s=Ce(e),i=n.caseInsensitive===!0,o=U(e,i),a=[];for(const c of t){const l=s?D(r,c):ve(c);ke(o,U(l,i))&&a.push(c)}return a},"matchFiles"),At=V((e,t,r,n={})=>{if(!t||t.length===0)return[...e];const s=n.caseInsensitive===!0;return e.filter(i=>{for(const o of t){const a=Ce(o)?D(r,i):ve(i),c=U(o,s);if(ke(c,U(a,s)))return!1}return!0})},"applyIgnore");var Mt=Object.defineProperty,H=f((e,t)=>Mt(e,"name",{value:t,configurable:!0}),"g");const jt=H(e=>{switch(e){case"failed":return"red";case"running":return"cyan";case"skipped":return"yellow";case"success":return"green";default:return"gray"}},"colorForStatus"),he=H(e=>{if(e==="running")return k(Ne,{type:"dots"});const t=e==="failed"?be:e==="skipped"?B:e==="success"?Te:B;return k(S,{color:jt(e),children:t})},"iconForStatus"),pe=H(({state:e,tick:t,verbose:r})=>x(A,{flexDirection:"column",children:[[...e.patterns.values()].map(n=>x(A,{flexDirection:"column",children:[x(A,{children:[he(n.status),x(S,{children:[" ",n.title]})]}),[...n.commands.values()].map(s=>x(A,{flexDirection:"column",marginLeft:2,children:[x(A,{children:[he(s.status),x(S,{children:[" ",s.title," "]}),s.status!=="pending"&&s.status!=="running"?x(S,{color:"gray",children:["(",s.durationMs,"ms)"]}):null]}),r&&s.output?k(A,{flexDirection:"column",marginLeft:2,children:s.output.split(/\r?\n/).slice(0,20).map((i,o)=>k(S,{color:"gray",children:i},`${s.id}-line-${o}`))}):null,s.status==="failed"&&s.error?k(A,{marginLeft:2,children:k(S,{color:"red",children:s.error.message})}):null]},s.id))]},n.id)),e.infoMessages.map((n,s)=>k(S,{color:"gray",children:n},`info-${s}`)),e.warnMessages.map((n,s)=>k(S,{color:"yellow",children:n},`warn-${s}`)),e.errorMessages.map(({message:n},s)=>k(S,{color:"red",children:n},`err-${s}`))]}),"App"),Ot=H((e={})=>{const t=e.verbose===!0,r={errorMessages:[],infoMessages:[],patterns:new Map,started:!1,warnMessages:[]};let n=0;const s=Ge(k(pe,{state:r,tick:n,verbose:t}),{exitOnCtrlC:!1,stdout:process.stderr}),i=H(()=>{n+=1,s.rerender(k(pe,{state:r,tick:n,verbose:t}))},"refresh");return{commandEnd({commandId:o,durationMs:a,error:c,output:l,patternId:u,status:d}){const p=r.patterns.get(u)?.commands.get(o);p&&(p.status=d,p.durationMs=a,p.output=l,p.error=c,i())},commandStart({commandId:o,patternId:a}){const c=r.patterns.get(a)?.commands.get(o);c&&(c.status="running",i())},error({error:o,message:a}){r.errorMessages.push({error:o,message:a}),i()},info({message:o}){r.infoMessages.push(o),i()},patternEnd({patternId:o,status:a}){const c=r.patterns.get(o);c&&(c.status=a,i())},patternStart({patternId:o}){const a=r.patterns.get(o);a&&(a.status="running",i())},start({patterns:o}){r.started=!0;for(const a of o){const c=new Map;for(const l of a.commands)c.set(l.id,{durationMs:0,id:l.id,status:"pending",title:l.title});r.patterns.set(a.id,{commands:c,id:a.id,status:"pending",title:a.title})}i()},async stop(){s.unmount(),await s.waitUntilExit()},warn({message:o}){r.warnMessages.push(o),i()}}},"createInkRenderer");var Pt=Object.defineProperty,J=f((e,t)=>Pt(e,"name",{value:t,configurable:!0}),"a$1");const ge=J((e={})=>{const{quiet:t=!1,verbose:r=!1}=e,n=new Map,s=new Map,i=J(a=>{t||process.stderr.write(`${a}
14
- `)},"print"),o=J(a=>{switch(a){case"failed":return Y(be);case"running":return K(">");case"skipped":return se(B);case"success":return _e(Te);default:return C(B)}},"iconFor");return{commandEnd({commandId:a,durationMs:c,error:l,output:u,status:d}){const p=s.get(a)??a,w=C(`(${c}ms)`);if(i(` ${o(d)} ${p} ${w}`),d==="failed"&&l&&i(C(l.message)),(d==="failed"||r)&&u&&u.trim().length>0)for(const h of u.split(/\r?\n/))i(` ${C(h)}`)},commandStart({commandId:a}){if(!r)return;const c=s.get(a)??a;i(` ${C("…")} ${c}`)},error({error:a,message:c}){t?process.stderr.write(`${Y(c)}
15
- `):i(Y(c)),a?.stack&&(r||!t)&&process.stderr.write(`${C(a.stack)}
16
- `)},info({message:a}){i(C(a))},patternEnd({patternId:a,status:c}){const l=n.get(a)??a;i(`${o(c)} ${l}`)},patternStart({patternId:a}){const c=n.get(a)??a;i(`${K(">")} ${c}`)},start({patterns:a}){if(a.length===0){i(C("No staged files matched any pattern."));return}const c=new Set(a.flatMap(l=>l.files)).size;i(`${K(">")} Running staged tasks on ${c} file${c===1?"":"s"} across ${a.length} pattern${a.length===1?"":"s"}`);for(const l of a){n.set(l.id,l.title);for(const u of l.commands)s.set(u.id,u.title)}},stop(){},warn({message:a}){i(se(a))}}},"createPlainRenderer");var Rt=Object.defineProperty,Ft=f((e,t)=>Rt(e,"name",{value:t,configurable:!0}),"t");const Nt=Ft(async e=>{const{env:t}=process;if(e.debug===!0||e.quiet===!0||t.NODE_ENV==="test"||t.TERM==="dumb"||t.CI!==void 0||!process.stderr.isTTY)return ge({quiet:e.quiet,verbose:e.verbose});try{return Ot({verbose:e.verbose})}catch{return ge({quiet:e.quiet,verbose:e.verbose})}},"pickRenderer");var Gt=Object.defineProperty,G=f((e,t)=>Gt(e,"name",{value:t,configurable:!0}),"a");const Q=G(e=>typeof e=="object"&&e!==null&&typeof e.title=="string"&&typeof e.task=="function","isCustomTask"),_t=G(async e=>{let t=0,r=0;const n=G(()=>(t+=1,`pattern-${t}`),"nextPatternId"),s=G(()=>(r+=1,`cmd-${r}`),"nextCommandId"),i=G(async(a,c,l)=>{if(typeof a=="string"){l.push({command:a,files:c,id:s(),source:"string",title:a});return}if(Array.isArray(a)){for(const u of a)await i(u,c,l);return}if(typeof a=="function"){const u=await a([...c]);if(typeof u=="string"){l.push({command:u,files:c,id:s(),source:"function",title:u});return}if(Array.isArray(u)){for(const d of u)if(typeof d=="string")l.push({command:d,files:c,id:s(),source:"function",title:d});else if(Q(d))l.push({files:c,id:s(),run:d.task,source:"custom",title:d.title});else throw new b("Task function returned an array with an unsupported entry — expected strings or { title, task }.");return}if(Q(u)){l.push({files:c,id:s(),run:u.task,source:"custom",title:u.title});return}throw new b("Task function returned an unsupported value — expected string, string[], or { title, task }.")}if(Q(a)){l.push({files:c,id:s(),run:a.task,source:"custom",title:a.title});return}throw new b("Unsupported task value — expected string, string[], function, or { title, task }.")},"expandTask"),o=[];for(const[a,c]of Object.entries(e.config)){const l=Et(a,e.files,e.cwd,{caseInsensitive:e.caseInsensitive===!0});if(l.length===0)continue;const u=e.relative?l.map(p=>D(e.cwd,p)):l,d=[];await i(c,u,d),d.length!==0&&o.push({commands:d,files:u,id:n(),pattern:a,title:`${a} — ${l.length} file${l.length===1?"":"s"}`})}return o},"buildTaskGraph");var Dt=Object.defineProperty,z=f((e,t)=>Dt(e,"name",{value:t,configurable:!0}),"d$1");const Ht=z(e=>{const t=[];let r="",n=!1,s=!1;for(let i=0;i<e.length;i+=1){const o=e[i];if(o===void 0)break;if(o==="\\"&&!n&&i+1<e.length){const a=e[i+1];if(a!==void 0){s&&a!=='"'&&a!=="\\"&&(r+=o),r+=a,i+=1;continue}}if(o==='"'&&!n){s=!s;continue}if(o==="'"&&!s){n=!n;continue}if(!n&&!s&&/\s/.test(o)){r.length>0&&(t.push(r),r="");continue}r+=o}if(n||s)throw new b(`Unterminated ${n?"single":"double"} quote in command: ${e}`);return r.length>0&&t.push(r),t},"parseCommandString"),Ie=process.platform==="win32"?28e3:131072,Lt=z((e,t,r)=>{const n=[];let s=[],i=t;const o=r<=0?Ie:r;for(const a of e){const c=Buffer.byteLength(a)+1;s.length>0&&i+c>o&&(n.push(s),s=[],i=t),s.push(a),i+=c}return s.length>0&&n.push(s),n},"chunkFiles"),Bt=z(async(e,t,r)=>{const n=Ht(e);if(n.length===0)throw new M(e,"Empty command for staged task.");const[s,...i]=n;if(s===void 0)throw new M(e,"Empty command for staged task.");const o=Buffer.byteLength(s)+i.reduce((u,d)=>u+Buffer.byteLength(d)+1,0),a=Lt(t,o,r.maxArgLength??Ie),c=Date.now(),l=[];for(const u of a){if(r.signal?.aborted===!0)throw new M(e,"Task aborted by earlier failure.");const d=await ye(s,[...i,...u],{cancelSignal:r.signal,cwd:r.cwd,env:Ut(r.env),killSignal:r.killSignal??"SIGTERM",reject:!1,stderr:"pipe",stdout:"pipe"}),p=typeof d.stdout=="string"?d.stdout:"",w=typeof d.stderr=="string"?d.stderr:"",h=[p,w].filter(m=>m.length>0).join(`
17
- `);if(h.length>0&&l.push(h),d.isCanceled||d.isTerminated||typeof d.exitCode!="number"){const m=d.isCanceled?"Task aborted by earlier failure.":d.isTerminated?`Task killed by signal ${d.signal??"(unknown)"}.`:h.trim()||"Task exited without a numeric status code.";throw new M(e,m)}if(d.exitCode!==0)throw new M(e,h.trim()||`Exit code ${d.exitCode} from ${s}`)}return{durationMs:Date.now()-c,output:l.join(`
18
- `)}},"execCommand"),Ut=z(e=>{const t={...process.env};return process.stderr.isTTY&&t.FORCE_COLOR===void 0&&t.NO_COLOR===void 0&&(t.FORCE_COLOR="1"),e?{...t,...e}:t},"buildTaskEnv");var qt=Object.defineProperty,j=f((e,t)=>qt(e,"name",{value:t,configurable:!0}),"o");const Vt=j(async(e,t,r)=>{const n=Wt(r.concurrent,e.length),s=[],i=new AbortController;let o=!1,a=0;const c=j(()=>{o||(o=!0,r.continueOnError||i.abort())},"cancel");r.externalSignal&&(r.externalSignal.aborted?c():r.externalSignal.addEventListener("abort",()=>{c()},{once:!0}));const l=j(h=>{for(const m of h.commands)t.commandEnd({commandId:m.id,durationMs:0,patternId:h.id,status:"skipped"})},"emitSkippedCommands"),u=j(async h=>{if(o){l(h),t.patternEnd({patternId:h.id,status:"skipped"});return}t.patternStart({patternId:h.id});let m="success";for(const v of h.commands){if(o){t.commandEnd({commandId:v.id,durationMs:0,patternId:h.id,status:"skipped"}),m=m==="success"?"skipped":m;continue}t.commandStart({commandId:v.id,patternId:h.id});const y=await zt(v,r,i.signal),T=y.status==="failed"&&i.signal.aborted?{...y,status:"skipped"}:y;if(t.commandEnd({commandId:v.id,durationMs:T.durationMs,error:T.error,output:T.output,patternId:h.id,status:T.status}),T.status==="failed"){s.push(v.title),m="failed",c();break}if(T.status==="skipped"){m=m==="success"?"skipped":m;break}}t.patternEnd({patternId:h.id,status:m})},"runOne"),d=j(async()=>{for(;a<e.length;){const h=e[a];a+=1,h&&await u(h)}},"pickNext"),p=[];for(let h=0;h<Math.min(n,e.length);h+=1)p.push(d());await Promise.all(p);const w=r.externalSignal?.aborted===!0;return{failedCommands:s,success:s.length===0&&!w}},"runTasks"),zt=j(async(e,t,r)=>{const n=Date.now();try{if(e.source==="custom"&&e.run)return await e.run([...e.files]),{durationMs:Date.now()-n,status:"success"};if(e.command){const s=await Bt(e.command,e.files,{cwd:t.cwd,killSignal:t.killSignal,maxArgLength:t.maxArgLength,signal:r});return{durationMs:s.durationMs,output:t.verbose?s.output:void 0,status:"success"}}return{durationMs:Date.now()-n,error:new M(e.title,"Command has no invocation target."),status:"failed"}}catch(s){const i=s instanceof Error?s:new Error(String(s));return{durationMs:Date.now()-n,error:i,output:i instanceof M?i.message:void 0,status:"failed"}}},"runCommand"),Wt=j((e,t)=>{if(e===!1)return 1;if(e===!0){const n=Math.max(1,typeof ne=="function"?ne():4);return Math.min(Math.max(1,t),n)}const r=Math.floor(e);return r>0?r:1},"concurrencyLimit");var Kt=Object.defineProperty,Z=f((e,t)=>Kt(e,"name",{value:t,configurable:!0}),"u");const Yt=!0,Jt=Z(e=>{try{return!je(e)}catch{return!1}},"detectCaseInsensitive"),Qt=Z(async(e={})=>{const t=e.cwd??process.cwd(),r=await Nt(e),n=await tt(e);typeof n!="function"&&oe(n);const s=new Ct({...e,cwd:t}),i=new AbortController;let o=!1;const a=Z(d=>{if(o){process.removeListener("SIGINT",a),process.removeListener("SIGTERM",a),process.kill(process.pid,d);return}o=!0,r.warn({message:`Received ${d} — cancelling staged tasks and restoring state. Press Ctrl+C again to abort.`}),i.abort()},"onInterrupt");process.on("SIGINT",a),process.on("SIGTERM",a);let c={failedCommands:[],ranTasks:!1,success:!0},l=!1,u=!1;try{await s.prepare(),l=!0;for(const y of s.warnings)r.warn({message:y});if(s.stagedFiles.length===0)return e.allowEmpty!==!0&&r.info({message:"No staged files found."}),u=!0,{failedCommands:[],ranTasks:!1,success:!0};const d=Jt(t),p=At(s.stagedFiles,e.ignore,t,{caseInsensitive:d});if(p.length===0&&s.stagedFiles.length>0)return r.info({message:"Every staged file was excluded by the `ignore` list."}),u=!0,{failedCommands:[],ranTasks:!1,success:!0};const w=typeof n=="function"?oe(await n([...p])):n,h=await _t({caseInsensitive:d,config:w,cwd:t,files:p,relative:e.relative});if(r.start({patterns:h}),h.length===0)return r.info({message:"No staged files matched any pattern."}),u=!0,{failedCommands:[],ranTasks:!1,success:!0};const{failedCommands:m,success:v}=await Vt(h,r,{concurrent:e.concurrent??Yt,continueOnError:e.continueOnError===!0,cwd:t,externalSignal:i.signal,killSignal:e.killSignal,maxArgLength:e.maxArgLength,verbose:e.verbose});if(c={failedCommands:m,ranTasks:!0,success:v},v){if(e.diff===void 0&&(await s.applyModifications({autoStage:e.autoStage===!0}),e.failOnChanges===!0&&s.indexTreeChanged()&&(r.warn({message:"Tasks modified staged content — failing because --fail-on-changes is set."}),c={failedCommands:[...m],ranTasks:!0,success:!1}),e.allowEmpty!==!0&&s.postTaskIndexMatchesHead()))throw new Ue("All staged changes were reverted by tasks. Re-stage changes or rerun with --allow-empty.")}else if(e.revert===!0)r.info({message:"Reverting working tree from backup stash."}),await s.revert();else{const y=s.recoveryHint();y&&r.warn({message:y})}return await s.restoreUnstagedChanges(),u=c.success,c}catch(d){const p=d instanceof Error?d.message:String(d),w=d instanceof Error?d:new Error(p);if(r.error({error:w,message:p}),d instanceof O)return{failedCommands:c.failedCommands,ranTasks:c.ranTasks,success:!1};throw d}finally{if(process.removeListener("SIGINT",a),process.removeListener("SIGTERM",a),l)try{await s.cleanup(u)}catch(d){r.error({error:d,message:"Cleanup failed."})}await r.stop()}},"runStaged");var Xt=Object.defineProperty,Zt=f((e,t)=>Xt(e,"name",{value:t,configurable:!0}),"n");const er="VIS_STAGED_CONCURRENT",me=Zt(e=>{const t=e.trim();if(t==="true"||t==="")return!0;if(t==="false")return!1;const r=Number(t);return Number.isNaN(r)?!0:r},"parseConcurrent");var tr=Object.defineProperty,L=f((e,t)=>tr(e,"name",{value:t,configurable:!0}),"d");const rr=L((e,t)=>{const r={};t!==void 0&&(r.config=t);const n=L(E=>e[E]===void 0?void 0:!!e[E],"readBool"),s=L(E=>{const W=e[E];return typeof W=="string"&&W.length>0?W:void 0},"readString"),i=n("allow-empty");i!==void 0&&(r.allowEmpty=i);const o=n("auto-stage");o!==void 0&&(r.autoStage=o);const a=n("continue-on-error");a!==void 0&&(r.continueOnError=a);const c=n("debug");c!==void 0&&(r.debug=c);const l=n("fail-on-changes");l!==void 0&&(r.failOnChanges=l);const u=n("hide-partially-staged");u!==void 0&&(r.hidePartiallyStaged=u);const d=n("hide-unstaged");d!==void 0&&(r.hideUnstaged=d);const p=n("quiet");p!==void 0&&(r.quiet=p);const w=n("relative");w!==void 0&&(r.relative=w);const h=n("revert");h!==void 0&&(r.revert=h);const m=n("stash");m!==void 0&&(r.stash=m);const v=n("verbose");v!==void 0&&(r.verbose=v);const y=s("cwd");y!==void 0&&(r.cwd=y);const T=s("diff");T!==void 0&&(r.diff=T);const te=s("diff-filter");if(te!==void 0&&(r.diffFilter=te),n("force-kill")===!0&&(r.killSignal="SIGKILL"),e.concurrent===void 0){const E=process.env[er];E!==void 0&&(r.concurrent=me(E.trim()))}else r.concurrent=me(String(e.concurrent));return r},"buildRunOptions"),hr=L(async({options:e,visConfig:t})=>{const r=(t??{}).staged;if(!r)throw new Error(`No "staged" config found in vis.config.ts. Add one:
7
+ const __cjs_getBuiltinModule = (module) => {
8
+ // Check if we're in Node.js and version supports getBuiltinModule
9
+ if (typeof __cjs_getProcess !== "undefined" && __cjs_getProcess.versions && __cjs_getProcess.versions.node) {
10
+ const [major, minor] = __cjs_getProcess.versions.node.split(".").map(Number);
11
+ // Node.js 20.16.0+ and 22.3.0+
12
+ if (major > 22 || (major === 22 && minor >= 3) || (major === 20 && minor >= 16)) {
13
+ return __cjs_getProcess.getBuiltinModule(module);
14
+ }
15
+ }
16
+ // Fallback to createRequire
17
+ return __cjs_require(module);
18
+ };
19
19
 
20
- // vis.config.ts
21
- import { defineConfig } from "@visulima/vis/config";
20
+ import { Box, Text, TextInput, ScrollBar, ScrollView, useApp, useWindowSize, useInput, Dialog, Tabs, Tab, render } from '@visulima/tui';
21
+ import { _ as QuitDialog, c as detectPm, i as isInCi } from './bin.js';
22
+ import React, { useMemo, useSyncExternalStore, useState, useRef, useEffect, useCallback } from 'react';
23
+ const {
24
+ writeFileSync
25
+ } = __cjs_getBuiltinModule("node:fs");
26
+ import { isAccessibleSync, readFileSync, ensureDirSync } from '@visulima/fs';
27
+ import { stripJsonComments } from '@visulima/fs/utils';
28
+ import { join, dirname } from '@visulima/path';
29
+ import { jsxs, jsx } from 'react/jsx-runtime';
22
30
 
23
- export default defineConfig({
24
- staged: { '*': 'vis check --fix' },
25
- });
31
+ const TEMPLATES = [
32
+ {
33
+ config: {
34
+ customizations: {
35
+ vscode: {
36
+ extensions: ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
37
+ }
38
+ },
39
+ features: {
40
+ "ghcr.io/devcontainers/features/git:1": {},
41
+ "ghcr.io/devcontainers/features/github-cli:1": {}
42
+ },
43
+ forwardPorts: [3e3],
44
+ image: "mcr.microsoft.com/devcontainers/javascript-node:22",
45
+ name: "Node.js",
46
+ postCreateCommand: "npm install"
47
+ },
48
+ description: "Node.js 22 with Git and GitHub CLI",
49
+ id: "node",
50
+ name: "Node.js"
51
+ },
52
+ {
53
+ config: {
54
+ customizations: {
55
+ vscode: {
56
+ extensions: ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
57
+ }
58
+ },
59
+ features: {
60
+ "ghcr.io/devcontainers/features/git:1": {},
61
+ "ghcr.io/devcontainers/features/github-cli:1": {}
62
+ },
63
+ forwardPorts: [3e3],
64
+ image: "mcr.microsoft.com/devcontainers/javascript-node:22",
65
+ mounts: [
66
+ {
67
+ source: "${localWorkspaceFolderBasename}-node_modules",
68
+ target: "${containerWorkspaceFolder}/node_modules",
69
+ type: "volume"
70
+ },
71
+ {
72
+ source: "${localWorkspaceFolderBasename}-pnpm-store",
73
+ target: "/home/node/.local/share/pnpm/store",
74
+ type: "volume"
75
+ }
76
+ ],
77
+ name: "Node.js + pnpm Monorepo",
78
+ postCreateCommand: "corepack enable && pnpm install",
79
+ remoteUser: "node",
80
+ workspaceFolder: "/workspaces/${localWorkspaceFolderBasename}"
81
+ },
82
+ description: "Node.js 22 with pnpm, corepack, and optimized volume mounts",
83
+ id: "node-pnpm",
84
+ name: "Node.js + pnpm"
85
+ },
86
+ {
87
+ config: {
88
+ customizations: {
89
+ vscode: {
90
+ extensions: ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "ms-azuretools.vscode-docker"]
91
+ }
92
+ },
93
+ dockerComposeFile: "docker-compose.yml",
94
+ forwardPorts: [3e3, 5432],
95
+ name: "Node.js + PostgreSQL",
96
+ postCreateCommand: "npm install",
97
+ service: "app",
98
+ workspaceFolder: "/workspaces/${localWorkspaceFolderBasename}"
99
+ },
100
+ description: "Node.js with PostgreSQL via Docker Compose",
101
+ id: "node-postgres",
102
+ name: "Node.js + PostgreSQL"
103
+ },
104
+ {
105
+ config: {
106
+ customizations: {
107
+ vscode: {
108
+ extensions: ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "ms-azuretools.vscode-docker"]
109
+ }
110
+ },
111
+ features: {
112
+ "ghcr.io/devcontainers/features/docker-in-docker:2": {},
113
+ "ghcr.io/devcontainers/features/git:1": {},
114
+ "ghcr.io/devcontainers/features/github-cli:1": {}
115
+ },
116
+ forwardPorts: [3e3],
117
+ image: "mcr.microsoft.com/devcontainers/javascript-node:22",
118
+ name: "Node.js + Docker",
119
+ postCreateCommand: "npm install"
120
+ },
121
+ description: "Node.js 22 with Docker-in-Docker for container workflows",
122
+ id: "node-dind",
123
+ name: "Node.js + Docker-in-Docker"
124
+ },
125
+ {
126
+ config: {
127
+ customizations: {
128
+ vscode: {
129
+ extensions: ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "ms-azuretools.vscode-docker"]
130
+ }
131
+ },
132
+ dockerComposeFile: "docker-compose.yml",
133
+ features: {
134
+ "ghcr.io/devcontainers/features/docker-in-docker:2": {}
135
+ },
136
+ forwardPorts: [3e3, 5432, 6379],
137
+ name: "Full Stack",
138
+ postCreateCommand: "npm install",
139
+ service: "app",
140
+ workspaceFolder: "/workspaces/${localWorkspaceFolderBasename}"
141
+ },
142
+ description: "Node.js + PostgreSQL + Redis + Docker via Compose",
143
+ id: "fullstack",
144
+ name: "Full Stack"
145
+ },
146
+ {
147
+ config: {
148
+ customizations: {
149
+ vscode: {
150
+ extensions: ["ms-python.python", "ms-python.vscode-pylance"],
151
+ settings: {
152
+ "editor.formatOnSave": true,
153
+ "python.defaultInterpreterPath": "/usr/local/bin/python"
154
+ }
155
+ }
156
+ },
157
+ features: {
158
+ "ghcr.io/devcontainers/features/git:1": {},
159
+ "ghcr.io/devcontainers/features/github-cli:1": {},
160
+ "ghcr.io/devcontainers/features/python:1": { version: "3.12" }
161
+ },
162
+ forwardPorts: [8e3],
163
+ image: "mcr.microsoft.com/devcontainers/python:3.12",
164
+ name: "Python",
165
+ postCreateCommand: "pip install -r requirements.txt || true"
166
+ },
167
+ description: "Python 3.12 with pip and venv",
168
+ id: "python",
169
+ name: "Python"
170
+ },
171
+ {
172
+ config: {
173
+ customizations: {
174
+ vscode: {
175
+ extensions: ["golang.go"],
176
+ settings: {
177
+ "editor.formatOnSave": true,
178
+ "go.toolsManagement.autoUpdate": true
179
+ }
180
+ }
181
+ },
182
+ features: {
183
+ "ghcr.io/devcontainers/features/git:1": {},
184
+ "ghcr.io/devcontainers/features/go:1": { version: "1.22" }
185
+ },
186
+ forwardPorts: [8080],
187
+ image: "mcr.microsoft.com/devcontainers/go:1.22",
188
+ name: "Go",
189
+ postCreateCommand: "go mod download || true"
190
+ },
191
+ description: "Go 1.22 development environment",
192
+ id: "go",
193
+ name: "Go"
194
+ },
195
+ {
196
+ config: {
197
+ customizations: {
198
+ vscode: {
199
+ extensions: ["rust-lang.rust-analyzer", "tamasfe.even-better-toml"],
200
+ settings: {
201
+ "editor.formatOnSave": true
202
+ }
203
+ }
204
+ },
205
+ features: {
206
+ "ghcr.io/devcontainers/features/git:1": {},
207
+ "ghcr.io/devcontainers/features/rust:1": {}
208
+ },
209
+ image: "mcr.microsoft.com/devcontainers/rust:latest",
210
+ name: "Rust",
211
+ postCreateCommand: "cargo build || true"
212
+ },
213
+ description: "Rust development with cargo and rust-analyzer",
214
+ id: "rust",
215
+ name: "Rust"
216
+ },
217
+ {
218
+ config: {
219
+ customizations: {
220
+ vscode: {
221
+ extensions: ["vscjava.vscode-java-pack", "vscjava.vscode-maven"]
222
+ }
223
+ },
224
+ features: {
225
+ "ghcr.io/devcontainers/features/git:1": {},
226
+ "ghcr.io/devcontainers/features/java:1": { version: "17" }
227
+ },
228
+ forwardPorts: [8080],
229
+ image: "mcr.microsoft.com/devcontainers/java:17",
230
+ name: "Java",
231
+ postCreateCommand: "./mvnw install || ./gradlew build || true"
232
+ },
233
+ description: "Java 17 with Maven/Gradle support",
234
+ id: "java",
235
+ name: "Java"
236
+ },
237
+ {
238
+ config: {
239
+ customizations: {
240
+ vscode: {
241
+ extensions: ["ms-azuretools.vscode-docker", "ms-kubernetes-tools.vscode-kubernetes-tools", "hashicorp.terraform"]
242
+ }
243
+ },
244
+ features: {
245
+ "ghcr.io/devcontainers/features/aws-cli:1": {},
246
+ "ghcr.io/devcontainers/features/azure-cli:1": {},
247
+ "ghcr.io/devcontainers/features/docker-in-docker:2": {},
248
+ "ghcr.io/devcontainers/features/kubectl-helm-minikube:1": {},
249
+ "ghcr.io/devcontainers/features/terraform:1": {}
250
+ },
251
+ image: "mcr.microsoft.com/devcontainers/base:ubuntu",
252
+ name: "DevOps"
253
+ },
254
+ description: "Docker, Kubernetes, Terraform, AWS & Azure CLIs",
255
+ id: "devops",
256
+ name: "DevOps"
257
+ },
258
+ {
259
+ config: {
260
+ features: {
261
+ "ghcr.io/devcontainers/features/common-utils:2": {}
262
+ },
263
+ image: "mcr.microsoft.com/devcontainers/base:ubuntu",
264
+ name: "Minimal",
265
+ remoteUser: "vscode"
266
+ },
267
+ description: "Bare Ubuntu with common utilities",
268
+ id: "minimal",
269
+ name: "Minimal"
270
+ },
271
+ {
272
+ config: {
273
+ image: "mcr.microsoft.com/devcontainers/base:ubuntu",
274
+ name: "Custom"
275
+ },
276
+ description: "Minimal Ubuntu base - configure from scratch",
277
+ id: "custom",
278
+ name: "Custom (Blank)"
279
+ }
280
+ ];
26
281
 
27
- Migrating from lint-staged or nano-staged? Run \`vis migrate lint-staged\` (or \`vis migrate nano-staged\`) to move the config in and remove the legacy files.`);(await Qt(rr(e,r))).success||(process.exitCode=1)},"execute");export{rr as buildRunOptions,hr as default};
282
+ const readDevcontainerJson = (workspaceRoot) => {
283
+ const filePath = join(workspaceRoot, ".devcontainer", "devcontainer.json");
284
+ if (!isAccessibleSync(filePath)) {
285
+ return null;
286
+ }
287
+ const raw = readFileSync(filePath);
288
+ const stripped = stripJsonComments(raw);
289
+ const hadComments = stripped !== raw;
290
+ let config;
291
+ try {
292
+ config = JSON.parse(stripped);
293
+ } catch (error) {
294
+ const message = error instanceof Error ? error.message : String(error);
295
+ throw new Error(`Failed to parse ${filePath}: ${message}`);
296
+ }
297
+ return { config, hadComments };
298
+ };
299
+ const writeDevcontainerJson = (workspaceRoot, config, outputPath) => {
300
+ const dir = outputPath ? dirname(outputPath) : join(workspaceRoot, ".devcontainer");
301
+ const filePath = outputPath ?? join(dir, "devcontainer.json");
302
+ ensureDirSync(dir);
303
+ writeFileSync(filePath, `${JSON.stringify(config, null, 2)}
304
+ `, "utf8");
305
+ };
306
+
307
+ const PM_MOUNTS = {
308
+ bun: [
309
+ {
310
+ source: "${localWorkspaceFolderBasename}-node_modules",
311
+ target: "${containerWorkspaceFolder}/node_modules",
312
+ type: "volume"
313
+ },
314
+ {
315
+ source: "${localWorkspaceFolderBasename}-bun-cache",
316
+ target: "/home/node/.bun/install/cache",
317
+ type: "volume"
318
+ }
319
+ ],
320
+ npm: [
321
+ {
322
+ source: "${localWorkspaceFolderBasename}-node_modules",
323
+ target: "${containerWorkspaceFolder}/node_modules",
324
+ type: "volume"
325
+ },
326
+ {
327
+ source: "${localWorkspaceFolderBasename}-npm-cache",
328
+ target: "/home/node/.npm",
329
+ type: "volume"
330
+ }
331
+ ],
332
+ pnpm: [
333
+ {
334
+ source: "${localWorkspaceFolderBasename}-node_modules",
335
+ target: "${containerWorkspaceFolder}/node_modules",
336
+ type: "volume"
337
+ },
338
+ {
339
+ source: "${localWorkspaceFolderBasename}-pnpm-store",
340
+ target: "/home/node/.local/share/pnpm/store",
341
+ type: "volume"
342
+ }
343
+ ],
344
+ yarn: [
345
+ {
346
+ source: "${localWorkspaceFolderBasename}-node_modules",
347
+ target: "${containerWorkspaceFolder}/node_modules",
348
+ type: "volume"
349
+ },
350
+ {
351
+ source: "${localWorkspaceFolderBasename}-yarn-cache",
352
+ target: "/home/node/.yarn/cache",
353
+ type: "volume"
354
+ }
355
+ ]
356
+ };
357
+ const FEATURE_MOUNTS = [
358
+ {
359
+ featureMatch: "docker-in-docker",
360
+ mounts: []
361
+ // Docker-in-Docker manages its own storage
362
+ },
363
+ {
364
+ featureMatch: "docker-outside-of-docker",
365
+ mounts: [
366
+ {
367
+ source: "/var/run/docker.sock",
368
+ target: "/var/run/docker.sock",
369
+ type: "bind"
370
+ }
371
+ ]
372
+ },
373
+ {
374
+ featureMatch: "/features/git:",
375
+ mounts: [
376
+ {
377
+ source: "${localWorkspaceFolderBasename}-git-config",
378
+ target: "/home/node/.gitconfig",
379
+ type: "volume"
380
+ }
381
+ ]
382
+ }
383
+ ];
384
+ const getSuggestedMounts = (pm, enabledFeatures, currentMounts) => {
385
+ const suggestions = [];
386
+ const existingTargets = new Set(currentMounts.map((m) => typeof m === "string" ? m : m.target));
387
+ if (pm) {
388
+ for (const mount of PM_MOUNTS[pm]) {
389
+ if (!existingTargets.has(mount.target)) {
390
+ suggestions.push(mount);
391
+ }
392
+ }
393
+ }
394
+ const featureIds = Object.keys(enabledFeatures);
395
+ for (const { featureMatch, mounts } of FEATURE_MOUNTS) {
396
+ const hasFeature = featureIds.some((id) => id.includes(featureMatch));
397
+ if (hasFeature) {
398
+ for (const mount of mounts) {
399
+ if (!existingTargets.has(mount.target)) {
400
+ suggestions.push(mount);
401
+ }
402
+ }
403
+ }
404
+ }
405
+ return suggestions;
406
+ };
407
+
408
+ const SECTION_ORDER = ["general", "features", "ports", "lifecycle", "extensions", "environment", "mounts", "compose"];
409
+
410
+ const deepClone = (value) => structuredClone(value);
411
+ class DevcontainerStore {
412
+ #listeners = /* @__PURE__ */ new Set();
413
+ #state;
414
+ constructor(config, hadComments, detectedPm = null) {
415
+ const isCreate = config === null;
416
+ const initial = config ?? { name: "" };
417
+ const cloned = deepClone(initial);
418
+ this.#state = {
419
+ config: cloned,
420
+ detectedPm,
421
+ extensionSearch: "",
422
+ featureSearch: "",
423
+ fieldEditing: false,
424
+ fieldIndex: 0,
425
+ hadComments,
426
+ isDirty: false,
427
+ mode: isCreate ? "create" : "edit",
428
+ originalConfig: isCreate ? null : deepClone(initial),
429
+ section: "general",
430
+ showTemplateSelector: isCreate,
431
+ suggestedMounts: getSuggestedMounts(detectedPm, cloned.features ?? {}, cloned.mounts ?? []),
432
+ templateIndex: 0
433
+ };
434
+ }
435
+ // ── React integration ───────────────────────────────────────────
436
+ getSnapshot = () => this.#state;
437
+ subscribe = (listener) => {
438
+ this.#listeners.add(listener);
439
+ return () => {
440
+ this.#listeners.delete(listener);
441
+ };
442
+ };
443
+ // ── Tab navigation ──────────────────────────────────────────────
444
+ setSection(section) {
445
+ if (section !== this.#state.section) {
446
+ this.#emit({
447
+ ...this.#state,
448
+ fieldEditing: false,
449
+ fieldIndex: 0,
450
+ section
451
+ });
452
+ }
453
+ }
454
+ nextSection() {
455
+ const current = SECTION_ORDER.indexOf(this.#state.section);
456
+ const next = (current + 1) % SECTION_ORDER.length;
457
+ this.setSection(SECTION_ORDER[next]);
458
+ }
459
+ previousSection() {
460
+ const current = SECTION_ORDER.indexOf(this.#state.section);
461
+ const previous = (current - 1 + SECTION_ORDER.length) % SECTION_ORDER.length;
462
+ this.setSection(SECTION_ORDER[previous]);
463
+ }
464
+ // ── Field navigation ────────────────────────────────────────────
465
+ setFieldIndex(index) {
466
+ if (index !== this.#state.fieldIndex) {
467
+ this.#emit({ ...this.#state, fieldIndex: Math.max(0, index) });
468
+ }
469
+ }
470
+ setFieldEditing(editing) {
471
+ if (editing !== this.#state.fieldEditing) {
472
+ this.#emit({ ...this.#state, fieldEditing: editing });
473
+ }
474
+ }
475
+ // ── Template selector ───────────────────────────────────────────
476
+ setTemplateIndex(index) {
477
+ const clamped = Math.max(0, Math.min(index, TEMPLATES.length - 1));
478
+ if (clamped !== this.#state.templateIndex) {
479
+ this.#emit({ ...this.#state, templateIndex: clamped });
480
+ }
481
+ }
482
+ applyTemplate(templateId) {
483
+ const template = TEMPLATES.find((t) => t.id === templateId);
484
+ if (template) {
485
+ this.#emit(
486
+ this.#withSuggestions({
487
+ ...this.#state,
488
+ config: deepClone(template.config),
489
+ isDirty: true,
490
+ showTemplateSelector: false
491
+ })
492
+ );
493
+ }
494
+ }
495
+ dismissTemplateSelector() {
496
+ this.#emit({ ...this.#state, showTemplateSelector: false });
497
+ }
498
+ // ── General config updates ──────────────────────────────────────
499
+ updateConfig(partial) {
500
+ this.#emit({
501
+ ...this.#state,
502
+ config: { ...this.#state.config, ...partial },
503
+ isDirty: true
504
+ });
505
+ }
506
+ // ── Features ────────────────────────────────────────────────────
507
+ toggleFeature(featureId) {
508
+ const features = { ...this.#state.config.features };
509
+ if (features[featureId] === void 0) {
510
+ features[featureId] = {};
511
+ } else {
512
+ delete features[featureId];
513
+ }
514
+ this.#emit(
515
+ this.#withSuggestions({
516
+ ...this.#state,
517
+ config: { ...this.#state.config, features },
518
+ isDirty: true
519
+ })
520
+ );
521
+ }
522
+ setFeatureSearch(search) {
523
+ this.#emit({ ...this.#state, featureSearch: search, fieldIndex: 0 });
524
+ }
525
+ // ── Ports ───────────────────────────────────────────────────────
526
+ addPort(port) {
527
+ const existing = this.#state.config.forwardPorts ?? [];
528
+ if (existing.includes(port)) {
529
+ return;
530
+ }
531
+ const ports = [...existing, port];
532
+ this.#emit({
533
+ ...this.#state,
534
+ config: { ...this.#state.config, forwardPorts: ports },
535
+ isDirty: true
536
+ });
537
+ }
538
+ removePort(index) {
539
+ const ports = [...this.#state.config.forwardPorts ?? []];
540
+ ports.splice(index, 1);
541
+ this.#emit({
542
+ ...this.#state,
543
+ config: { ...this.#state.config, forwardPorts: ports.length > 0 ? ports : void 0 },
544
+ isDirty: true
545
+ });
546
+ }
547
+ // ── Extensions ──────────────────────────────────────────────────
548
+ toggleExtension(extensionId) {
549
+ const customizations = { ...this.#state.config.customizations };
550
+ const vscode = { ...customizations.vscode };
551
+ const extensions = [...vscode.extensions ?? []];
552
+ const index = extensions.indexOf(extensionId);
553
+ if (index === -1) {
554
+ extensions.push(extensionId);
555
+ } else {
556
+ extensions.splice(index, 1);
557
+ }
558
+ vscode.extensions = extensions.length > 0 ? extensions : void 0;
559
+ customizations.vscode = vscode.extensions || vscode.settings ? vscode : void 0;
560
+ this.#emit({
561
+ ...this.#state,
562
+ config: {
563
+ ...this.#state.config,
564
+ customizations: customizations.vscode || customizations.jetbrains ? customizations : void 0
565
+ },
566
+ isDirty: true
567
+ });
568
+ }
569
+ setExtensionSearch(search) {
570
+ this.#emit({ ...this.#state, extensionSearch: search, fieldIndex: 0 });
571
+ }
572
+ // ── Environment variables ───────────────────────────────────────
573
+ addEnvVar(target, key, value) {
574
+ const field = target === "container" ? "containerEnv" : "remoteEnv";
575
+ const env = { ...this.#state.config[field], [key]: value };
576
+ this.#emit({
577
+ ...this.#state,
578
+ config: { ...this.#state.config, [field]: env },
579
+ isDirty: true
580
+ });
581
+ }
582
+ removeEnvVar(target, key) {
583
+ const field = target === "container" ? "containerEnv" : "remoteEnv";
584
+ const env = { ...this.#state.config[field] };
585
+ delete env[key];
586
+ this.#emit({
587
+ ...this.#state,
588
+ config: { ...this.#state.config, [field]: Object.keys(env).length > 0 ? env : void 0 },
589
+ isDirty: true
590
+ });
591
+ }
592
+ // ── Mounts ──────────────────────────────────────────────────────
593
+ addMount(mount) {
594
+ const mounts = [...this.#state.config.mounts ?? [], mount];
595
+ this.#emit(
596
+ this.#withSuggestions({
597
+ ...this.#state,
598
+ config: { ...this.#state.config, mounts },
599
+ isDirty: true
600
+ })
601
+ );
602
+ }
603
+ removeMount(index) {
604
+ const mounts = [...this.#state.config.mounts ?? []];
605
+ mounts.splice(index, 1);
606
+ this.#emit(
607
+ this.#withSuggestions({
608
+ ...this.#state,
609
+ config: { ...this.#state.config, mounts: mounts.length > 0 ? mounts : void 0 },
610
+ isDirty: true
611
+ })
612
+ );
613
+ }
614
+ /** Add all currently suggested mounts to the config. */
615
+ applySuggestedMounts() {
616
+ if (this.#state.suggestedMounts.length === 0) {
617
+ return;
618
+ }
619
+ const mounts = [...this.#state.config.mounts ?? [], ...this.#state.suggestedMounts];
620
+ this.#emit(
621
+ this.#withSuggestions({
622
+ ...this.#state,
623
+ config: { ...this.#state.config, mounts },
624
+ isDirty: true
625
+ })
626
+ );
627
+ }
628
+ // ── Lifecycle commands ───────────────────────────────────────────
629
+ setLifecycleCommand(hook, command) {
630
+ this.#emit({
631
+ ...this.#state,
632
+ config: { ...this.#state.config, [hook]: command || void 0 },
633
+ isDirty: true
634
+ });
635
+ }
636
+ // ── Save lifecycle ────────────────────────────────────────────────
637
+ markClean() {
638
+ this.#emit({
639
+ ...this.#state,
640
+ isDirty: false,
641
+ originalConfig: deepClone(this.#state.config)
642
+ });
643
+ }
644
+ // ── Preview / Serialization ─────────────────────────────────────
645
+ getJsonPreview() {
646
+ return JSON.stringify(this.#cleanConfig(), null, 2);
647
+ }
648
+ /** Return a cleaned config with empty/undefined fields stripped. */
649
+ cleanConfig() {
650
+ return this.#cleanConfig();
651
+ }
652
+ // ── Internal ────────────────────────────────────────────────────
653
+ #cleanConfig() {
654
+ const config = deepClone(this.#state.config);
655
+ for (const [key, value] of Object.entries(config)) {
656
+ if (value === "" || value === void 0) {
657
+ delete config[key];
658
+ }
659
+ }
660
+ if (config.build) {
661
+ if (config.build.dockerfile === "") {
662
+ delete config.build.dockerfile;
663
+ }
664
+ if (config.build.context === "") {
665
+ delete config.build.context;
666
+ }
667
+ if (config.build.args && Object.keys(config.build.args).length === 0) {
668
+ delete config.build.args;
669
+ }
670
+ if (Object.keys(config.build).length === 0) {
671
+ delete config.build;
672
+ }
673
+ }
674
+ if (config.forwardPorts?.length === 0) {
675
+ delete config.forwardPorts;
676
+ }
677
+ if (config.mounts?.length === 0) {
678
+ delete config.mounts;
679
+ }
680
+ if (config.runServices?.length === 0) {
681
+ delete config.runServices;
682
+ }
683
+ if (config.capAdd?.length === 0) {
684
+ delete config.capAdd;
685
+ }
686
+ if (config.securityOpt?.length === 0) {
687
+ delete config.securityOpt;
688
+ }
689
+ if (config.features && Object.keys(config.features).length === 0) {
690
+ delete config.features;
691
+ }
692
+ if (config.customizations?.vscode?.extensions?.length === 0) {
693
+ delete config.customizations.vscode.extensions;
694
+ }
695
+ if (config.customizations?.vscode && Object.keys(config.customizations.vscode).length === 0) {
696
+ delete config.customizations.vscode;
697
+ }
698
+ if (config.customizations && Object.keys(config.customizations).length === 0) {
699
+ delete config.customizations;
700
+ }
701
+ if (config.containerEnv && Object.keys(config.containerEnv).length === 0) {
702
+ delete config.containerEnv;
703
+ }
704
+ if (config.remoteEnv && Object.keys(config.remoteEnv).length === 0) {
705
+ delete config.remoteEnv;
706
+ }
707
+ return config;
708
+ }
709
+ /** Recalculate suggested mounts for the given state. */
710
+ #withSuggestions(state) {
711
+ return {
712
+ ...state,
713
+ suggestedMounts: getSuggestedMounts(state.detectedPm, state.config.features ?? {}, state.config.mounts ?? [])
714
+ };
715
+ }
716
+ #emit(newState) {
717
+ this.#state = newState;
718
+ for (const listener of this.#listeners) {
719
+ try {
720
+ listener();
721
+ } catch {
722
+ }
723
+ }
724
+ }
725
+ }
726
+
727
+ const EXTENSION_CATALOG = [
728
+ // Linting
729
+ {
730
+ category: "linting",
731
+ description: "Integrates ESLint into the editor",
732
+ id: "dbaeumer.vscode-eslint",
733
+ name: "ESLint"
734
+ },
735
+ {
736
+ category: "linting",
737
+ description: "Stylelint CSS/SCSS linting",
738
+ id: "stylelint.vscode-stylelint",
739
+ name: "Stylelint"
740
+ },
741
+ // Formatting
742
+ {
743
+ category: "formatting",
744
+ description: "Opinionated code formatter",
745
+ id: "esbenp.prettier-vscode",
746
+ name: "Prettier"
747
+ },
748
+ {
749
+ category: "formatting",
750
+ description: "EditorConfig file support",
751
+ id: "editorconfig.editorconfig",
752
+ name: "EditorConfig"
753
+ },
754
+ {
755
+ category: "formatting",
756
+ description: "Fast Rust-based formatter and linter",
757
+ id: "biomejs.biome",
758
+ name: "Biome"
759
+ },
760
+ // Language
761
+ {
762
+ category: "language",
763
+ description: "Rich TypeScript and JavaScript support",
764
+ id: "ms-vscode.vscode-typescript-next",
765
+ name: "TypeScript Nightly"
766
+ },
767
+ {
768
+ category: "language",
769
+ description: "Tailwind CSS IntelliSense",
770
+ id: "bradlc.vscode-tailwindcss",
771
+ name: "Tailwind CSS"
772
+ },
773
+ {
774
+ category: "language",
775
+ description: "YAML language support with schemas",
776
+ id: "redhat.vscode-yaml",
777
+ name: "YAML"
778
+ },
779
+ {
780
+ category: "language",
781
+ description: "TOML language support",
782
+ id: "tamasfe.even-better-toml",
783
+ name: "TOML"
784
+ },
785
+ {
786
+ category: "language",
787
+ description: "Dockerfile and Docker Compose support",
788
+ id: "ms-azuretools.vscode-docker",
789
+ name: "Docker"
790
+ },
791
+ {
792
+ category: "language",
793
+ description: "Python language support with Pylance",
794
+ id: "ms-python.python",
795
+ name: "Python"
796
+ },
797
+ {
798
+ category: "language",
799
+ description: "Go language support",
800
+ id: "golang.go",
801
+ name: "Go"
802
+ },
803
+ {
804
+ category: "language",
805
+ description: "Rust language support via rust-analyzer",
806
+ id: "rust-lang.rust-analyzer",
807
+ name: "rust-analyzer"
808
+ },
809
+ // Git
810
+ {
811
+ category: "git",
812
+ description: "Git supercharged: blame, history, stash, etc.",
813
+ id: "eamodio.gitlens",
814
+ name: "GitLens"
815
+ },
816
+ {
817
+ category: "git",
818
+ description: "GitHub Pull Requests and Issues",
819
+ id: "github.vscode-pull-request-github",
820
+ name: "GitHub PR"
821
+ },
822
+ // Testing
823
+ {
824
+ category: "testing",
825
+ description: "Vitest test explorer integration",
826
+ id: "vitest.explorer",
827
+ name: "Vitest Explorer"
828
+ },
829
+ {
830
+ category: "testing",
831
+ description: "Jest test runner and assertions",
832
+ id: "orta.vscode-jest",
833
+ name: "Jest"
834
+ },
835
+ // Debugging
836
+ {
837
+ category: "debugging",
838
+ description: "REST client for testing APIs",
839
+ id: "humao.rest-client",
840
+ name: "REST Client"
841
+ },
842
+ {
843
+ category: "debugging",
844
+ description: "Error Lens: inline error highlighting",
845
+ id: "usernamehw.errorlens",
846
+ name: "Error Lens"
847
+ },
848
+ // Other
849
+ {
850
+ category: "other",
851
+ description: "Intelligent code completion with AI",
852
+ id: "github.copilot",
853
+ name: "GitHub Copilot"
854
+ },
855
+ {
856
+ category: "other",
857
+ description: "Path autocompletion for imports",
858
+ id: "christian-kohler.path-intellisense",
859
+ name: "Path Intellisense"
860
+ },
861
+ {
862
+ category: "other",
863
+ description: "Import cost display in editor",
864
+ id: "wix.vscode-import-cost",
865
+ name: "Import Cost"
866
+ },
867
+ {
868
+ category: "other",
869
+ description: "Todo Tree: highlight and list TODOs",
870
+ id: "gruntfuggly.todo-tree",
871
+ name: "Todo Tree"
872
+ }
873
+ ];
874
+
875
+ const FEATURE_CATALOG = [
876
+ // Languages
877
+ {
878
+ category: "language",
879
+ description: "Node.js runtime via nvm with optional pnpm/yarn",
880
+ id: "ghcr.io/devcontainers/features/node:1",
881
+ name: "Node.js"
882
+ },
883
+ {
884
+ category: "language",
885
+ description: "Python runtime with pip and optional tools",
886
+ id: "ghcr.io/devcontainers/features/python:1",
887
+ name: "Python"
888
+ },
889
+ {
890
+ category: "language",
891
+ description: "Go compiler and tools",
892
+ id: "ghcr.io/devcontainers/features/go:1",
893
+ name: "Go"
894
+ },
895
+ {
896
+ category: "language",
897
+ description: "Rust toolchain via rustup",
898
+ id: "ghcr.io/devcontainers/features/rust:1",
899
+ name: "Rust"
900
+ },
901
+ {
902
+ category: "language",
903
+ description: "Java runtime and JDK via SDKMAN",
904
+ id: "ghcr.io/devcontainers/features/java:1",
905
+ name: "Java"
906
+ },
907
+ {
908
+ category: "language",
909
+ description: ".NET SDK and runtime",
910
+ id: "ghcr.io/devcontainers/features/dotnet:2",
911
+ name: ".NET"
912
+ },
913
+ // Tools
914
+ {
915
+ category: "tool",
916
+ description: "Common utilities: zsh, Oh My Zsh, git, curl, etc.",
917
+ id: "ghcr.io/devcontainers/features/common-utils:2",
918
+ name: "Common Utilities"
919
+ },
920
+ {
921
+ category: "tool",
922
+ description: "Git version control",
923
+ id: "ghcr.io/devcontainers/features/git:1",
924
+ name: "Git"
925
+ },
926
+ {
927
+ category: "tool",
928
+ description: "Git Large File Storage support",
929
+ id: "ghcr.io/devcontainers/features/git-lfs:1",
930
+ name: "Git LFS"
931
+ },
932
+ {
933
+ category: "tool",
934
+ description: "GitHub CLI for repository management",
935
+ id: "ghcr.io/devcontainers/features/github-cli:1",
936
+ name: "GitHub CLI"
937
+ },
938
+ {
939
+ category: "tool",
940
+ description: "Run Docker containers inside the dev container",
941
+ id: "ghcr.io/devcontainers/features/docker-in-docker:2",
942
+ name: "Docker-in-Docker"
943
+ },
944
+ {
945
+ category: "tool",
946
+ description: "Access host Docker daemon from inside the container",
947
+ id: "ghcr.io/devcontainers/features/docker-outside-of-docker:1",
948
+ name: "Docker-from-Docker"
949
+ },
950
+ {
951
+ category: "tool",
952
+ description: "kubectl, Helm, and Minikube for Kubernetes",
953
+ id: "ghcr.io/devcontainers/features/kubectl-helm-minikube:1",
954
+ name: "Kubernetes Tools"
955
+ },
956
+ {
957
+ category: "tool",
958
+ description: "Infrastructure as code with Terraform",
959
+ id: "ghcr.io/devcontainers/features/terraform:1",
960
+ name: "Terraform"
961
+ },
962
+ {
963
+ category: "tool",
964
+ description: "Nix package manager",
965
+ id: "ghcr.io/devcontainers/features/nix:1",
966
+ name: "Nix"
967
+ },
968
+ {
969
+ category: "tool",
970
+ description: "SSH server for remote connections to the container",
971
+ id: "ghcr.io/devcontainers/features/sshd:1",
972
+ name: "SSH Server"
973
+ },
974
+ // Cloud
975
+ {
976
+ category: "cloud",
977
+ description: "Amazon Web Services CLI v2",
978
+ id: "ghcr.io/devcontainers/features/aws-cli:1",
979
+ name: "AWS CLI"
980
+ },
981
+ {
982
+ category: "cloud",
983
+ description: "Microsoft Azure CLI",
984
+ id: "ghcr.io/devcontainers/features/azure-cli:1",
985
+ name: "Azure CLI"
986
+ },
987
+ {
988
+ category: "cloud",
989
+ description: "Google Cloud Platform CLI",
990
+ id: "ghcr.io/devcontainers/features/gcloud:1",
991
+ name: "Google Cloud CLI"
992
+ },
993
+ // Databases
994
+ {
995
+ category: "database",
996
+ description: "PostgreSQL client tools",
997
+ id: "ghcr.io/devcontainers-extra/features/postgres-client:1",
998
+ name: "PostgreSQL Client"
999
+ },
1000
+ {
1001
+ category: "database",
1002
+ description: "Redis client tools",
1003
+ id: "ghcr.io/devcontainers-extra/features/redis-client:1",
1004
+ name: "Redis Client"
1005
+ }
1006
+ ];
1007
+
1008
+ const filterFeatures = (searchText) => {
1009
+ if (!searchText) {
1010
+ return FEATURE_CATALOG;
1011
+ }
1012
+ const lower = searchText.toLowerCase();
1013
+ return FEATURE_CATALOG.filter(
1014
+ (f) => f.name.toLowerCase().includes(lower) || f.id.toLowerCase().includes(lower) || f.description.toLowerCase().includes(lower)
1015
+ );
1016
+ };
1017
+ const filterExtensions = (searchText) => {
1018
+ if (!searchText) {
1019
+ return EXTENSION_CATALOG;
1020
+ }
1021
+ const lower = searchText.toLowerCase();
1022
+ return EXTENSION_CATALOG.filter(
1023
+ (e) => e.name.toLowerCase().includes(lower) || e.id.toLowerCase().includes(lower) || e.description.toLowerCase().includes(lower)
1024
+ );
1025
+ };
1026
+
1027
+ const FIELDS$1 = ["dockerComposeFile", "service"];
1028
+ const FIELD_LABELS$1 = {
1029
+ dockerComposeFile: "Compose File",
1030
+ service: "Service"
1031
+ };
1032
+ const FIELD_PLACEHOLDERS$1 = {
1033
+ dockerComposeFile: "docker-compose.yml",
1034
+ service: "app"
1035
+ };
1036
+ const FIELD_DESCRIPTIONS = {
1037
+ dockerComposeFile: "Path to Docker Compose file (relative to .devcontainer/)",
1038
+ service: "Which service in the compose file to connect the IDE to"
1039
+ };
1040
+ const DockerComposeSection = ({ config, fieldEditing, fieldIndex, onUpdate }) => {
1041
+ const hasCompose = Boolean(config.dockerComposeFile);
1042
+ const hasImage = Boolean(config.image || config.build);
1043
+ return jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
1044
+ jsx(Box, { marginBottom: 1, children: jsx(Text, { bold: true, color: "cyan", children: "Docker Compose Integration" }) }),
1045
+ hasImage && hasCompose && jsx(Box, { marginBottom: 1, children: jsx(Text, { color: "yellow", children: "Note: When using Docker Compose, the image/build settings in General are ignored." }) }),
1046
+ FIELDS$1.map((field, index) => {
1047
+ const isSelected = index === fieldIndex;
1048
+ const value = config[field] ?? "";
1049
+ const displayValue = Array.isArray(config[field]) ? config[field].join(", ") : value;
1050
+ return jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
1051
+ jsxs(Box, { children: [
1052
+ jsx(Box, { width: 20, children: jsxs(Text, { bold: isSelected, color: isSelected ? "cyan" : "white", children: [
1053
+ isSelected ? "❯ " : " ",
1054
+ FIELD_LABELS$1[field],
1055
+ ":"
1056
+ ] }) }),
1057
+ jsx(Box, { flexGrow: 1, children: isSelected && fieldEditing ? jsx(
1058
+ TextInput,
1059
+ {
1060
+ defaultValue: displayValue,
1061
+ onChange: (newValue) => {
1062
+ onUpdate({ [field]: newValue || void 0 });
1063
+ },
1064
+ placeholder: FIELD_PLACEHOLDERS$1[field]
1065
+ }
1066
+ ) : jsx(Text, { color: displayValue ? "white" : "gray", children: displayValue || FIELD_PLACEHOLDERS$1[field] }) })
1067
+ ] }),
1068
+ jsx(Box, { paddingLeft: 4, children: jsx(Text, { dimColor: true, children: FIELD_DESCRIPTIONS[field] }) })
1069
+ ] }, field);
1070
+ }),
1071
+ jsx(Box, { marginTop: 1, children: jsxs(Text, { dimColor: true, children: [
1072
+ jsx(Text, { bold: true, color: "white", children: "Enter" }),
1073
+ " ",
1074
+ "edit field",
1075
+ " ",
1076
+ jsx(Text, { bold: true, color: "white", children: "↑↓" }),
1077
+ " ",
1078
+ "navigate",
1079
+ " ",
1080
+ jsx(Text, { bold: true, color: "white", children: "Esc" }),
1081
+ " ",
1082
+ "stop editing"
1083
+ ] }) })
1084
+ ] });
1085
+ };
1086
+ const COMPOSE_FIELD_COUNT = FIELDS$1.length;
1087
+
1088
+ const EnvironmentSection = ({ config, fieldIndex }) => {
1089
+ const containerEnv = config.containerEnv ?? {};
1090
+ const remoteEnv = config.remoteEnv ?? {};
1091
+ const containerKeys = Object.keys(containerEnv);
1092
+ const remoteKeys = Object.keys(remoteEnv);
1093
+ const containerAddIndex = containerKeys.length;
1094
+ const remoteStart = containerKeys.length + 1;
1095
+ const remoteAddIndex = remoteStart + remoteKeys.length;
1096
+ const isInContainerSection = fieldIndex <= containerAddIndex;
1097
+ const isInRemoteSection = fieldIndex > containerAddIndex;
1098
+ return jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
1099
+ jsxs(Box, { borderColor: isInContainerSection ? "cyan" : "gray", borderStyle: "single", flexDirection: "column", paddingX: 1, paddingY: 0, children: [
1100
+ jsxs(Box, { flexShrink: 0, marginBottom: containerKeys.length > 0 ? 1 : 0, children: [
1101
+ jsx(Text, { bold: true, color: isInContainerSection ? "cyan" : "white", children: "containerEnv" }),
1102
+ jsx(Text, { dimColor: true, children: " — baked into the container image" })
1103
+ ] }),
1104
+ containerKeys.map((key, index) => {
1105
+ const isSelected = index === fieldIndex;
1106
+ return jsx(Box, { flexShrink: 0, children: jsxs(Text, { color: isSelected ? "cyan" : void 0, inverse: isSelected, wrap: "truncate", children: [
1107
+ isSelected ? " ❯ " : " ",
1108
+ jsx(Text, { bold: true, children: key }),
1109
+ jsx(Text, { dimColor: true, children: " = " }),
1110
+ jsx(Text, { children: containerEnv[key] })
1111
+ ] }) }, key);
1112
+ }),
1113
+ jsx(Box, { flexShrink: 0, marginTop: containerKeys.length > 0 ? 1 : 0, children: jsxs(Text, { color: fieldIndex === containerAddIndex ? "cyan" : "gray", inverse: fieldIndex === containerAddIndex, children: [
1114
+ " ",
1115
+ "+ Add variable..."
1116
+ ] }) })
1117
+ ] }),
1118
+ jsxs(Box, { borderColor: isInRemoteSection ? "cyan" : "gray", borderStyle: "single", flexDirection: "column", marginTop: 1, paddingX: 1, paddingY: 0, children: [
1119
+ jsxs(Box, { flexShrink: 0, marginBottom: remoteKeys.length > 0 ? 1 : 0, children: [
1120
+ jsx(Text, { bold: true, color: isInRemoteSection ? "cyan" : "white", children: "remoteEnv" }),
1121
+ jsx(Text, { dimColor: true, children: " — set at runtime by the IDE" })
1122
+ ] }),
1123
+ remoteKeys.map((key, remoteIdx) => {
1124
+ const actualIndex = remoteStart + remoteIdx;
1125
+ const isSelected = actualIndex === fieldIndex;
1126
+ return jsx(Box, { flexShrink: 0, children: jsxs(Text, { color: isSelected ? "cyan" : void 0, inverse: isSelected, wrap: "truncate", children: [
1127
+ isSelected ? " ❯ " : " ",
1128
+ jsx(Text, { bold: true, children: key }),
1129
+ jsx(Text, { dimColor: true, children: " = " }),
1130
+ jsx(Text, { children: remoteEnv[key] })
1131
+ ] }) }, key);
1132
+ }),
1133
+ jsx(Box, { flexShrink: 0, marginTop: remoteKeys.length > 0 ? 1 : 0, children: jsxs(Text, { color: fieldIndex === remoteAddIndex ? "cyan" : "gray", inverse: fieldIndex === remoteAddIndex, children: [
1134
+ " ",
1135
+ "+ Add variable..."
1136
+ ] }) })
1137
+ ] }),
1138
+ jsx(Box, { flexShrink: 0, marginTop: 1, children: jsxs(Text, { dimColor: true, wrap: "truncate", children: [
1139
+ jsx(Text, { bold: true, color: "white", children: "a" }),
1140
+ "/",
1141
+ jsx(Text, { bold: true, color: "white", children: "Enter" }),
1142
+ " ",
1143
+ "add on + row",
1144
+ " ",
1145
+ jsx(Text, { bold: true, color: "white", children: "d" }),
1146
+ " ",
1147
+ "remove",
1148
+ " ",
1149
+ jsx(Text, { bold: true, color: "white", children: "↑↓" }),
1150
+ " ",
1151
+ "navigate"
1152
+ ] }) })
1153
+ ] });
1154
+ };
1155
+ const getEnvFieldCount = (config) => {
1156
+ const containerCount = Object.keys(config.containerEnv ?? {}).length;
1157
+ const remoteCount = Object.keys(config.remoteEnv ?? {}).length;
1158
+ return containerCount + 1 + remoteCount + 1;
1159
+ };
1160
+
1161
+ const ExtensionsSection = ({ config, fieldIndex, scrollOffset, searchText, viewportHeight }) => {
1162
+ const enabledExtensions = useMemo(() => new Set(config.customizations?.vscode?.extensions), [config.customizations?.vscode?.extensions]);
1163
+ const filtered = useMemo(() => filterExtensions(searchText), [searchText]);
1164
+ const contentHeight = filtered.length;
1165
+ const showScrollbar = contentHeight > viewportHeight && viewportHeight > 0;
1166
+ return jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [
1167
+ jsxs(Box, { flexShrink: 0, gap: 1, paddingX: 1, children: [
1168
+ jsxs(Text, { bold: true, color: "cyan", children: [
1169
+ enabledExtensions.size,
1170
+ " selected"
1171
+ ] }),
1172
+ searchText && jsxs(Text, { dimColor: true, children: [
1173
+ "— filter: ",
1174
+ jsx(Text, { color: "yellow", children: searchText }),
1175
+ " (",
1176
+ filtered.length,
1177
+ " results)"
1178
+ ] })
1179
+ ] }),
1180
+ jsxs(Box, { flexDirection: "row", flexGrow: 1, overflow: "hidden", children: [
1181
+ jsx(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", paddingLeft: 1, children: jsx(Box, { flexDirection: "column", marginTop: -scrollOffset, children: filtered.map((ext, index) => {
1182
+ const isSelected = index === fieldIndex;
1183
+ const isEnabled = enabledExtensions.has(ext.id);
1184
+ return jsxs(Box, { flexShrink: 0, height: 1, children: [
1185
+ jsx(Text, { children: isSelected ? ">" : " " }),
1186
+ jsxs(Text, { color: isEnabled ? "white" : "gray", children: [
1187
+ " ",
1188
+ isEnabled ? "☑" : "☐",
1189
+ " "
1190
+ ] }),
1191
+ jsx(Box, { flexGrow: 1, children: jsxs(Text, { bold: isSelected, inverse: isSelected, wrap: "truncate", children: [
1192
+ ext.name,
1193
+ jsxs(Text, { dimColor: true, children: [
1194
+ " -",
1195
+ ext.id
1196
+ ] })
1197
+ ] }) })
1198
+ ] }, ext.id);
1199
+ }) }) }),
1200
+ showScrollbar && jsx(Box, { flexShrink: 0, marginLeft: 1, marginRight: 1, children: jsx(ScrollBar, { contentHeight, placement: "inset", scrollOffset, style: "block", viewportHeight }) })
1201
+ ] }),
1202
+ filtered.length === 0 && jsx(Box, { paddingX: 1, children: jsx(Text, { dimColor: true, children: "No extensions match the search." }) })
1203
+ ] });
1204
+ };
1205
+
1206
+ const FeaturesSection = ({ config, fieldIndex, scrollOffset, searchText, viewportHeight }) => {
1207
+ const enabledFeatures = useMemo(() => new Set(Object.keys(config.features ?? {})), [config.features]);
1208
+ const filtered = useMemo(() => filterFeatures(searchText), [searchText]);
1209
+ const contentHeight = filtered.length;
1210
+ const showScrollbar = contentHeight > viewportHeight && viewportHeight > 0;
1211
+ return jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [
1212
+ jsxs(Box, { flexShrink: 0, gap: 1, paddingX: 1, children: [
1213
+ jsxs(Text, { bold: true, color: "cyan", children: [
1214
+ enabledFeatures.size,
1215
+ " selected"
1216
+ ] }),
1217
+ searchText && jsxs(Text, { dimColor: true, children: [
1218
+ "— filter: ",
1219
+ jsx(Text, { color: "yellow", children: searchText }),
1220
+ " (",
1221
+ filtered.length,
1222
+ " results)"
1223
+ ] })
1224
+ ] }),
1225
+ jsxs(Box, { flexDirection: "row", flexGrow: 1, overflow: "hidden", children: [
1226
+ jsx(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", paddingLeft: 1, children: jsx(Box, { flexDirection: "column", marginTop: -scrollOffset, children: filtered.map((feature, index) => {
1227
+ const isSelected = index === fieldIndex;
1228
+ const isEnabled = enabledFeatures.has(feature.id);
1229
+ return jsxs(Box, { flexShrink: 0, height: 1, children: [
1230
+ jsx(Text, { children: isSelected ? ">" : " " }),
1231
+ jsxs(Text, { color: isEnabled ? "white" : "gray", children: [
1232
+ " ",
1233
+ isEnabled ? "☑" : "☐",
1234
+ " "
1235
+ ] }),
1236
+ jsx(Box, { flexGrow: 1, children: jsxs(Text, { bold: isSelected, inverse: isSelected, wrap: "truncate", children: [
1237
+ feature.name,
1238
+ jsxs(Text, { dimColor: true, children: [
1239
+ " -",
1240
+ feature.description
1241
+ ] })
1242
+ ] }) })
1243
+ ] }, feature.id);
1244
+ }) }) }),
1245
+ showScrollbar && jsx(Box, { flexShrink: 0, marginLeft: 1, marginRight: 1, children: jsx(ScrollBar, { contentHeight, placement: "inset", scrollOffset, style: "block", viewportHeight }) })
1246
+ ] }),
1247
+ filtered.length === 0 && jsx(Box, { paddingX: 1, children: jsx(Text, { dimColor: true, children: "No features match the search." }) })
1248
+ ] });
1249
+ };
1250
+
1251
+ const FIELDS = ["name", "image", "workspaceFolder", "workspaceMount", "remoteUser", "containerUser", "shutdownAction"];
1252
+ const FIELD_LABELS = {
1253
+ containerUser: "Container User",
1254
+ image: "Image",
1255
+ name: "Name",
1256
+ remoteUser: "Remote User",
1257
+ shutdownAction: "Shutdown Action",
1258
+ workspaceFolder: "Workspace Folder",
1259
+ workspaceMount: "Workspace Mount"
1260
+ };
1261
+ const FIELD_PLACEHOLDERS = {
1262
+ containerUser: "root",
1263
+ image: "mcr.microsoft.com/devcontainers/javascript-node:22",
1264
+ name: "My Dev Container",
1265
+ remoteUser: "node",
1266
+ shutdownAction: "none | stopContainer",
1267
+ workspaceFolder: "/workspaces/${localWorkspaceFolderBasename}",
1268
+ workspaceMount: "source=${localWorkspaceFolder},target=...,type=bind"
1269
+ };
1270
+ const BOOLEAN_FIELDS = ["privileged", "overrideCommand"];
1271
+ const BOOLEAN_LABELS = {
1272
+ overrideCommand: "Override Command",
1273
+ privileged: "Privileged"
1274
+ };
1275
+ const ALL_FIELD_COUNT = FIELDS.length + BOOLEAN_FIELDS.length;
1276
+ const GeneralSection = ({ config, fieldEditing, fieldIndex, onUpdate }) => jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
1277
+ jsx(Box, { marginBottom: 1, children: jsx(Text, { bold: true, color: "cyan", children: "General Configuration" }) }),
1278
+ FIELDS.map((field, index) => {
1279
+ const isSelected = index === fieldIndex;
1280
+ const value = config[field] ?? "";
1281
+ return jsxs(Box, { marginBottom: 1, children: [
1282
+ jsx(Box, { width: 20, children: jsxs(Text, { bold: isSelected, color: isSelected ? "cyan" : "white", children: [
1283
+ isSelected ? "❯ " : " ",
1284
+ FIELD_LABELS[field],
1285
+ ":"
1286
+ ] }) }),
1287
+ jsx(Box, { flexGrow: 1, children: isSelected && fieldEditing ? jsx(
1288
+ TextInput,
1289
+ {
1290
+ defaultValue: value,
1291
+ onChange: (newValue) => {
1292
+ onUpdate({ [field]: newValue });
1293
+ },
1294
+ placeholder: FIELD_PLACEHOLDERS[field]
1295
+ }
1296
+ ) : jsx(Text, { color: value ? "white" : "gray", children: value || FIELD_PLACEHOLDERS[field] }) })
1297
+ ] }, field);
1298
+ }),
1299
+ BOOLEAN_FIELDS.map((field, boolIndex) => {
1300
+ const absoluteIndex = FIELDS.length + boolIndex;
1301
+ const isSelected = absoluteIndex === fieldIndex;
1302
+ const value = config[field] ?? false;
1303
+ return jsxs(Box, { marginBottom: boolIndex < BOOLEAN_FIELDS.length - 1 ? 1 : 0, children: [
1304
+ jsx(Box, { width: 20, children: jsxs(Text, { bold: isSelected, color: isSelected ? "cyan" : "white", children: [
1305
+ isSelected ? "❯ " : " ",
1306
+ BOOLEAN_LABELS[field],
1307
+ ":"
1308
+ ] }) }),
1309
+ jsx(Box, { flexGrow: 1, children: jsxs(Text, { color: value ? "green" : "gray", children: [
1310
+ value ? "yes" : "no",
1311
+ isSelected && jsx(Text, { dimColor: true, children: " (Space to toggle)" })
1312
+ ] }) })
1313
+ ] }, field);
1314
+ }),
1315
+ jsx(Box, { marginTop: 1, children: jsxs(Text, { dimColor: true, children: [
1316
+ jsx(Text, { bold: true, color: "white", children: "Enter" }),
1317
+ " ",
1318
+ "edit field",
1319
+ " ",
1320
+ jsx(Text, { bold: true, color: "white", children: "Space" }),
1321
+ " ",
1322
+ "toggle",
1323
+ " ",
1324
+ jsx(Text, { bold: true, color: "white", children: "↑↓" }),
1325
+ " ",
1326
+ "navigate",
1327
+ " ",
1328
+ jsx(Text, { bold: true, color: "white", children: "Esc" }),
1329
+ " ",
1330
+ "stop editing"
1331
+ ] }) })
1332
+ ] });
1333
+ const GENERAL_FIELD_COUNT = ALL_FIELD_COUNT;
1334
+ const GENERAL_BOOLEAN_FIELDS = BOOLEAN_FIELDS;
1335
+
1336
+ const HOOKS = ["postCreateCommand", "postStartCommand", "postAttachCommand", "onCreateCommand"];
1337
+ const HOOK_LABELS = {
1338
+ onCreateCommand: "On Create",
1339
+ postAttachCommand: "Post Attach",
1340
+ postCreateCommand: "Post Create",
1341
+ postStartCommand: "Post Start"
1342
+ };
1343
+ const HOOK_DESCRIPTIONS = {
1344
+ onCreateCommand: "Runs once when the container is first created",
1345
+ postAttachCommand: "Runs each time the IDE attaches",
1346
+ postCreateCommand: "Runs after the container is created and workspace mounted",
1347
+ postStartCommand: "Runs each time the container starts"
1348
+ };
1349
+ const LifecycleSection = ({ config, fieldEditing, fieldIndex, onSetCommand }) => jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
1350
+ jsx(Box, { marginBottom: 1, children: jsx(Text, { bold: true, color: "cyan", children: "Lifecycle Commands" }) }),
1351
+ HOOKS.map((hook, index) => {
1352
+ const isSelected = index === fieldIndex;
1353
+ const value = config[hook];
1354
+ const displayValue = Array.isArray(value) ? value.join(" && ") : value ?? "";
1355
+ return jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
1356
+ jsx(Box, { children: jsxs(Text, { bold: isSelected, color: isSelected ? "cyan" : "white", children: [
1357
+ isSelected ? "❯ " : " ",
1358
+ HOOK_LABELS[hook]
1359
+ ] }) }),
1360
+ jsx(Box, { paddingLeft: 4, children: jsx(Text, { dimColor: true, children: HOOK_DESCRIPTIONS[hook] }) }),
1361
+ jsx(Box, { paddingLeft: 4, children: isSelected && fieldEditing ? jsx(
1362
+ TextInput,
1363
+ {
1364
+ defaultValue: displayValue,
1365
+ onChange: (newValue) => {
1366
+ onSetCommand(hook, newValue);
1367
+ },
1368
+ placeholder: "e.g. npm install"
1369
+ }
1370
+ ) : jsx(Text, { color: displayValue ? "green" : "gray", children: displayValue || "(not set)" }) })
1371
+ ] }, hook);
1372
+ }),
1373
+ jsx(Box, { marginTop: 1, children: jsxs(Text, { dimColor: true, children: [
1374
+ jsx(Text, { bold: true, color: "white", children: "Enter" }),
1375
+ " ",
1376
+ "edit command",
1377
+ " ",
1378
+ jsx(Text, { bold: true, color: "white", children: "↑↓" }),
1379
+ " ",
1380
+ "navigate",
1381
+ " ",
1382
+ jsx(Text, { bold: true, color: "white", children: "Esc" }),
1383
+ " ",
1384
+ "stop editing"
1385
+ ] }) })
1386
+ ] });
1387
+ const LIFECYCLE_FIELD_COUNT = HOOKS.length;
1388
+
1389
+ const formatMount = (mount) => {
1390
+ if (typeof mount === "string") {
1391
+ return mount;
1392
+ }
1393
+ return `[${mount.type}] ${mount.source} → ${mount.target}`;
1394
+ };
1395
+ const MountsSection = ({
1396
+ addingMount,
1397
+ config,
1398
+ detectedPm,
1399
+ fieldIndex,
1400
+ mountPhase,
1401
+ mountSource,
1402
+ mountTarget,
1403
+ mountType,
1404
+ suggestedMounts
1405
+ }) => {
1406
+ const mounts = config.mounts ?? [];
1407
+ return jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
1408
+ jsxs(Box, { flexShrink: 0, gap: 1, paddingX: 1, children: [
1409
+ jsxs(Text, { bold: true, color: "cyan", children: [
1410
+ mounts.length,
1411
+ " mounts"
1412
+ ] }),
1413
+ detectedPm && jsxs(Text, { dimColor: true, children: [
1414
+ "— detected: ",
1415
+ jsx(Text, { color: "white", children: detectedPm })
1416
+ ] })
1417
+ ] }),
1418
+ suggestedMounts.length > 0 && !addingMount && jsxs(Box, { borderColor: "yellow", borderStyle: "single", flexDirection: "column", marginBottom: 1, marginTop: 1, paddingX: 1, children: [
1419
+ jsxs(Box, { flexShrink: 0, children: [
1420
+ jsx(Text, { bold: true, color: "yellow", children: "Suggested mounts" }),
1421
+ jsxs(Text, { dimColor: true, children: [
1422
+ " ",
1423
+ "— press",
1424
+ " ",
1425
+ jsx(Text, { bold: true, color: "white", children: "A" }),
1426
+ " ",
1427
+ "to add all"
1428
+ ] })
1429
+ ] }),
1430
+ suggestedMounts.map((mount, index) => jsx(Box, { flexShrink: 0, children: jsxs(Text, { dimColor: true, wrap: "truncate", children: [
1431
+ " + ",
1432
+ formatMount(mount)
1433
+ ] }) }, `suggestion-${String(index)}`))
1434
+ ] }),
1435
+ mounts.length > 0 && jsx(Box, { flexDirection: "column", marginBottom: 1, children: mounts.map((mount, index) => {
1436
+ const isSelected = index === fieldIndex;
1437
+ return jsxs(Box, { flexShrink: 0, height: 1, children: [
1438
+ jsx(Text, { children: isSelected ? ">" : " " }),
1439
+ jsx(Box, { flexGrow: 1, children: jsxs(Text, { bold: isSelected, inverse: isSelected, wrap: "truncate", children: [
1440
+ " ",
1441
+ formatMount(mount)
1442
+ ] }) })
1443
+ ] }, `mount-${String(index)}`);
1444
+ }) }),
1445
+ !addingMount && jsx(Box, { flexShrink: 0, children: jsxs(Text, { color: fieldIndex === mounts.length ? "cyan" : "gray", inverse: fieldIndex === mounts.length, children: [
1446
+ " ",
1447
+ "+ Add mount..."
1448
+ ] }) }),
1449
+ addingMount && jsxs(Box, { borderColor: "cyan", borderStyle: "single", flexDirection: "column", marginTop: 1, paddingX: 1, children: [
1450
+ jsx(Box, { flexShrink: 0, marginBottom: 1, children: jsx(Text, { bold: true, color: "cyan", children: "New Mount" }) }),
1451
+ jsxs(Box, { flexShrink: 0, children: [
1452
+ jsx(Box, { width: 12, children: jsxs(Text, { bold: mountPhase === "source", color: mountPhase === "source" ? "cyan" : "white", children: [
1453
+ mountPhase === "source" ? "❯ " : " ",
1454
+ "Source:"
1455
+ ] }) }),
1456
+ jsx(Text, { color: mountSource ? "yellow" : "gray", children: mountSource || (mountPhase === "source" ? "_" : "(type source, Enter to continue)") })
1457
+ ] }),
1458
+ jsxs(Box, { flexShrink: 0, children: [
1459
+ jsx(Box, { width: 12, children: jsxs(Text, { bold: mountPhase === "target", color: mountPhase === "target" ? "cyan" : "white", children: [
1460
+ mountPhase === "target" ? "❯ " : " ",
1461
+ "Target:"
1462
+ ] }) }),
1463
+ jsx(Text, { color: mountTarget ? "yellow" : "gray", children: mountTarget || (mountPhase === "target" ? "_" : "/container/path") })
1464
+ ] }),
1465
+ jsxs(Box, { flexShrink: 0, children: [
1466
+ jsx(Box, { width: 12, children: jsxs(Text, { bold: mountPhase === "type", color: mountPhase === "type" ? "cyan" : "white", children: [
1467
+ mountPhase === "type" ? "❯ " : " ",
1468
+ "Type:"
1469
+ ] }) }),
1470
+ mountPhase === "type" ? jsxs(Text, { children: [
1471
+ jsx(Text, { bold: mountType === "volume", color: mountType === "volume" ? "cyan" : "gray", children: "[1] volume" }),
1472
+ " ",
1473
+ jsx(Text, { bold: mountType === "bind", color: mountType === "bind" ? "cyan" : "gray", children: "[2] bind" }),
1474
+ " ",
1475
+ jsx(Text, { bold: mountType === "tmpfs", color: mountType === "tmpfs" ? "cyan" : "gray", children: "[3] tmpfs" })
1476
+ ] }) : jsx(Text, { color: "gray", children: mountType })
1477
+ ] }),
1478
+ jsx(Box, { flexShrink: 0, marginTop: 1, children: jsx(Text, { dimColor: true, wrap: "truncate", children: mountPhase === "type" ? "1/2/3 select type, Enter confirm, Esc cancel" : "Type text, Enter next step, Esc cancel" }) })
1479
+ ] }),
1480
+ mounts.length === 0 && !addingMount && suggestedMounts.length === 0 && jsx(Box, { marginTop: 1, children: jsx(Text, { dimColor: true, children: "Tip: Use volume mounts for node_modules and caches to improve performance." }) })
1481
+ ] });
1482
+ };
1483
+
1484
+ const PortsSection = ({ addingPort, addPortValue, config, fieldIndex }) => {
1485
+ const ports = config.forwardPorts ?? [];
1486
+ const isAddRowSelected = fieldIndex === ports.length;
1487
+ return jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
1488
+ jsxs(Box, { marginBottom: 1, children: [
1489
+ jsx(Text, { bold: true, color: "cyan", children: "Forwarded Ports" }),
1490
+ jsxs(Text, { dimColor: true, children: [
1491
+ " (",
1492
+ ports.length,
1493
+ " ports)"
1494
+ ] })
1495
+ ] }),
1496
+ ports.map((port, index) => {
1497
+ const isSelected = index === fieldIndex;
1498
+ return jsx(Box, { children: jsxs(Text, { color: isSelected ? "cyan" : void 0, inverse: isSelected, children: [
1499
+ " ",
1500
+ String(port)
1501
+ ] }) }, `port-${String(port)}`);
1502
+ }),
1503
+ jsx(Box, { marginTop: ports.length > 0 ? 1 : 0, children: jsxs(Text, { color: isAddRowSelected ? "cyan" : "gray", inverse: isAddRowSelected, children: [
1504
+ " ",
1505
+ isAddRowSelected && addingPort ? jsxs(Text, { children: [
1506
+ "Enter port: ",
1507
+ jsx(Text, { color: "yellow", children: addPortValue || "_" })
1508
+ ] }) : "+ Add port..."
1509
+ ] }) }),
1510
+ jsx(Box, { marginTop: 1, children: jsxs(Text, { dimColor: true, children: [
1511
+ jsx(Text, { bold: true, color: "white", children: "Enter" }),
1512
+ " ",
1513
+ isAddRowSelected ? "type port number, Enter to confirm" : "select",
1514
+ " ",
1515
+ jsx(Text, { bold: true, color: "white", children: "d" }),
1516
+ " ",
1517
+ "remove selected",
1518
+ " ",
1519
+ jsx(Text, { bold: true, color: "white", children: "↑↓" }),
1520
+ " ",
1521
+ "navigate"
1522
+ ] }) })
1523
+ ] });
1524
+ };
1525
+
1526
+ const PreviewPanel = ({ focused, hadComments, jsonPreview, mode, scrollRef }) => jsxs(Box, { borderColor: focused ? "cyan" : "gray", borderStyle: "single", flexDirection: "column", flexGrow: 1, children: [
1527
+ jsxs(Box, { flexShrink: 0, paddingX: 1, children: [
1528
+ jsx(Text, { bold: true, color: focused ? "cyan" : "white", children: "Preview" }),
1529
+ jsxs(Text, { dimColor: true, children: [
1530
+ " (",
1531
+ mode === "create" ? "new" : "edit",
1532
+ ")"
1533
+ ] })
1534
+ ] }),
1535
+ hadComments && mode === "edit" && jsx(Box, { flexShrink: 0, paddingX: 1, children: jsx(Text, { color: "yellow", children: "Comments will not be preserved." }) }),
1536
+ jsx(ScrollView, { flexGrow: 1, ref: scrollRef, scrollbar: true, scrollbarColor: "gray", children: jsonPreview.split("\n").map((line, index) => jsx(Text, { color: "green", children: line }, `line-${String(index)}`)) })
1537
+ ] });
1538
+
1539
+ const validateConfig = (config) => {
1540
+ const errors = [];
1541
+ const warnings = [];
1542
+ const suggestions = [];
1543
+ if (!config.image && !config.build && !config.dockerComposeFile) {
1544
+ errors.push({ field: "image", message: 'One of "image", "build", or "dockerComposeFile" is required' });
1545
+ }
1546
+ if (config.build) {
1547
+ if (config.image) {
1548
+ warnings.push({ field: "image", message: 'Both "image" and "build" are set; "build" takes precedence' });
1549
+ }
1550
+ if (!config.build.dockerfile) {
1551
+ errors.push({ field: "build.dockerfile", message: '"build" requires a "dockerfile" path' });
1552
+ }
1553
+ }
1554
+ if (config.dockerComposeFile && !config.service) {
1555
+ errors.push({ field: "service", message: '"service" is required when using "dockerComposeFile"' });
1556
+ }
1557
+ if (config.features !== void 0 && (typeof config.features !== "object" || Array.isArray(config.features))) {
1558
+ errors.push({ field: "features", message: '"features" must be an object mapping feature IDs to options' });
1559
+ }
1560
+ if (config.forwardPorts) {
1561
+ if (Array.isArray(config.forwardPorts)) {
1562
+ for (const [index, port] of config.forwardPorts.entries()) {
1563
+ if (typeof port === "number" && (port < 1 || port > 65535)) {
1564
+ errors.push({ field: "forwardPorts", message: `Invalid port ${String(port)} at index ${String(index)}` });
1565
+ }
1566
+ }
1567
+ } else {
1568
+ errors.push({ field: "forwardPorts", message: '"forwardPorts" must be an array' });
1569
+ }
1570
+ }
1571
+ if (config.customizations?.vscode?.extensions && !Array.isArray(config.customizations.vscode.extensions)) {
1572
+ errors.push({ field: "customizations.vscode.extensions", message: "Extensions must be an array" });
1573
+ }
1574
+ if (config.customizations?.vscode?.settings && typeof config.customizations.vscode.settings !== "object") {
1575
+ errors.push({ field: "customizations.vscode.settings", message: "Settings must be an object" });
1576
+ }
1577
+ if (!config.name) {
1578
+ suggestions.push({ field: "name", message: "Consider adding a name for better identification" });
1579
+ }
1580
+ if (!config.features || Object.keys(config.features).length === 0) {
1581
+ suggestions.push({ field: "features", message: "Consider adding features for common tools" });
1582
+ }
1583
+ if (!config.customizations?.vscode?.extensions || config.customizations.vscode.extensions.length === 0) {
1584
+ suggestions.push({ field: "extensions", message: "Consider adding VS Code extensions for your stack" });
1585
+ }
1586
+ if (config.privileged) {
1587
+ warnings.push({ field: "privileged", message: "Running in privileged mode is a security risk" });
1588
+ }
1589
+ return { errors, suggestions, valid: errors.length === 0, warnings };
1590
+ };
1591
+
1592
+ const MIN_VIEWPORT_WIDTH = 80;
1593
+ const MIN_VIEWPORT_HEIGHT = 15;
1594
+ const MIN_SPLIT_WIDTH = 120;
1595
+ const EDITOR_SECTIONS = [
1596
+ { description: "Container name, base image, workspace folder, and user", id: "general", label: "General" },
1597
+ { description: "Installable tools and runtimes (Node, Python, Docker, etc.)", id: "features", label: "Features" },
1598
+ { description: "Ports to forward from the container to your host", id: "ports", label: "Ports" },
1599
+ { description: "Commands to run at different stages of the container lifecycle", id: "lifecycle", label: "Lifecycle" },
1600
+ { description: "VS Code extensions to auto-install in the container", id: "extensions", label: "Extensions" },
1601
+ { description: "Environment variables for the container and IDE", id: "environment", label: "Env" },
1602
+ { description: "Volume and bind mounts for persistent data and caches", id: "mounts", label: "Mounts" },
1603
+ { description: "Docker Compose integration for multi-container setups", id: "compose", label: "Compose" }
1604
+ ];
1605
+ const getFieldCount = (section, config, featureSearch, extensionSearch) => {
1606
+ switch (section) {
1607
+ case "compose": {
1608
+ return COMPOSE_FIELD_COUNT;
1609
+ }
1610
+ case "environment": {
1611
+ return getEnvFieldCount(config);
1612
+ }
1613
+ case "extensions": {
1614
+ return filterExtensions(extensionSearch).length;
1615
+ }
1616
+ case "features": {
1617
+ return filterFeatures(featureSearch).length;
1618
+ }
1619
+ case "general": {
1620
+ return GENERAL_FIELD_COUNT;
1621
+ }
1622
+ case "lifecycle": {
1623
+ return LIFECYCLE_FIELD_COUNT;
1624
+ }
1625
+ case "mounts": {
1626
+ return (config.mounts?.length ?? 0) + 1;
1627
+ }
1628
+ case "ports": {
1629
+ return (config.forwardPorts?.length ?? 0) + 1;
1630
+ }
1631
+ default: {
1632
+ return 0;
1633
+ }
1634
+ }
1635
+ };
1636
+ const VisDevcontainerApp = ({ onSave, store }) => {
1637
+ const { exit } = useApp();
1638
+ const { columns, rows } = useWindowSize();
1639
+ const state = useSyncExternalStore(store.subscribe, store.getSnapshot);
1640
+ const [helpVisible, setHelpVisible] = useState(false);
1641
+ const [quitDialogVisible, setQuitDialogVisible] = useState(false);
1642
+ const [searchActive, setSearchActive] = useState(false);
1643
+ const [saveMessage, setSaveMessage] = useState(null);
1644
+ const [focusedPanel, setFocusedPanel] = useState("editor");
1645
+ const [listScrollOffset, setListScrollOffset] = useState(0);
1646
+ const [addingPort, setAddingPort] = useState(false);
1647
+ const [addPortValue, setAddPortValue] = useState("");
1648
+ const [addingEnv, setAddingEnv] = useState(null);
1649
+ const [addEnvKey, setAddEnvKey] = useState("");
1650
+ const [addEnvValue, setAddEnvValue] = useState("");
1651
+ const [addEnvPhase, setAddEnvPhase] = useState("key");
1652
+ const [addingMount, setAddingMount] = useState(false);
1653
+ const [mountSource, setMountSource] = useState("");
1654
+ const [mountTarget, setMountTarget] = useState("");
1655
+ const [mountType, setMountType] = useState("volume");
1656
+ const [mountPhase, setMountPhase] = useState("source");
1657
+ const helpScrollRef = useRef(null);
1658
+ const previewScrollRef = useRef(null);
1659
+ const saveTimerRef = useRef(null);
1660
+ const mountedRef = useRef(true);
1661
+ useEffect(() => {
1662
+ mountedRef.current = true;
1663
+ return () => {
1664
+ mountedRef.current = false;
1665
+ if (saveTimerRef.current) {
1666
+ clearTimeout(saveTimerRef.current);
1667
+ }
1668
+ };
1669
+ }, []);
1670
+ const fieldCount = getFieldCount(state.section, state.config, state.featureSearch, state.extensionSearch);
1671
+ const listViewportHeight = Math.max(1, rows - 9);
1672
+ useEffect(() => {
1673
+ if (state.section !== "features" && state.section !== "extensions") {
1674
+ return;
1675
+ }
1676
+ setListScrollOffset((current) => {
1677
+ if (state.fieldIndex >= current + listViewportHeight) {
1678
+ return state.fieldIndex - listViewportHeight + 1;
1679
+ }
1680
+ if (state.fieldIndex < current) {
1681
+ return state.fieldIndex;
1682
+ }
1683
+ return current;
1684
+ });
1685
+ }, [state.fieldIndex, state.section, listViewportHeight]);
1686
+ useEffect(() => {
1687
+ setListScrollOffset(0);
1688
+ }, [state.section, state.featureSearch, state.extensionSearch]);
1689
+ const handleSave = useCallback(() => {
1690
+ const cleanConfig = store.cleanConfig();
1691
+ const validation = validateConfig(cleanConfig);
1692
+ if (!validation.valid) {
1693
+ const firstError = validation.errors[0];
1694
+ setSaveMessage(firstError ? `Error: ${firstError.message}` : "Validation failed");
1695
+ if (saveTimerRef.current) {
1696
+ clearTimeout(saveTimerRef.current);
1697
+ }
1698
+ saveTimerRef.current = setTimeout(() => {
1699
+ if (mountedRef.current) {
1700
+ setSaveMessage(null);
1701
+ }
1702
+ }, 3e3);
1703
+ return;
1704
+ }
1705
+ onSave(cleanConfig);
1706
+ store.markClean();
1707
+ const warningCount = validation.warnings.length;
1708
+ setSaveMessage(warningCount > 0 ? `Saved! (${String(warningCount)} warning${warningCount > 1 ? "s" : ""})` : "Saved!");
1709
+ if (saveTimerRef.current) {
1710
+ clearTimeout(saveTimerRef.current);
1711
+ }
1712
+ saveTimerRef.current = setTimeout(() => {
1713
+ if (mountedRef.current) {
1714
+ setSaveMessage(null);
1715
+ }
1716
+ }, 2e3);
1717
+ }, [onSave, store]);
1718
+ useInput(
1719
+ (input, key) => {
1720
+ if (key.downArrow || input === "j") {
1721
+ store.setTemplateIndex(state.templateIndex + 1);
1722
+ } else if (key.upArrow || input === "k") {
1723
+ store.setTemplateIndex(state.templateIndex - 1);
1724
+ } else if (key.return) {
1725
+ const template = TEMPLATES[state.templateIndex];
1726
+ if (template) {
1727
+ store.applyTemplate(template.id);
1728
+ }
1729
+ } else if (key.escape) {
1730
+ store.dismissTemplateSelector();
1731
+ }
1732
+ },
1733
+ { isActive: state.showTemplateSelector }
1734
+ );
1735
+ useInput(
1736
+ (input, key) => {
1737
+ if (key.escape) {
1738
+ setAddingPort(false);
1739
+ setAddPortValue("");
1740
+ return;
1741
+ }
1742
+ if (key.return) {
1743
+ const parsed = Number.parseInt(addPortValue, 10);
1744
+ if (!Number.isNaN(parsed) && parsed > 0 && parsed <= 65535) {
1745
+ store.addPort(parsed);
1746
+ }
1747
+ setAddingPort(false);
1748
+ setAddPortValue("");
1749
+ return;
1750
+ }
1751
+ if (key.backspace) {
1752
+ setAddPortValue((v) => v.slice(0, -1));
1753
+ return;
1754
+ }
1755
+ if (input && /^\d$/u.test(input)) {
1756
+ setAddPortValue((v) => v + input);
1757
+ }
1758
+ },
1759
+ { isActive: addingPort }
1760
+ );
1761
+ useInput(
1762
+ (input, key) => {
1763
+ if (key.escape) {
1764
+ setAddingEnv(null);
1765
+ setAddEnvKey("");
1766
+ setAddEnvValue("");
1767
+ setAddEnvPhase("key");
1768
+ return;
1769
+ }
1770
+ if (key.return) {
1771
+ if (addEnvPhase === "key" && addEnvKey) {
1772
+ setAddEnvPhase("value");
1773
+ return;
1774
+ }
1775
+ if (addEnvPhase === "value" && addEnvKey) {
1776
+ store.addEnvVar(addingEnv, addEnvKey, addEnvValue);
1777
+ setAddingEnv(null);
1778
+ setAddEnvKey("");
1779
+ setAddEnvValue("");
1780
+ setAddEnvPhase("key");
1781
+ return;
1782
+ }
1783
+ }
1784
+ if (key.backspace) {
1785
+ if (addEnvPhase === "key") {
1786
+ setAddEnvKey((v) => v.slice(0, -1));
1787
+ } else {
1788
+ setAddEnvValue((v) => v.slice(0, -1));
1789
+ }
1790
+ return;
1791
+ }
1792
+ if (input && !key.ctrl && !key.meta) {
1793
+ if (addEnvPhase === "key") {
1794
+ setAddEnvKey((v) => v + input);
1795
+ } else {
1796
+ setAddEnvValue((v) => v + input);
1797
+ }
1798
+ }
1799
+ },
1800
+ { isActive: addingEnv !== null }
1801
+ );
1802
+ useInput(
1803
+ (input, key) => {
1804
+ if (key.escape) {
1805
+ setAddingMount(false);
1806
+ setMountSource("");
1807
+ setMountTarget("");
1808
+ setMountPhase("source");
1809
+ return;
1810
+ }
1811
+ if (key.return) {
1812
+ if (mountPhase === "source" && mountSource) {
1813
+ setMountPhase("target");
1814
+ return;
1815
+ }
1816
+ if (mountPhase === "target" && mountTarget) {
1817
+ setMountPhase("type");
1818
+ return;
1819
+ }
1820
+ if (mountPhase === "type") {
1821
+ store.addMount({ source: mountSource, target: mountTarget, type: mountType });
1822
+ setAddingMount(false);
1823
+ setMountSource("");
1824
+ setMountTarget("");
1825
+ setMountPhase("source");
1826
+ return;
1827
+ }
1828
+ }
1829
+ if (mountPhase === "type") {
1830
+ switch (input) {
1831
+ case "1": {
1832
+ setMountType("volume");
1833
+ break;
1834
+ }
1835
+ case "2": {
1836
+ setMountType("bind");
1837
+ break;
1838
+ }
1839
+ case "3": {
1840
+ setMountType("tmpfs");
1841
+ break;
1842
+ }
1843
+ }
1844
+ return;
1845
+ }
1846
+ if (key.backspace) {
1847
+ if (mountPhase === "source") {
1848
+ setMountSource((v) => v.slice(0, -1));
1849
+ } else if (mountPhase === "target") {
1850
+ setMountTarget((v) => v.slice(0, -1));
1851
+ }
1852
+ return;
1853
+ }
1854
+ if (input && !key.ctrl && !key.meta) {
1855
+ if (mountPhase === "source") {
1856
+ setMountSource((v) => v + input);
1857
+ } else if (mountPhase === "target") {
1858
+ setMountTarget((v) => v + input);
1859
+ }
1860
+ }
1861
+ },
1862
+ { isActive: addingMount }
1863
+ );
1864
+ useInput(
1865
+ (input, key) => {
1866
+ if (input === "c" && key.ctrl) {
1867
+ exit();
1868
+ return;
1869
+ }
1870
+ if (quitDialogVisible) {
1871
+ return;
1872
+ }
1873
+ if (helpVisible) {
1874
+ if (key.escape || input === "?") {
1875
+ setHelpVisible(false);
1876
+ } else if (key.downArrow || input === "j") {
1877
+ helpScrollRef.current?.scrollBy(1);
1878
+ } else if (key.upArrow || input === "k") {
1879
+ helpScrollRef.current?.scrollBy(-1);
1880
+ } else if (input === "q") {
1881
+ setHelpVisible(false);
1882
+ setQuitDialogVisible(true);
1883
+ }
1884
+ return;
1885
+ }
1886
+ if (searchActive) {
1887
+ if (key.escape) {
1888
+ setSearchActive(false);
1889
+ if (state.section === "features") {
1890
+ store.setFeatureSearch("");
1891
+ } else {
1892
+ store.setExtensionSearch("");
1893
+ }
1894
+ return;
1895
+ }
1896
+ if (key.return) {
1897
+ setSearchActive(false);
1898
+ return;
1899
+ }
1900
+ if (key.backspace) {
1901
+ if (state.section === "features") {
1902
+ store.setFeatureSearch(state.featureSearch.slice(0, -1));
1903
+ } else {
1904
+ store.setExtensionSearch(state.extensionSearch.slice(0, -1));
1905
+ }
1906
+ return;
1907
+ }
1908
+ if (input && !key.ctrl && !key.meta) {
1909
+ if (state.section === "features") {
1910
+ store.setFeatureSearch(state.featureSearch + input);
1911
+ } else {
1912
+ store.setExtensionSearch(state.extensionSearch + input);
1913
+ }
1914
+ return;
1915
+ }
1916
+ return;
1917
+ }
1918
+ if (state.fieldEditing) {
1919
+ if (key.escape) {
1920
+ store.setFieldEditing(false);
1921
+ return;
1922
+ }
1923
+ if (key.return) {
1924
+ store.setFieldEditing(false);
1925
+ return;
1926
+ }
1927
+ return;
1928
+ }
1929
+ if (input === "?") {
1930
+ setHelpVisible(true);
1931
+ return;
1932
+ }
1933
+ if (input === "q") {
1934
+ if (state.isDirty) {
1935
+ setQuitDialogVisible(true);
1936
+ } else {
1937
+ exit();
1938
+ }
1939
+ return;
1940
+ }
1941
+ if (input === "s") {
1942
+ handleSave();
1943
+ return;
1944
+ }
1945
+ if (key.tab) {
1946
+ setFocusedPanel((p) => p === "editor" ? "preview" : "editor");
1947
+ return;
1948
+ }
1949
+ if (focusedPanel === "preview") {
1950
+ if (key.downArrow || input === "j") {
1951
+ previewScrollRef.current?.scrollBy(1);
1952
+ return;
1953
+ }
1954
+ if (key.upArrow || input === "k") {
1955
+ previewScrollRef.current?.scrollBy(-1);
1956
+ return;
1957
+ }
1958
+ if (key.pageDown) {
1959
+ previewScrollRef.current?.scrollBy(10);
1960
+ return;
1961
+ }
1962
+ if (key.pageUp) {
1963
+ previewScrollRef.current?.scrollBy(-10);
1964
+ return;
1965
+ }
1966
+ if (key.home) {
1967
+ previewScrollRef.current?.scrollToTop();
1968
+ return;
1969
+ }
1970
+ if (key.end) {
1971
+ previewScrollRef.current?.scrollToBottom();
1972
+ return;
1973
+ }
1974
+ if (key.escape) {
1975
+ setFocusedPanel("editor");
1976
+ }
1977
+ return;
1978
+ }
1979
+ if (key.downArrow || input === "j") {
1980
+ if (fieldCount > 0) {
1981
+ store.setFieldIndex(Math.min(state.fieldIndex + 1, fieldCount - 1));
1982
+ }
1983
+ return;
1984
+ }
1985
+ if (key.upArrow || input === "k") {
1986
+ store.setFieldIndex(Math.max(state.fieldIndex - 1, 0));
1987
+ return;
1988
+ }
1989
+ if (key.return) {
1990
+ switch (state.section) {
1991
+ case "compose":
1992
+ case "general":
1993
+ case "lifecycle": {
1994
+ store.setFieldEditing(true);
1995
+ break;
1996
+ }
1997
+ case "environment": {
1998
+ const containerCount = Object.keys(state.config.containerEnv ?? {}).length;
1999
+ const containerAddIndex = containerCount;
2000
+ const remoteAddIndex = containerCount + 1 + Object.keys(state.config.remoteEnv ?? {}).length;
2001
+ if (state.fieldIndex === containerAddIndex) {
2002
+ setAddingEnv("container");
2003
+ setAddEnvKey("");
2004
+ setAddEnvValue("");
2005
+ setAddEnvPhase("key");
2006
+ } else if (state.fieldIndex === remoteAddIndex) {
2007
+ setAddingEnv("remote");
2008
+ setAddEnvKey("");
2009
+ setAddEnvValue("");
2010
+ setAddEnvPhase("key");
2011
+ }
2012
+ break;
2013
+ }
2014
+ case "mounts": {
2015
+ const mounts = state.config.mounts ?? [];
2016
+ if (state.fieldIndex === mounts.length) {
2017
+ setAddingMount(true);
2018
+ setMountSource("");
2019
+ setMountTarget("");
2020
+ setMountType("volume");
2021
+ setMountPhase("source");
2022
+ }
2023
+ break;
2024
+ }
2025
+ case "ports": {
2026
+ const ports = state.config.forwardPorts ?? [];
2027
+ if (state.fieldIndex === ports.length) {
2028
+ setAddingPort(true);
2029
+ setAddPortValue("");
2030
+ }
2031
+ break;
2032
+ }
2033
+ }
2034
+ return;
2035
+ }
2036
+ if (input === " ") {
2037
+ switch (state.section) {
2038
+ case "extensions": {
2039
+ const catalog = filterExtensions(state.extensionSearch);
2040
+ const ext = catalog[state.fieldIndex];
2041
+ if (ext) {
2042
+ store.toggleExtension(ext.id);
2043
+ }
2044
+ break;
2045
+ }
2046
+ case "features": {
2047
+ const catalog = filterFeatures(state.featureSearch);
2048
+ const feature = catalog[state.fieldIndex];
2049
+ if (feature) {
2050
+ store.toggleFeature(feature.id);
2051
+ }
2052
+ break;
2053
+ }
2054
+ case "general": {
2055
+ const stringFieldCount = GENERAL_FIELD_COUNT - GENERAL_BOOLEAN_FIELDS.length;
2056
+ const boolIndex = state.fieldIndex - stringFieldCount;
2057
+ if (boolIndex >= 0 && boolIndex < GENERAL_BOOLEAN_FIELDS.length) {
2058
+ const field = GENERAL_BOOLEAN_FIELDS[boolIndex];
2059
+ store.updateConfig({ [field]: !state.config[field] });
2060
+ }
2061
+ break;
2062
+ }
2063
+ }
2064
+ return;
2065
+ }
2066
+ if (input === "/") {
2067
+ if (state.section === "features" || state.section === "extensions") {
2068
+ setSearchActive(true);
2069
+ }
2070
+ return;
2071
+ }
2072
+ if (input === "A" && state.section === "mounts") {
2073
+ store.applySuggestedMounts();
2074
+ return;
2075
+ }
2076
+ if (input === "a") {
2077
+ if (state.section === "environment") {
2078
+ const containerCount = Object.keys(state.config.containerEnv ?? {}).length;
2079
+ const target = state.fieldIndex <= containerCount ? "container" : "remote";
2080
+ setAddingEnv(target);
2081
+ setAddEnvKey("");
2082
+ setAddEnvValue("");
2083
+ setAddEnvPhase("key");
2084
+ } else if (state.section === "mounts") {
2085
+ setAddingMount(true);
2086
+ setMountSource("");
2087
+ setMountTarget("");
2088
+ setMountType("volume");
2089
+ setMountPhase("source");
2090
+ }
2091
+ return;
2092
+ }
2093
+ if (input === "d") {
2094
+ switch (state.section) {
2095
+ case "environment": {
2096
+ const containerKeys = Object.keys(state.config.containerEnv ?? {});
2097
+ const remoteKeys = Object.keys(state.config.remoteEnv ?? {});
2098
+ if (state.fieldIndex < containerKeys.length) {
2099
+ store.removeEnvVar("container", containerKeys[state.fieldIndex]);
2100
+ if (containerKeys.length === 1) ; else if (state.fieldIndex >= containerKeys.length - 1) {
2101
+ store.setFieldIndex(containerKeys.length - 2);
2102
+ }
2103
+ } else {
2104
+ const remoteIndex = state.fieldIndex - containerKeys.length - 1;
2105
+ if (remoteIndex >= 0 && remoteIndex < remoteKeys.length) {
2106
+ store.removeEnvVar("remote", remoteKeys[remoteIndex]);
2107
+ if (remoteKeys.length === 1) ; else if (remoteIndex >= remoteKeys.length - 1) {
2108
+ store.setFieldIndex(state.fieldIndex - 1);
2109
+ }
2110
+ }
2111
+ }
2112
+ break;
2113
+ }
2114
+ case "mounts": {
2115
+ const mounts = state.config.mounts ?? [];
2116
+ if (state.fieldIndex < mounts.length) {
2117
+ store.removeMount(state.fieldIndex);
2118
+ const newCount = mounts.length - 1;
2119
+ if (state.fieldIndex >= newCount && newCount > 0) {
2120
+ store.setFieldIndex(newCount - 1);
2121
+ }
2122
+ }
2123
+ break;
2124
+ }
2125
+ case "ports": {
2126
+ const ports = state.config.forwardPorts ?? [];
2127
+ if (state.fieldIndex < ports.length) {
2128
+ store.removePort(state.fieldIndex);
2129
+ const newCount = ports.length - 1;
2130
+ if (state.fieldIndex >= newCount && newCount > 0) {
2131
+ store.setFieldIndex(newCount - 1);
2132
+ }
2133
+ }
2134
+ break;
2135
+ }
2136
+ }
2137
+ }
2138
+ },
2139
+ { isActive: !state.showTemplateSelector && !addingPort && addingEnv === null && !addingMount }
2140
+ );
2141
+ const jsonPreview = useMemo(() => store.getJsonPreview(), [state.config]);
2142
+ if (columns < MIN_VIEWPORT_WIDTH || rows < MIN_VIEWPORT_HEIGHT) {
2143
+ return jsx(Box, { alignItems: "center", height: rows, justifyContent: "center", width: columns, children: jsxs(Text, { color: "yellow", children: [
2144
+ "Terminal too small (",
2145
+ columns,
2146
+ "x",
2147
+ rows,
2148
+ "), need ",
2149
+ MIN_VIEWPORT_WIDTH,
2150
+ "x",
2151
+ MIN_VIEWPORT_HEIGHT
2152
+ ] }) });
2153
+ }
2154
+ if (state.showTemplateSelector) {
2155
+ return jsx(Box, { alignItems: "center", flexDirection: "column", height: rows, justifyContent: "center", width: columns, children: jsxs(Box, { borderColor: "cyan", borderStyle: "round", flexDirection: "column", paddingX: 2, paddingY: 1, width: 60, children: [
2156
+ jsx(Box, { justifyContent: "center", marginBottom: 1, children: jsx(Text, { bold: true, color: "cyan", children: "Select a Template" }) }),
2157
+ TEMPLATES.map((template, index) => {
2158
+ const isSelected = index === state.templateIndex;
2159
+ return jsx(Box, { children: jsxs(Text, { color: isSelected ? "cyan" : void 0, inverse: isSelected, children: [
2160
+ isSelected ? " ❯ " : " ",
2161
+ jsx(Text, { bold: isSelected, children: template.name }),
2162
+ jsxs(Text, { dimColor: true, children: [
2163
+ " -",
2164
+ template.description
2165
+ ] })
2166
+ ] }) }, template.id);
2167
+ }),
2168
+ jsx(Box, { justifyContent: "center", marginTop: 1, children: jsxs(Text, { dimColor: true, children: [
2169
+ jsx(Text, { bold: true, color: "white", children: "↑↓" }),
2170
+ " ",
2171
+ "navigate",
2172
+ " ",
2173
+ jsx(Text, { bold: true, color: "white", children: "Enter" }),
2174
+ " ",
2175
+ "select",
2176
+ " ",
2177
+ jsx(Text, { bold: true, color: "white", children: "Esc" }),
2178
+ " ",
2179
+ "blank"
2180
+ ] }) })
2181
+ ] }) });
2182
+ }
2183
+ let sectionContent;
2184
+ switch (state.section) {
2185
+ case "compose": {
2186
+ sectionContent = jsx(
2187
+ DockerComposeSection,
2188
+ {
2189
+ config: state.config,
2190
+ fieldEditing: state.fieldEditing,
2191
+ fieldIndex: state.fieldIndex,
2192
+ onUpdate: (partial) => {
2193
+ store.updateConfig(partial);
2194
+ }
2195
+ }
2196
+ );
2197
+ break;
2198
+ }
2199
+ case "environment": {
2200
+ sectionContent = jsxs(Box, { flexDirection: "column", children: [
2201
+ jsx(EnvironmentSection, { config: state.config, fieldIndex: state.fieldIndex }),
2202
+ addingEnv !== null && jsx(Box, { marginTop: 1, paddingX: 1, children: jsxs(Text, { color: "cyan", children: [
2203
+ "Add ",
2204
+ addingEnv,
2205
+ " env:",
2206
+ " ",
2207
+ addEnvPhase === "key" ? jsxs(Text, { children: [
2208
+ "key=",
2209
+ jsx(Text, { color: "yellow", children: addEnvKey || "_" }),
2210
+ " (Enter to set value)"
2211
+ ] }) : jsxs(Text, { children: [
2212
+ addEnvKey,
2213
+ "=",
2214
+ jsx(Text, { color: "yellow", children: addEnvValue || "_" }),
2215
+ " (Enter to confirm, Esc to cancel)"
2216
+ ] })
2217
+ ] }) })
2218
+ ] });
2219
+ break;
2220
+ }
2221
+ case "extensions": {
2222
+ sectionContent = jsx(
2223
+ ExtensionsSection,
2224
+ {
2225
+ config: state.config,
2226
+ fieldIndex: state.fieldIndex,
2227
+ scrollOffset: listScrollOffset,
2228
+ searchText: state.extensionSearch,
2229
+ viewportHeight: listViewportHeight
2230
+ }
2231
+ );
2232
+ break;
2233
+ }
2234
+ case "features": {
2235
+ sectionContent = jsx(
2236
+ FeaturesSection,
2237
+ {
2238
+ config: state.config,
2239
+ fieldIndex: state.fieldIndex,
2240
+ scrollOffset: listScrollOffset,
2241
+ searchText: state.featureSearch,
2242
+ viewportHeight: listViewportHeight
2243
+ }
2244
+ );
2245
+ break;
2246
+ }
2247
+ case "general": {
2248
+ sectionContent = jsx(
2249
+ GeneralSection,
2250
+ {
2251
+ config: state.config,
2252
+ fieldEditing: state.fieldEditing,
2253
+ fieldIndex: state.fieldIndex,
2254
+ onUpdate: (partial) => {
2255
+ store.updateConfig(partial);
2256
+ }
2257
+ }
2258
+ );
2259
+ break;
2260
+ }
2261
+ case "lifecycle": {
2262
+ sectionContent = jsx(
2263
+ LifecycleSection,
2264
+ {
2265
+ config: state.config,
2266
+ fieldEditing: state.fieldEditing,
2267
+ fieldIndex: state.fieldIndex,
2268
+ onSetCommand: (hook, command) => {
2269
+ store.setLifecycleCommand(hook, command);
2270
+ }
2271
+ }
2272
+ );
2273
+ break;
2274
+ }
2275
+ case "mounts": {
2276
+ sectionContent = jsx(
2277
+ MountsSection,
2278
+ {
2279
+ addingMount,
2280
+ config: state.config,
2281
+ detectedPm: state.detectedPm,
2282
+ fieldIndex: state.fieldIndex,
2283
+ mountPhase,
2284
+ mountSource,
2285
+ mountTarget,
2286
+ mountType,
2287
+ suggestedMounts: state.suggestedMounts
2288
+ }
2289
+ );
2290
+ break;
2291
+ }
2292
+ case "ports": {
2293
+ sectionContent = jsx(PortsSection, { addingPort, addPortValue, config: state.config, fieldIndex: state.fieldIndex });
2294
+ break;
2295
+ }
2296
+ default: {
2297
+ sectionContent = jsx(Text, { children: "Unknown section" });
2298
+ }
2299
+ }
2300
+ const footer = jsxs(Box, { borderBottom: false, borderColor: "gray", borderLeft: false, borderRight: false, borderStyle: "single", flexShrink: 0, children: [
2301
+ jsxs(Box, { flexGrow: 1, flexWrap: "wrap", gap: 2, paddingX: 1, children: [
2302
+ jsxs(Box, { gap: 1, children: [
2303
+ jsx(Text, { bold: true, color: "white", children: "q" }),
2304
+ jsx(Text, { dimColor: true, children: "QUIT" })
2305
+ ] }),
2306
+ jsxs(Box, { gap: 1, children: [
2307
+ jsx(Text, { bold: true, color: "white", children: "?" }),
2308
+ jsx(Text, { dimColor: true, children: "HELP" })
2309
+ ] }),
2310
+ jsxs(Box, { gap: 1, children: [
2311
+ jsx(Text, { bold: true, color: "white", children: "↑↓" }),
2312
+ jsx(Text, { dimColor: true, children: "NAV" })
2313
+ ] }),
2314
+ (state.section === "features" || state.section === "extensions") && jsxs(Box, { gap: 1, children: [
2315
+ jsx(Text, { bold: true, color: "white", children: "Space" }),
2316
+ jsx(Text, { dimColor: true, children: "CHECK" })
2317
+ ] }),
2318
+ jsxs(Box, { gap: 1, children: [
2319
+ jsx(Text, { bold: true, color: "white", children: "←→" }),
2320
+ jsx(Text, { dimColor: true, children: "TABS" })
2321
+ ] }),
2322
+ jsxs(Box, { gap: 1, children: [
2323
+ jsx(Text, { bold: true, color: "white", children: "Tab" }),
2324
+ jsx(Text, { dimColor: true, children: "PANEL" })
2325
+ ] }),
2326
+ (state.section === "features" || state.section === "extensions") && jsxs(Box, { gap: 1, children: [
2327
+ jsx(Text, { bold: true, color: "white", children: "/" }),
2328
+ jsx(Text, { dimColor: true, children: "FILTER" })
2329
+ ] }),
2330
+ jsxs(Box, { gap: 1, children: [
2331
+ jsx(Text, { bold: true, color: "white", children: "s" }),
2332
+ jsx(Text, { dimColor: true, children: "SAVE" })
2333
+ ] })
2334
+ ] }),
2335
+ jsxs(Box, { paddingX: 1, children: [
2336
+ saveMessage && jsxs(Text, { color: saveMessage.startsWith("Error") ? "red" : "green", children: [
2337
+ saveMessage,
2338
+ " "
2339
+ ] }),
2340
+ state.isDirty && jsx(Text, { color: "yellow", children: "[modified]" }),
2341
+ !state.isDirty && !saveMessage && jsx(Text, { dimColor: true, children: "[saved]" })
2342
+ ] })
2343
+ ] });
2344
+ const helpPopup = jsxs(
2345
+ Dialog,
2346
+ {
2347
+ footer: jsxs(Text, { dimColor: true, children: [
2348
+ jsx(Text, { bold: true, color: "white", children: "↑↓" }),
2349
+ " ",
2350
+ "scroll",
2351
+ " ",
2352
+ jsx(Text, { bold: true, color: "white", children: "?" }),
2353
+ "/",
2354
+ jsx(Text, { bold: true, color: "white", children: "Esc" }),
2355
+ " ",
2356
+ "close"
2357
+ ] }),
2358
+ scrollRef: helpScrollRef,
2359
+ title: "KEYBOARD SHORTCUTS",
2360
+ visible: helpVisible,
2361
+ width: 56,
2362
+ children: [
2363
+ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
2364
+ jsxs(Box, { marginBottom: 1, children: [
2365
+ jsx(Text, { dimColor: true, children: "── " }),
2366
+ jsx(Text, { bold: true, color: "white", children: "NAVIGATION" })
2367
+ ] }),
2368
+ jsxs(Text, { children: [
2369
+ " ",
2370
+ jsx(Text, { bold: true, color: "white", children: "←→" }),
2371
+ jsx(Text, { dimColor: true, children: " Switch tabs" })
2372
+ ] }),
2373
+ jsxs(Text, { children: [
2374
+ " ",
2375
+ jsx(Text, { bold: true, color: "white", children: "↑↓" }),
2376
+ "/",
2377
+ jsx(Text, { bold: true, color: "white", children: "j/k" }),
2378
+ jsx(Text, { dimColor: true, children: " Navigate within section" })
2379
+ ] }),
2380
+ jsxs(Text, { children: [
2381
+ " ",
2382
+ jsx(Text, { bold: true, color: "white", children: "Tab" }),
2383
+ jsx(Text, { dimColor: true, children: " Switch editor/preview panel" })
2384
+ ] }),
2385
+ jsxs(Text, { children: [
2386
+ " ",
2387
+ jsx(Text, { bold: true, color: "white", children: "Enter" }),
2388
+ jsx(Text, { dimColor: true, children: " Edit selected field" })
2389
+ ] }),
2390
+ jsxs(Text, { children: [
2391
+ " ",
2392
+ jsx(Text, { bold: true, color: "white", children: "Esc" }),
2393
+ jsx(Text, { dimColor: true, children: " Stop editing / cancel" })
2394
+ ] })
2395
+ ] }),
2396
+ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
2397
+ jsxs(Box, { marginBottom: 1, children: [
2398
+ jsx(Text, { dimColor: true, children: "── " }),
2399
+ jsx(Text, { bold: true, color: "white", children: "FEATURES / EXTENSIONS" })
2400
+ ] }),
2401
+ jsxs(Text, { children: [
2402
+ " ",
2403
+ jsx(Text, { bold: true, color: "white", children: "Space" }),
2404
+ jsx(Text, { dimColor: true, children: " Toggle selection" })
2405
+ ] }),
2406
+ jsxs(Text, { children: [
2407
+ " ",
2408
+ jsx(Text, { bold: true, color: "white", children: "/" }),
2409
+ jsx(Text, { dimColor: true, children: " Search / filter" })
2410
+ ] })
2411
+ ] }),
2412
+ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
2413
+ jsxs(Box, { marginBottom: 1, children: [
2414
+ jsx(Text, { dimColor: true, children: "── " }),
2415
+ jsx(Text, { bold: true, color: "white", children: "LISTS (Ports, Mounts, Env)" })
2416
+ ] }),
2417
+ jsxs(Text, { children: [
2418
+ " ",
2419
+ jsx(Text, { bold: true, color: "white", children: "a" }),
2420
+ jsx(Text, { dimColor: true, children: " Add new entry" })
2421
+ ] }),
2422
+ jsxs(Text, { children: [
2423
+ " ",
2424
+ jsx(Text, { bold: true, color: "white", children: "d" }),
2425
+ jsx(Text, { dimColor: true, children: " Delete selected entry" })
2426
+ ] })
2427
+ ] }),
2428
+ jsxs(Box, { flexDirection: "column", children: [
2429
+ jsxs(Box, { marginBottom: 1, children: [
2430
+ jsx(Text, { dimColor: true, children: "── " }),
2431
+ jsx(Text, { bold: true, color: "white", children: "ACTIONS" })
2432
+ ] }),
2433
+ jsxs(Text, { children: [
2434
+ " ",
2435
+ jsx(Text, { bold: true, color: "white", children: "s" }),
2436
+ jsx(Text, { dimColor: true, children: " Save configuration" })
2437
+ ] }),
2438
+ jsxs(Text, { children: [
2439
+ " ",
2440
+ jsx(Text, { bold: true, color: "white", children: "q" }),
2441
+ jsx(Text, { dimColor: true, children: " Quit" })
2442
+ ] }),
2443
+ jsxs(Text, { children: [
2444
+ " ",
2445
+ jsx(Text, { bold: true, color: "white", children: "?" }),
2446
+ jsx(Text, { dimColor: true, children: " Toggle help" })
2447
+ ] })
2448
+ ] })
2449
+ ]
2450
+ }
2451
+ );
2452
+ const previewPanel = jsx(
2453
+ PreviewPanel,
2454
+ {
2455
+ focused: focusedPanel === "preview",
2456
+ hadComments: state.hadComments,
2457
+ jsonPreview,
2458
+ mode: state.mode,
2459
+ scrollRef: previewScrollRef
2460
+ }
2461
+ );
2462
+ const isSplitLayout = columns >= MIN_SPLIT_WIDTH;
2463
+ const previewWidth = isSplitLayout ? Math.floor(columns * 0.38) : 0;
2464
+ return jsxs(Box, { flexDirection: "column", height: rows, width: columns, children: [
2465
+ jsxs(Box, { flexShrink: 0, gap: 1, paddingX: 1, children: [
2466
+ jsx(Text, { bold: true, inverse: true, children: " VIS " }),
2467
+ jsxs(Text, { wrap: "truncate", children: [
2468
+ state.mode === "create" ? "Create" : "Edit",
2469
+ " devcontainer"
2470
+ ] })
2471
+ ] }),
2472
+ jsx(Box, { flexShrink: 0, paddingX: 1, paddingY: 1, children: jsx(
2473
+ Tabs,
2474
+ {
2475
+ defaultValue: state.section,
2476
+ keyMap: { useNumbers: false, useTab: false },
2477
+ onChange: (name) => {
2478
+ store.setSection(name);
2479
+ setFocusedPanel("editor");
2480
+ },
2481
+ showIndex: false,
2482
+ children: EDITOR_SECTIONS.map(({ id, label }) => jsx(Tab, { name: id, children: label }, id))
2483
+ }
2484
+ ) }),
2485
+ jsx(Box, { flexShrink: 0, paddingRight: 2, children: jsx(Text, { dimColor: true, wrap: "truncate", children: EDITOR_SECTIONS.find((s) => s.id === state.section)?.description ?? "" }) }),
2486
+ jsxs(Box, { flexDirection: "row", flexGrow: 1, overflow: "hidden", children: [
2487
+ jsx(Box, { borderColor: focusedPanel === "editor" ? "white" : "gray", borderStyle: "single", flexDirection: "column", flexGrow: 1, overflow: "hidden", children: sectionContent }),
2488
+ isSplitLayout && jsx(Box, { flexShrink: 0, width: previewWidth, children: previewPanel })
2489
+ ] }),
2490
+ footer,
2491
+ jsx(
2492
+ QuitDialog,
2493
+ {
2494
+ autoExitSeconds: 3,
2495
+ onCancel: () => {
2496
+ setQuitDialogVisible(false);
2497
+ },
2498
+ visible: quitDialogVisible
2499
+ }
2500
+ ),
2501
+ helpPopup
2502
+ ] });
2503
+ };
2504
+
2505
+ const execute = async ({ logger, options, workspaceRoot: wsRoot }) => {
2506
+ if (!wsRoot) {
2507
+ throw new Error("Could not determine workspace root. Run this command inside a monorepo or project directory.");
2508
+ }
2509
+ const workspaceRoot = wsRoot;
2510
+ const templateId = options.template;
2511
+ const outputPath = options.output;
2512
+ const isTTY = Boolean(process.stdout.isTTY) && !isInCi;
2513
+ let detectedPm = null;
2514
+ try {
2515
+ const pmInfo = detectPm(workspaceRoot);
2516
+ detectedPm = pmInfo.name;
2517
+ } catch {
2518
+ }
2519
+ const existing = readDevcontainerJson(workspaceRoot);
2520
+ let initialConfig = existing?.config ?? null;
2521
+ const hadComments = existing?.hadComments ?? false;
2522
+ if (templateId && !existing) {
2523
+ const template = TEMPLATES.find((t) => t.id === templateId);
2524
+ if (!template) {
2525
+ const validIds = TEMPLATES.map((t) => t.id).join(", ");
2526
+ throw new Error(`Unknown template "${templateId}". Valid templates: ${validIds}`);
2527
+ }
2528
+ initialConfig = template.config;
2529
+ }
2530
+ if (!isTTY) {
2531
+ if (initialConfig) {
2532
+ logger.info(JSON.stringify(initialConfig, null, 2));
2533
+ } else {
2534
+ logger.error("No existing devcontainer.json found. Use --template to generate one in non-TTY mode.");
2535
+ process.exitCode = 1;
2536
+ }
2537
+ return;
2538
+ }
2539
+ if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
2540
+ process.stdin.setRawMode(true);
2541
+ process.stdin.ref();
2542
+ process.stdin.resume();
2543
+ }
2544
+ const keepAlive = setInterval(() => {
2545
+ }, 1e3);
2546
+ const store = new DevcontainerStore(initialConfig, hadComments, detectedPm);
2547
+ if (templateId && !existing) {
2548
+ store.dismissTemplateSelector();
2549
+ }
2550
+ let savedConfig = null;
2551
+ const instance = render(
2552
+ React.createElement(VisDevcontainerApp, {
2553
+ onSave: (config) => {
2554
+ writeDevcontainerJson(workspaceRoot, config, outputPath);
2555
+ savedConfig = config;
2556
+ },
2557
+ store
2558
+ }),
2559
+ {
2560
+ alternateScreen: true,
2561
+ exitOnCtrlC: false,
2562
+ interactive: true,
2563
+ patchConsole: true
2564
+ }
2565
+ );
2566
+ await instance.waitUntilExit();
2567
+ clearInterval(keepAlive);
2568
+ if (savedConfig) {
2569
+ const target = outputPath ?? ".devcontainer/devcontainer.json";
2570
+ logger.info(`DevContainer config saved to ${target}`);
2571
+ }
2572
+ };
2573
+
2574
+ export { execute as default };