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

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 (116) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/LICENSE.md +559 -186
  3. package/README.md +18 -0
  4. package/dist/bin.js +1 -9
  5. package/dist/config/index.d.ts +477 -556
  6. package/dist/config/index.js +1 -2
  7. package/dist/generate/index.js +1 -3
  8. package/dist/packem_chunks/applyDefaults.js +2 -336
  9. package/dist/packem_chunks/bin.js +234 -9552
  10. package/dist/packem_chunks/doctor-probe.js +2 -112
  11. package/dist/packem_chunks/fix.js +11 -234
  12. package/dist/packem_chunks/handler.js +1 -99
  13. package/dist/packem_chunks/handler10.js +2 -53
  14. package/dist/packem_chunks/handler11.js +1 -32
  15. package/dist/packem_chunks/handler12.js +5 -100
  16. package/dist/packem_chunks/handler13.js +1 -25
  17. package/dist/packem_chunks/handler14.js +18 -916
  18. package/dist/packem_chunks/handler15.js +15 -201
  19. package/dist/packem_chunks/handler16.js +1 -124
  20. package/dist/packem_chunks/handler17.js +1 -13
  21. package/dist/packem_chunks/handler18.js +1 -106
  22. package/dist/packem_chunks/handler19.js +1 -19
  23. package/dist/packem_chunks/handler2.js +2 -75
  24. package/dist/packem_chunks/handler20.js +5 -29
  25. package/dist/packem_chunks/handler21.js +1 -222
  26. package/dist/packem_chunks/handler22.js +1 -237
  27. package/dist/packem_chunks/handler23.js +5 -101
  28. package/dist/packem_chunks/handler24.js +1 -110
  29. package/dist/packem_chunks/handler25.js +3 -402
  30. package/dist/packem_chunks/handler26.js +1 -13
  31. package/dist/packem_chunks/handler27.js +1 -63
  32. package/dist/packem_chunks/handler28.js +7 -34
  33. package/dist/packem_chunks/handler29.js +21 -456
  34. package/dist/packem_chunks/handler3.js +4 -95
  35. package/dist/packem_chunks/handler30.js +3 -170
  36. package/dist/packem_chunks/handler31.js +1 -530
  37. package/dist/packem_chunks/handler32.js +2 -214
  38. package/dist/packem_chunks/handler33.js +25 -119
  39. package/dist/packem_chunks/handler34.js +2 -630
  40. package/dist/packem_chunks/handler35.js +3 -283
  41. package/dist/packem_chunks/handler36.js +22 -542
  42. package/dist/packem_chunks/handler37.js +410 -744
  43. package/dist/packem_chunks/handler38.js +22 -989
  44. package/dist/packem_chunks/handler39.js +22 -574
  45. package/dist/packem_chunks/handler4.js +2 -90
  46. package/dist/packem_chunks/handler40.js +22 -1685
  47. package/dist/packem_chunks/handler41.js +6 -1088
  48. package/dist/packem_chunks/handler42.js +5 -797
  49. package/dist/packem_chunks/handler43.js +10 -2658
  50. package/dist/packem_chunks/handler44.js +51 -3784
  51. package/dist/packem_chunks/handler45.js +25 -2574
  52. package/dist/packem_chunks/handler46.js +3 -3769
  53. package/dist/packem_chunks/handler47.js +21 -1485
  54. package/dist/packem_chunks/handler48.js +42 -0
  55. package/dist/packem_chunks/handler5.js +8 -174
  56. package/dist/packem_chunks/handler6.js +1 -95
  57. package/dist/packem_chunks/handler7.js +1 -115
  58. package/dist/packem_chunks/handler8.js +1 -12
  59. package/dist/packem_chunks/handler9.js +1 -29
  60. package/dist/packem_chunks/heal-accept.js +10 -522
  61. package/dist/packem_chunks/heal.js +14 -673
  62. package/dist/packem_chunks/index.js +7 -873
  63. package/dist/packem_chunks/loader.js +1 -23
  64. package/dist/packem_chunks/tar.js +3 -0
  65. package/dist/packem_shared/ai-analysis-hm8d2W7z.js +67 -0
  66. package/dist/packem_shared/ai-cache-DoiF80AR.js +1 -0
  67. package/dist/packem_shared/ai-fix-nn4zOE95.js +43 -0
  68. package/dist/packem_shared/cache-directory-CwHlJhgx.js +1 -0
  69. package/dist/packem_shared/dependency-scan-COr5n63B.js +2 -0
  70. package/dist/packem_shared/docker-D6OGr5_S.js +2 -0
  71. package/dist/packem_shared/failure-log-iUVLf6ts.js +2 -0
  72. package/dist/packem_shared/flakiness-D9wf0t56.js +1 -0
  73. package/dist/packem_shared/giget-CcEy_Elm.js +2 -0
  74. package/dist/packem_shared/index-DH-5hsrC.js +1 -0
  75. package/dist/packem_shared/otel-DxDUPJJH.js +6 -0
  76. package/dist/packem_shared/otelPlugin-CQq6poq8.js +1 -0
  77. package/dist/packem_shared/registry-CkubDdiY.js +2 -0
  78. package/dist/packem_shared/run-summary-utils-BfBvjzhY.js +1 -0
  79. package/dist/packem_shared/runtime-check-BXZ43CBW.js +1 -0
  80. package/dist/packem_shared/selectors-BylODRiM.js +3 -0
  81. package/dist/packem_shared/symbols-CQmER5MT.js +1 -0
  82. package/dist/packem_shared/toolchain-BgBOUHII.js +5 -0
  83. package/dist/packem_shared/typosquats-CcZl99B1.js +1 -0
  84. package/dist/packem_shared/use-measured-height-DjYgUOKk.js +1 -0
  85. package/dist/packem_shared/utils-DrNg0XTR.js +1 -0
  86. package/dist/packem_shared/verify-Baj5mFJ7.js +1 -0
  87. package/dist/packem_shared/vis-update-app-D1jl0UZZ.js +1 -0
  88. package/dist/packem_shared/xxh3-DrAUNq4n.js +1 -0
  89. package/index.js +556 -727
  90. package/package.json +19 -29
  91. package/schemas/project.schema.json +739 -297
  92. package/schemas/vis-config.schema.json +3365 -384
  93. package/templates/buildkite-ci/template.yml +20 -20
  94. package/dist/packem_shared/VisUpdateApp-D-Yz_wvg.js +0 -1316
  95. package/dist/packem_shared/_commonjsHelpers-BqLXS_qQ.js +0 -5
  96. package/dist/packem_shared/ai-analysis-CHeB1joD.js +0 -367
  97. package/dist/packem_shared/ai-cache-Be_jexe4.js +0 -142
  98. package/dist/packem_shared/ai-fix-B9iQVcD2.js +0 -379
  99. package/dist/packem_shared/cache-directory-2qvs4goY.js +0 -98
  100. package/dist/packem_shared/catalog-BJTtyi-O.js +0 -1371
  101. package/dist/packem_shared/dependency-scan-A0KSklpG.js +0 -188
  102. package/dist/packem_shared/docker-2iZzc280.js +0 -181
  103. package/dist/packem_shared/failure-log-Cz3Z4SKL.js +0 -100
  104. package/dist/packem_shared/flakiness-goTxXuCX.js +0 -180
  105. package/dist/packem_shared/otel-DCvqCTz_.js +0 -158
  106. package/dist/packem_shared/otelPlugin-DFaLDvJf.js +0 -3
  107. package/dist/packem_shared/registry-CbqXI0rc.js +0 -272
  108. package/dist/packem_shared/run-summary-utils-PVMl4aIh.js +0 -130
  109. package/dist/packem_shared/runtime-check-Cobi3p6l.js +0 -127
  110. package/dist/packem_shared/selectors-SM69TfqC.js +0 -194
  111. package/dist/packem_shared/symbols-Ta7g2nU-.js +0 -14
  112. package/dist/packem_shared/toolchain-BdZd9eBi.js +0 -975
  113. package/dist/packem_shared/typosquats-C-bCh3PX.js +0 -1210
  114. package/dist/packem_shared/use-measured-height-CNP0vT4M.js +0 -20
  115. package/dist/packem_shared/utils-CthVdBPS.js +0 -40
  116. package/dist/packem_shared/xxh3-Ck8mXNg1.js +0 -239
@@ -1,2574 +1,25 @@
1
- import { createRequire as __cjs_createRequire } from "node:module";
2
-
3
- const __cjs_require = __cjs_createRequire(import.meta.url);
4
-
5
- const __cjs_getProcess = typeof globalThis !== "undefined" && typeof globalThis.process !== "undefined" ? globalThis.process : process;
6
-
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
-
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';
30
-
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
- ];
281
-
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 };
1
+ var he=Object.defineProperty;var D=(e,s)=>he(e,"name",{value:s,configurable:!0});import{createRequire as ke}from"node:module";import{green as w,bold as v,dim as u,cyan as R,yellow as x,red as b}from"@visulima/colorize";import{isAccessibleSync as I,readJsonSync as j,readFileSync as Pe,writeFileSync as ve,writeJsonSync as E,walkSync as U}from"@visulima/fs";import{join as W,resolve as K,relative as P}from"@visulima/path";import{o as re}from"../packem_shared/index-DH-5hsrC.js";import{aA as pe,aB as te,z as C,al as we,ak as L,aC as De,E as oe,aF as se,K as be,g as xe,aG as Ne,aH as Je,aI as Se,aJ as Te}from"./bin.js";const ye=ke(import.meta.url),z=typeof globalThis<"u"&&typeof globalThis.process<"u"?globalThis.process:process,$e=D(e=>{if(typeof z<"u"&&z.versions&&z.versions.node){const[s,o]=z.versions.node.split(".").map(Number);if(s>22||s===22&&o>=3||s===20&&o>=16)return z.getBuiltinModule(e)}return ye(e)},"__cjs_getBuiltinModule"),{readFileSync:ie,writeFileSync:ee}=$e("node:fs");var Re=Object.defineProperty,O=D((e,s)=>Re(e,"name",{value:s,configurable:!0}),"r$3");const We=O(e=>/[*?[\]{}!]/.test(e),"isGlob"),je=O(e=>typeof e=="string"?e:e.reason,"ruleReason"),Me=O(e=>typeof e=="string"?void 0:e.replacement,"ruleReplacement"),ae=O((e,s)=>e.some(o=>re(o,s)),"matchesAnyGlob"),ne=O((e,s)=>{if(typeof e=="string")return!0;const o=Array.isArray(e.packages)&&e.packages.length>0,t=Array.isArray(e.paths)&&e.paths.length>0;return!!(!o&&!t||o&&s.packageName!==void 0&&ae(e.packages,s.packageName)||t&&ae(e.paths,s.packageDir))},"ruleAppliesToInstance"),Fe=O((e,s)=>{const o=s[e.depName];if(o!==void 0&&ne(o,e))return{pattern:e.depName,rule:o};for(const[t,r]of Object.entries(s))if(We(t)&&re(t,e.depName)&&ne(r,e))return{pattern:t,rule:r}},"findMatchingRule"),Ie=O((e,s)=>{if(Object.keys(s).length===0)return[];const o=[];for(const t of e){if(t.isInternal)continue;const r=Fe(t,s);r&&o.push({depName:t.depName,depType:t.depType,matchedPattern:r.pattern,packageDir:t.packageDir,packageJsonPath:t.packageJsonPath,packageName:t.packageName,reason:je(r.rule),replacement:Me(r.rule),specifier:t.specifier})}return o},"lintBannedDeps");var Ee=Object.defineProperty,_=D((e,s)=>Ee(e,"name",{value:s,configurable:!0}),"m");const Ce=new Set(["dependencies","devDependencies","peerDependencies"]),Ae=_(e=>e.startsWith("catalog:"),"isCatalogReference"),Ve=_(e=>e.startsWith("workspace:"),"isWorkspaceReference"),Oe=_((e,s)=>{if(!e)return!1;for(const o of e.values())if(o.has(s))return!0;return!1},"isPinnedInAnyCatalog"),He=_((e,s={})=>{const o=s.min??3,t=new Set(s.ignoreDeps),r=e.filter(i=>i.isInternal||!Ce.has(i.depType)||Ve(i.specifier)||s.dep!==void 0&&i.depName!==s.dep?!1:!t.has(i.depName)),n=new Map;for(const i of r){const p=n.get(i.depName);p?p.push(i):n.set(i.depName,[i])}const a=[];for(const[i,p]of n){if(Oe(s.catalogs,i)||p.some(m=>Ae(m.specifier)))continue;const c=new Map;for(const m of p){let d=c.get(m.specifier);d||(d=new Set,c.set(m.specifier,d)),d.add(m.packageJsonPath)}const f=[...c.entries()].sort((m,d)=>m[1].size!==d[1].size?d[1].size-m[1].size:m[0].localeCompare(d[0]))[0];!f||f[1].size<o||a.push({catalogName:"default",depName:i,instanceCount:f[1].size,specifier:f[0]})}return a.sort((i,p)=>i.depName.localeCompare(p.depName))},"proposeCatalogAdditions"),_e=_((e,s)=>{if(s.length===0)return;const o=W(e,"pnpm-workspace.yaml"),t=I(o)?ie(o,"utf8"):"",r=t.includes(`\r
2
+ `)?`\r
3
+ `:`
4
+ `,n=s.filter(d=>d.catalogName==="default").sort((d,g)=>d.depName.localeCompare(g.depName));if(n.length===0)return;const a=t.length>0?t.split(r):[],i=a.findIndex(d=>/^catalog\s*:\s*$/.test(d));if(i===-1){const d=["catalog:"];for(const $ of n)d.push(` ${$.depName}: "${$.specifier}"`);let g=0;for(;g<a.length&&(a[g]??"").trim().length===0;)g+=1;const h=[...a.slice(0,g),...d,"",...a.slice(g)].join(r);return ee(o,h.endsWith(r)?h:`${h}${r}`),o}let p=a.length;const c=new Set;for(let d=i+1;d<a.length;d+=1){const g=a[d]??"",h=g.trimStart();if(g.length===0)continue;if(g.length-h.length===0&&h.length>0&&!h.startsWith("#")){p=d;break}const $=/^([\w./@-]+)\s*:/.exec(h);$?.[1]&&c.add($[1])}const f=[];for(const d of n)c.has(d.depName)||f.push(` ${d.depName}: "${d.specifier}"`);if(f.length===0)return;const m=[...a.slice(0,p),...f,...a.slice(p)].join(r);return ee(o,m.endsWith(r)?m:`${m}${r}`),o},"applyCatalogProposals"),ze=_((e,s)=>{if(s.length===0)return"";const o=W(e,"pnpm-workspace.yaml"),t=I(o)?ie(o,"utf8"):"",r=[...s].sort((i,p)=>i.depName.localeCompare(p.depName)),n=["--- pnpm-workspace.yaml","+++ pnpm-workspace.yaml"],a=t.includes(`\r
5
+ `)?`\r
6
+ `:`
7
+ `;if((t.length>0?t.split(a):[]).findIndex(i=>/^catalog\s*:\s*$/.test(i))===-1){n.push("@@ +1 @@","+catalog:");for(const i of r)n.push(`+ ${i.depName}: "${i.specifier}"`)}else{n.push("@@ catalog: @@");for(const i of r)n.push(`+ ${i.depName}: "${i.specifier}"`)}return n.join(a)},"renderCatalogProposalsDiff");var Be=Object.defineProperty,ce=D((e,s)=>Be(e,"name",{value:s,configurable:!0}),"l");const Le=ce(e=>{const s=[],o=pe(e);if(o){const r=W(e,"pnpm-workspace.yaml");for(const n of o)n.startsWith("!")||te(e,[n]).length===0&&s.push({pattern:n,source:"pnpm-workspace.yaml",sourcePath:r})}const t=W(e,"package.json");if(I(t)){const r=j(t).workspaces,n=Array.isArray(r)?r:r?.packages;if(n)for(const a of n)typeof a!="string"||a.startsWith("!")||te(e,[a]).length===0&&s.push({pattern:a,source:"package.json",sourcePath:t})}return s},"lintDeadWorkspacePatterns"),qe=ce((e,s={})=>{const{useEditorconfig:o}=s,t=new Map;for(const n of e){const a=t.get(n.sourcePath);a?a.push(n):t.set(n.sourcePath,[n])}const r=[];for(const[n,a]of t){const i=new Set(a.map(f=>f.pattern));if(n.endsWith(".yaml")||n.endsWith(".yml")){const f=Pe(n).split(`
8
+ `).filter(m=>{const d=m.trim();if(!d.startsWith("- "))return!0;const g=d.slice(2).replaceAll(/^['"]|['"]$/g,"");return!i.has(g)}).join(`
9
+ `);ve(n,f,{overwrite:!0}),r.push(n);continue}const p=j(n),c=p.workspaces;Array.isArray(c)?p.workspaces=c.filter(f=>typeof f!="string"||!i.has(f)):c&&Array.isArray(c.packages)&&(c.packages=c.packages.filter(f=>typeof f!="string"||!i.has(f))),E(n,p,{indent:C(n,{useEditorconfig:o}),overwrite:!0}),r.push(n)}return r},"applyDeadWorkspacePatternFixes");var Ge=Object.defineProperty,fe=D((e,s)=>Ge(e,"name",{value:s,configurable:!0}),"f$2");const Ue=["dependencies","devDependencies","peerDependencies","optionalDependencies"],Ke=fe((e,s={})=>{const o=new Set(s.ignoreBlocks),t=we(e),r=[];for(const n of t){const a=W(e,n,"package.json"),i=L(a);if(!i)continue;const p=typeof i.name=="string"?i.name:void 0;for(const c of Ue){if(o.has(c))continue;const f=i[c];typeof f=="object"&&f!==null&&!Array.isArray(f)&&Object.keys(f).length===0&&r.push({depType:c,packageDir:n,packageJsonPath:a,packageName:p})}}return r},"lintEmptyDeps"),Qe=fe((e,s={})=>{const{useEditorconfig:o}=s,t=new Map;for(const n of e){const a=t.get(n.packageJsonPath);a?a.push(n):t.set(n.packageJsonPath,[n])}const r=[];for(const[n,a]of t){const i=j(n);for(const p of a){const c=i[p.depType];typeof c=="object"&&c!==null&&!Array.isArray(c)&&Object.keys(c).length===0&&Reflect.deleteProperty(i,p.depType)}E(n,i,{indent:C(n,{useEditorconfig:o}),overwrite:!0}),r.push(n)}return r},"applyEmptyDepsFixes");var Xe=Object.defineProperty,le=D((e,s)=>Xe(e,"name",{value:s,configurable:!0}),"o");const Ye=/\/+$/,Q=/node_modules/,X=/\.git/,Ze=/[$()+.?[\\\]^{|}]/g,et=/\/\*\*$|\/\*\/\*$/,tt=le((e,s)=>{const o=s.replace(Ye,"");if(o.startsWith("!"))return[];const t=[];if(o.endsWith("/*")){const n=o.slice(0,-2),a=K(e,n);if(!I(a))return[];for(const i of U(a,{includeFiles:!1,includeSymlinks:!1,maxDepth:1,skip:[Q,X]}))i.path===a||i.name.startsWith(".")||t.push(W(n,i.name));return t}if(o.endsWith("/**")||o.endsWith("/*/*")){const n=o.replace(et,""),a=K(e,n);if(!I(a))return[];for(const i of U(a,{includeFiles:!1,includeSymlinks:!1,skip:[Q,X]})){if(i.path===a)continue;const p=i.path.slice(a.length+1);t.push(`${n}/${p}`)}return t}if(!o.includes("/")&&o.includes("*")){const n=o.replaceAll(Ze,"\\$&").replaceAll("*",".*"),a=new RegExp(`^${n}$`);for(const i of U(e,{includeFiles:!1,includeSymlinks:!1,maxDepth:1,skip:[Q,X]}))i.path!==e&&a.test(i.name)&&t.push(i.name);return t}const r=K(e,o);return I(r)&&t.push(o),t},"collectPatternMatches"),ot=le(e=>{const s=pe(e)??De(e)??[],o=new Set,t=[];for(const r of s)for(const n of tt(e,r))n==="."||o.has(n)||(o.add(n),I(W(e,n,"package.json"))||t.push({packageDir:n}));return t},"lintMissingPackageJson");var st=Object.defineProperty,at=D((e,s)=>st(e,"name",{value:s,configurable:!0}),"r$2");const nt=["dependencies","devDependencies","optionalDependencies","peerDependencies"],rt=at((e,s={})=>{const o=new Set(s.depTypes??nt),t=new Set(s.ignoreDeps),r=new Map;for(const a of e)a.packageDir!=="."||!o.has(a.depType)||r.set(a.depName,{depType:a.depType,specifier:a.specifier});if(r.size===0)return[];const n=[];for(const a of e){if(a.packageDir==="."||!o.has(a.depType)||t.has(a.depName))continue;const i=r.get(a.depName);i&&n.push({childSpecifier:a.specifier,depName:a.depName,depType:a.depType,packageDir:a.packageDir,packageJsonPath:a.packageJsonPath,packageName:a.packageName,rootDepType:i.depType,rootSpecifier:i.specifier})}return n},"lintRedefineRoot");var it=Object.defineProperty,ge=D((e,s)=>it(e,"name",{value:s,configurable:!0}),"a$1");const pt=ge((e,s)=>{if(!s)return[];const o=W(e,"package.json"),t=L(o);if(!t)return[];if(t.private!==!0)return[];const r=t.dependencies;if(typeof r!="object"||r===null||Array.isArray(r))return[];const n=Object.keys(r);return n.length===0?[]:[{depNames:n,packageJsonPath:o}]},"lintRootDeps"),ct=ge((e,s={})=>{const{useEditorconfig:o}=s,t=[];for(const r of e){const n=j(r.packageJsonPath),a=n.dependencies;if(typeof a!="object"||a===null)continue;const i=a;n.devDependencies??={};const p=n.devDependencies;for(const c of r.depNames){const f=i[c];typeof f=="string"&&(c in p||(p[c]=f),Reflect.deleteProperty(i,c))}Object.keys(i).length===0&&Reflect.deleteProperty(n,"dependencies"),E(r.packageJsonPath,n,{indent:C(r.packageJsonPath,{useEditorconfig:o}),overwrite:!0}),t.push(r.packageJsonPath)}return t},"applyRootDepsFixes");var ft=Object.defineProperty,de=D((e,s)=>ft(e,"name",{value:s,configurable:!0}),"r$1");const lt=/^[a-z][\w-]*@\S+$/i,gt=de((e,s,o={})=>{if(!s)return[];const t=W(e,"package.json"),r=L(t);if(!r)return[];const n=r.packageManager;return typeof n=="string"&&lt.test(n)?[]:[{packageJsonPath:t,suggested:o.suggested}]},"lintRootPackageManager"),dt=de((e,s={})=>{const{useEditorconfig:o}=s,t=[];for(const r of e){if(!r.suggested)continue;const n=j(r.packageJsonPath);n.packageManager=r.suggested,E(r.packageJsonPath,n,{indent:C(r.packageJsonPath,{useEditorconfig:o}),overwrite:!0}),t.push(r.packageJsonPath)}return t},"applyRootPackageManagerFixes");var ut=Object.defineProperty,ue=D((e,s)=>ut(e,"name",{value:s,configurable:!0}),"a");const mt=ue((e,s)=>{if(!s)return[];const o=W(e,"package.json"),t=L(o);return t?t.private===!0?[]:[{packageJsonPath:o,rawValue:t.private}]:[]},"lintRootPrivate"),ht=ue((e,s={})=>{const{useEditorconfig:o}=s,t=[];for(const r of e){const n=j(r.packageJsonPath);let a=n;if("private"in n)n.private=!0;else{const{name:i,version:p,...c}=n,f={};i!==void 0&&(f.name=i),p!==void 0&&(f.version=p),f.private=!0;for(const[m,d]of Object.entries(c))f[m]=d;a=f}E(r.packageJsonPath,a,{indent:C(r.packageJsonPath,{useEditorconfig:o}),overwrite:!0}),t.push(r.packageJsonPath)}return t},"applyRootPrivateFixes");var kt=Object.defineProperty,Z=D((e,s)=>kt(e,"name",{value:s,configurable:!0}),"p$1");const yt=[{id:"react",label:"React",members:["react","react-dom","react-test-renderer"]},{id:"next",label:"Next.js",members:["next","@next/font","@next/bundle-analyzer","@next/mdx","@next/third-parties","@next/eslint-plugin-next","eslint-config-next"]},{id:"babel",label:"Babel",prefixes:["@babel/"]},{id:"storybook",label:"Storybook",members:["storybook","sb"],prefixes:["@storybook/"]},{id:"vitest",label:"Vitest",members:["vitest"],prefixes:["@vitest/"]},{id:"playwright",label:"Playwright",members:["playwright","@playwright/test"]},{id:"trpc",label:"tRPC",prefixes:["@trpc/"]},{id:"prisma",label:"Prisma",members:["prisma"],prefixes:["@prisma/"]},{id:"turborepo",label:"Turborepo",members:["turbo","turbo-ignore","@turbo/gen","eslint-config-turbo","eslint-plugin-turbo"]},{id:"typescript-eslint",label:"typescript-eslint",members:["typescript-eslint"],prefixes:["@typescript-eslint/"]},{id:"eslint-stylistic",label:"ESLint Stylistic",prefixes:["@stylistic/"]},{id:"lexical",label:"Lexical",members:["lexical"],prefixes:["@lexical/"]},{id:"nx",label:"Nx",prefixes:["@nx/","@nrwl/"]}],$t=new Set(["dependencies","devDependencies","peerDependencies"]),Pt=Z((e,s)=>{for(const o of e)if(o.members?.includes(s)||o.prefixes?.some(t=>s.startsWith(t)))return o},"familyForDep"),vt=Z(e=>e.startsWith("workspace:")||e.startsWith("catalog:"),"isWorkspaceOrCatalogReference"),wt=Z((e,s={})=>{const o=new Set(s.ignoreFamilies),t=new Map;for(const i of yt)t.set(i.id,i);for(const i of s.extraFamilies??[])t.set(i.id,i);const r=[...t.values()],n=new Map;for(const i of e){if(i.isInternal||!$t.has(i.depType)||vt(i.specifier))continue;const p=Pt(r,i.depName);if(!p||o.has(p.id))continue;const c=n.get(p.id),f={depName:i.depName,depType:i.depType,packageDir:i.packageDir,packageJsonPath:i.packageJsonPath,packageName:i.packageName,specifier:i.specifier};c?c.push(f):n.set(p.id,[f])}const a=[];for(const[i,p]of n){const c=[...new Set(p.map(m=>m.specifier))];if(c.length<2)continue;const f=t.get(i);f&&a.push({family:i,familyLabel:f.label??i,members:p,specifiers:c})}return a},"lintSimilarDeps");var Dt=Object.defineProperty,B=D((e,s)=>Dt(e,"name",{value:s,configurable:!0}),"r");const bt=B(e=>e.startsWith("@types/"),"isTypesPackage"),xt=B((e,s={})=>{const o=new Set(s.ignoreDeps),t=new Map,r=B(a=>{const i=t.get(a);if(i!==void 0)return i;try{const p=j(a).private===!0;return t.set(a,p),p}catch{return t.set(a,!1),!1}},"isPrivate"),n=[];for(const a of e)a.depType==="dependencies"&&bt(a.depName)&&(o.has(a.depName)||r(a.packageJsonPath)&&n.push({childSpecifier:a.specifier,depName:a.depName,packageDir:a.packageDir,packageJsonPath:a.packageJsonPath,packageName:a.packageName}));return n},"lintTypesInDeps"),Nt=B((e,s={})=>{const{useEditorconfig:o}=s,t=new Map;for(const n of e){const a=t.get(n.packageJsonPath);a?a.push(n):t.set(n.packageJsonPath,[n])}const r=[];for(const[n,a]of t){const i=j(n),p=i.dependencies;if(typeof p!="object"||p===null)continue;const c=p;i.devDependencies??={};const f=i.devDependencies;for(const m of a){const d=c[m.depName];typeof d=="string"&&(m.depName in f||(f[m.depName]=d),Reflect.deleteProperty(c,m.depName))}Object.keys(c).length===0&&Reflect.deleteProperty(i,"dependencies"),E(n,i,{indent:C(n,{useEditorconfig:o}),overwrite:!0}),r.push(n)}return r},"applyTypesInDepsFixes");var Jt=Object.defineProperty,q=D((e,s)=>Jt(e,"name",{value:s,configurable:!0}),"f");const St=q(e=>e.startsWith("workspace:"),"isWorkspaceSpecifier"),Tt=q((e,s={})=>{const o=s.fixSpecifier??"workspace:*",t=[];for(const r of e)r.isInternal&&(St(r.specifier)||t.push({depName:r.depName,depType:r.depType,fix:o,packageDir:r.packageDir,packageJsonPath:r.packageJsonPath,packageName:r.packageName,specifier:r.specifier}));return t},"lintWorkspaceProtocol"),Rt=q((e,s,o,t)=>{const r=s.split(".");let n=e;for(let p=0;p<r.length-1;p+=1){const c=r[p],f=n[c];(typeof f!="object"||f===null)&&(n[c]={}),n=n[c]}const a=r.at(-1);let i=n[a];(typeof i!="object"||i===null)&&(i={},n[a]=i),i[o]=t},"setNestedField"),Wt=q((e,s={})=>{const{useEditorconfig:o}=s,t=new Map;for(const n of e){const a=t.get(n.packageJsonPath);a?a.push(n):t.set(n.packageJsonPath,[n])}const r=[];for(const[n,a]of t){const i=j(n);for(const p of a)if(p.depType.includes("."))Rt(i,p.depType,p.depName,p.fix);else{const c=i[p.depType];typeof c=="object"&&c!==null&&(c[p.depName]=p.fix)}E(n,i,{indent:C(n,{useEditorconfig:o}),overwrite:!0}),r.push(n)}return r},"applyWorkspaceProtocolFixes");var jt=Object.defineProperty,A=D((e,s)=>jt(e,"name",{value:s,configurable:!0}),"p");const Mt=new Set(["dependencies","devDependencies","peerDependencies"]),Ft=A(e=>e.startsWith("catalog:"),"isCatalogReference"),It=A(e=>e.startsWith("workspace:"),"isWorkspaceReference"),Et=A(e=>{if(!e.startsWith("catalog:"))return;const s=e.slice(8);return s===""?"default":s},"catalogNameOf"),Ct=A((e,s)=>{if(e.get("default")?.has(s))return"default";const o=[...e.keys()].filter(t=>t!=="default").sort();for(const t of o)if(e.get(t)?.has(s))return t},"findCatalogPinning"),At=A(e=>e==="default"?"catalog:":`catalog:${e}`,"buildCatalogSpecifier"),Vt=A((e,s)=>{const o=[...e].sort((r,n)=>(r.packageName??r.packageDir).localeCompare(n.packageName??n.packageDir));let t;for(const r of o){const n=oe(r.specifier);if(!n)continue;if(!t){t=r;continue}const a=oe(t.specifier);if(!a){t=r;continue}const i=se(a,n),p=se(n,a);(s==="highest"&&i||s==="lowest"&&p)&&(t=r)}if(t)return{canonical:t,canonicalSource:t.packageName??t.packageDir}},"pickCanonicalBySemver"),Ot=A((e,s={})=>{const o=s.resolve??"highest",t=new Set(s.ignoreDeps),r=[],n=e.filter(i=>i.isInternal||!Mt.has(i.depType)||It(i.specifier)||s.dep!==void 0&&i.depName!==s.dep?!1:!t.has(i.depName)),a=new Map;for(const i of n){const p=a.get(i.depName);p?p.push(i):a.set(i.depName,[i])}for(const[i,p]of a){const c=s.pinned?.get(i);if(c!==void 0){for(const g of p)g.specifier!==c&&r.push({canonicalSource:"cli:--pin",depName:i,depType:g.depType,fix:c,packageDir:g.packageDir,packageJsonPath:g.packageJsonPath,packageName:g.packageName,specifier:g.specifier});continue}if(o==="catalog"){const{catalogs:g}=s;if(!g)continue;const h=Ct(g,i);if(!h)continue;const $=At(h);for(const S of p)Et(S.specifier)!==h&&r.push({canonicalSource:`catalog:${h}`,depName:i,depType:S.depType,fix:$,packageDir:S.packageDir,packageJsonPath:S.packageJsonPath,packageName:S.packageName,specifier:S.specifier});continue}const f=p.filter(g=>!Ft(g.specifier));if(f.length<2||new Set(f.map(g=>g.specifier)).size<=1)continue;const m=Vt(f,o);if(!m)continue;const d=m.canonical.specifier;for(const g of f)g.specifier!==d&&r.push({canonicalSource:m.canonicalSource,depName:i,depType:g.depType,fix:d,packageDir:g.packageDir,packageJsonPath:g.packageJsonPath,packageName:g.packageName,specifier:g.specifier})}return r},"lintWorkspaceVersions"),Ht=A((e,s={})=>{const{useEditorconfig:o}=s,t=new Map;for(const n of e){const a=t.get(n.packageJsonPath);a?a.push(n):t.set(n.packageJsonPath,[n])}const r=[];for(const[n,a]of t){const i=j(n);for(const p of a){const c=i[p.depType];typeof c=="object"&&c!==null&&(c[p.depName]=p.fix)}E(n,i,{indent:C(n,{useEditorconfig:o}),overwrite:!0}),r.push(n)}return r},"applyWorkspaceVersionsFixes");var _t=Object.defineProperty,y=D((e,s)=>_t(e,"name",{value:s,configurable:!0}),"u");const zt=y(e=>{if(I(W(e,"pnpm-workspace.yaml")))return!0;const s=W(e,"package.json");if(!I(s))return!1;try{return j(s).workspaces!==void 0}catch{return!1}},"detectWorkspaceConfig"),V=y((e,s)=>{const o=new Map;for(const t of e){const r=s(t),n=o.get(r);n?n.push(t):o.set(r,[t])}return o},"groupBy"),Bt=y((e,s,o,t)=>{if(e.length===0){t.info(w("✓ workspace-protocol: no violations"));return}const r=o?"Fixed":"Found",n=o?R:x;t.info(n(v(`${r} ${String(e.length)} workspace-protocol violation${e.length===1?"":"s"}`)));for(const[a,i]of V(e,p=>p.packageName??p.packageJsonPath)){const p=P(s,i[0].packageJsonPath);t.info(` ${v(a)} ${u(`(${p})`)}`);for(const c of i){const f=o?R("→"):x("→");t.info(` ${u(c.depType)} ${c.depName}: ${b(c.specifier)} ${f} ${w(c.fix)}`)}}o||t.info(u(" Run with --fix to rewrite specifiers in place."))},"printWorkspaceProtocolHuman"),Lt=y((e,s,o)=>{if(e.length===0){o.info(w("✓ redefine-root: no violations"));return}o.info(x(v(`Found ${String(e.length)} dep${e.length===1?"":"s"} re-declared from root`)));for(const[t,r]of V(e,n=>n.packageName??n.packageJsonPath)){const n=P(s,r[0].packageJsonPath);o.info(` ${v(t)} ${u(`(${n})`)}`);for(const a of r)o.info(` ${u(a.depType)} ${a.depName}: ${b(a.childSpecifier)} ${u(`(root ${a.rootDepType}: ${a.rootSpecifier})`)}`)}o.info(u(" Remove these from child package.json files — root pin will resolve."))},"printRedefineRootHuman"),qt=y((e,s,o,t)=>{if(e.length===0){t.info(w("✓ workspace-versions: no drift"));return}const r=o?"Fixed":"Found",n=o?R:x;t.info(n(v(`${r} ${String(e.length)} workspace-version drift${e.length===1?"":"s"}`)));for(const[a,i]of V(e,p=>p.depName)){const p=i[0].fix,c=i[0].canonicalSource;t.info(` ${v(a)} ${u(`canonical: ${p} (from ${c})`)}`);for(const f of i){const m=P(s,f.packageJsonPath),d=f.packageName??m,g=o?R("→"):x("→");t.info(` ${d} ${u(`(${m})`)} ${u(f.depType)}: ${b(f.specifier)} ${g} ${w(f.fix)}`)}}o||t.info(u(" Run with --fix to align drifting specifiers."))},"printWorkspaceVersionsHuman"),Gt=y((e,s,o)=>{if(e.length===0){o.info(w("✓ banned-deps: no violations"));return}o.info(b(v(`Found ${String(e.length)} banned dep${e.length===1?"":"s"}`)));for(const[t,r]of V(e,n=>n.packageName??n.packageJsonPath)){const n=P(s,r[0].packageJsonPath);o.info(` ${v(t)} ${u(`(${n})`)}`);for(const a of r){const i=a.replacement?` ${u("→")} ${w(a.replacement)}`:"";o.info(` ${u(a.depType)} ${b(a.depName)}${i}`),o.info(` ${u(a.reason)}`)}}},"printBannedDepsHuman"),Ut=y((e,s,o,t)=>{if(e.length===0){t.info(w("✓ catalog-proposals: nothing worth promoting"));return}const r=o?"Added":"Would add",n=o?R:x;t.info(n(v(`${r} ${String(e.length)} catalog entr${e.length===1?"y":"ies"}`)));for(const a of e)t.info(` ${v(a.depName)}: ${w(a.specifier)} ${u(`(${String(a.instanceCount)} packages agree)`)}`);if(!o){const a=ze(s,e);if(a){t.info(""),t.info(u("Proposed pnpm-workspace.yaml changes:"));for(const i of a.split(`
10
+ `))i.startsWith("+")?t.info(w(i)):i.startsWith("-")?t.info(b(i)):t.info(u(i))}t.info(u(" Run with --fix to write these entries to pnpm-workspace.yaml."))}},"printCatalogProposalsHuman"),Kt=y((e,s,o,t)=>{if(e.length===0){t.info(w("✓ custom-types: no engines / packageManager / volta drift"));return}const r=o?"Fixed":"Found",n=o?R:x;t.info(n(v(`${r} ${String(e.length)} custom-type drift${e.length===1?"":"s"}`)));for(const[a,i]of V(e,p=>`${p.customType} ${p.depName}`)){const p=i[0].fix,c=i[0].canonicalSource;t.info(` ${v(a)} ${u(`canonical: ${p} (from ${c})`)}`);for(const f of i){const m=P(s,f.packageJsonPath),d=f.packageName??m,g=o?R("→"):x("→");t.info(` ${d} ${u(`(${m})`)}: ${b(f.specifier)} ${g} ${w(f.fix)}`)}}o||t.info(u(" Run with --fix to align engines/packageManager/volta versions."))},"printCustomTypesHuman"),Qt=y((e,s,o,t)=>{if(e.length===0){t.info(w("✓ empty-deps: no empty dependency blocks"));return}const r=o?"Removed":"Found",n=o?R:x;t.info(n(v(`${r} ${String(e.length)} empty dependency block${e.length===1?"":"s"}`)));for(const[a,i]of V(e,p=>p.packageName??p.packageJsonPath)){const p=P(s,i[0].packageJsonPath);t.info(` ${v(a)} ${u(`(${p})`)}`);for(const c of i)t.info(` ${u(c.depType)}: ${b("{}")}`)}o||t.info(u(" Run with --fix to drop empty blocks."))},"printEmptyDepsHuman"),Xt=y((e,s,o,t)=>{if(e.length===0){t.info(w('✓ root-private: root package.json is "private": true'));return}const r=o?"Set":"Missing",n=o?R:b;for(const a of e){const i=P(s,a.packageJsonPath);if(t.info(n(v(`${r} "private": true on root ${u(`(${i})`)}`))),!o){const p=a.rawValue===void 0?"absent":JSON.stringify(a.rawValue);t.info(` ${u("current:")} ${b(p)}`)}}o||t.info(u(' Run with --fix to set "private": true.'))},"printRootPrivateHuman"),Yt=y((e,s,o,t)=>{if(e.length===0){t.info(w("✓ root-package-manager: packageManager field present"));return}const r=o?"Set":"Missing",n=o?R:b;for(const a of e){const i=P(s,a.packageJsonPath);t.info(n(v(`${r} packageManager on root ${u(`(${i})`)}`))),!o&&!a.suggested&&t.info(u(" no canonical specifier configured (set policy.rootPackageManager.suggested to enable --fix)"))}o||t.info(u(' e.g. "packageManager": "pnpm@10.32.1"'))},"printRootPackageManagerHuman"),Zt=y((e,s,o,t)=>{if(e.length===0){t.info(w("✓ root-deps: no runtime dependencies on private root"));return}const r=o?"Moved":"Found",n=o?R:x;for(const a of e){const i=P(s,a.packageJsonPath);t.info(n(v(`${r} ${String(a.depNames.length)} runtime dep${a.depNames.length===1?"":"s"} on private root ${u(`(${i})`)}`)));for(const p of a.depNames)t.info(` ${b(p)}`)}o||t.info(u(" Run with --fix to move them to devDependencies."))},"printRootDepsHuman"),eo=y((e,s)=>{if(e.length===0){s.info(w("✓ missing-package-json: every workspace dir has a package.json"));return}s.info(x(v(`Found ${String(e.length)} workspace dir${e.length===1?"":"s"} without a package.json`)));for(const o of e)s.info(` ${b(o.packageDir)}`);s.info(u(" Either delete the directory or scaffold a package.json (vis create)."))},"printMissingPackageJsonHuman"),to=y((e,s,o)=>{if(e.length===0){o.info(w("✓ dead-workspace-pattern: every workspace pattern matches at least one package"));return}const t=s?"Removed":"Found",r=s?R:x;o.info(r(v(`${t} ${String(e.length)} unmatched workspace pattern${e.length===1?"":"s"}`)));for(const[n,a]of V(e,i=>i.source)){o.info(` ${v(n)}`);for(const i of a)o.info(` ${b(i.pattern)}`)}s||o.info(u(" Run with --fix to drop dead patterns."))},"printDeadWorkspacePatternsHuman"),oo=y((e,s,o,t)=>{if(e.length===0){t.info(w("✓ types-in-deps: no @types/* in dependencies of private packages"));return}const r=o?"Moved":"Found",n=o?R:x;t.info(n(v(`${r} ${String(e.length)} @types/* dep${e.length===1?"":"s"} in dependencies`)));for(const[a,i]of V(e,p=>p.packageName??p.packageJsonPath)){const p=P(s,i[0].packageJsonPath);t.info(` ${v(a)} ${u(`(${p})`)}`);for(const c of i)t.info(` ${b(c.depName)} ${u(c.childSpecifier)}`)}o||t.info(u(" Run with --fix to move them to devDependencies."))},"printTypesInDepsHuman"),so=y((e,s,o)=>{if(e.length===0){o.info(w("✓ similar-deps: every related dep family is in sync"));return}o.info(x(v(`Found ${String(e.length)} family${e.length===1?"":" families"} with version drift`)));for(const t of e){o.info(` ${v(t.familyLabel)} ${u(`(${t.specifiers.join(", ")})`)}`);for(const r of t.members){const n=P(s,r.packageJsonPath),a=r.packageName??n;o.info(` ${a} ${u(`(${n})`)} ${u(r.depType)}: ${b(r.depName)}@${x(r.specifier)}`)}}o.info(u(" Pick a single specifier per family and align by hand — auto-fix is unsafe across name boundaries."))},"printSimilarDepsHuman"),ao=y((e,s,o,t,r)=>{let n=!0;const a=y(i=>{n||r.info(""),n=!1,i()},"section");t.workspaceProtocol&&a(()=>{Bt(e.workspaceProtocol??[],s,o.workspaceProtocol,r)}),t.redefineRoot&&a(()=>{Lt(e.redefineRoot??[],s,r)}),t.workspaceVersions&&a(()=>{qt(e.workspaceVersions??[],s,o.workspaceVersions,r)}),t.customTypes&&a(()=>{Kt(e.customTypes??[],s,o.customTypes,r)}),e.catalogProposals!==void 0&&a(()=>{Ut(e.catalogProposals??[],s,o.catalogProposals,r)}),t.bannedDeps&&a(()=>{Gt(e.bannedDeps??[],s,r)}),t.emptyDeps&&a(()=>{Qt(e.emptyDeps??[],s,o.emptyDeps,r)}),t.rootPrivate&&a(()=>{Xt(e.rootPrivate??[],s,o.rootPrivate,r)}),t.rootPackageManager&&a(()=>{Yt(e.rootPackageManager??[],s,o.rootPackageManager,r)}),t.rootDeps&&a(()=>{Zt(e.rootDeps??[],s,o.rootDeps,r)}),t.missingPackageJson&&a(()=>{eo(e.missingPackageJson??[],r)}),t.deadWorkspacePatterns&&a(()=>{to(e.deadWorkspacePatterns??[],o.deadWorkspacePatterns,r)}),t.typesInDeps&&a(()=>{oo(e.typesInDeps??[],s,o.typesInDeps,r)}),t.similarDeps&&a(()=>{so(e.similarDeps??[],s,r)})},"printHuman"),no=y((e,s)=>{for(const o of e.workspaceProtocol??[]){const t=P(s,o.packageJsonPath);process.stdout.write(`workspace-protocol ${t} ${o.depType} ${o.depName} ${o.specifier} → ${o.fix}
11
+ `)}for(const o of e.redefineRoot??[]){const t=P(s,o.packageJsonPath);process.stdout.write(`redefine-root ${t} ${o.depType} ${o.depName} ${o.childSpecifier}
12
+ `)}for(const o of e.workspaceVersions??[]){const t=P(s,o.packageJsonPath);process.stdout.write(`workspace-versions ${t} ${o.depType} ${o.depName} ${o.specifier} ${o.fix}
13
+ `)}for(const o of e.customTypes??[]){const t=P(s,o.packageJsonPath);process.stdout.write(`custom-types ${t} ${o.customType} ${o.depName} ${o.specifier} → ${o.fix}
14
+ `)}for(const o of e.bannedDeps??[]){const t=P(s,o.packageJsonPath);process.stdout.write(`banned-deps ${t} ${o.depType} ${o.depName} ${o.reason}
15
+ `)}for(const o of e.catalogProposals??[])process.stdout.write(`catalog-proposal ${o.catalogName} ${o.depName} ${o.specifier} ${String(o.instanceCount)}
16
+ `);for(const o of e.emptyDeps??[]){const t=P(s,o.packageJsonPath);process.stdout.write(`empty-deps ${t} ${o.depType}
17
+ `)}for(const o of e.rootPrivate??[]){const t=P(s,o.packageJsonPath);process.stdout.write(`root-private ${t}
18
+ `)}for(const o of e.rootPackageManager??[]){const t=P(s,o.packageJsonPath);process.stdout.write(`root-package-manager ${t} ${o.suggested??""}
19
+ `)}for(const o of e.rootDeps??[]){const t=P(s,o.packageJsonPath);for(const r of o.depNames)process.stdout.write(`root-deps ${t} ${r}
20
+ `)}for(const o of e.missingPackageJson??[])process.stdout.write(`missing-package-json ${o.packageDir}
21
+ `);for(const o of e.deadWorkspacePatterns??[])process.stdout.write(`dead-workspace-pattern ${o.source} ${o.pattern}
22
+ `);for(const o of e.typesInDeps??[]){const t=P(s,o.packageJsonPath);process.stdout.write(`types-in-deps ${t} ${o.depName} ${o.childSpecifier}
23
+ `)}for(const o of e.similarDeps??[])for(const t of o.members){const r=P(s,t.packageJsonPath);process.stdout.write(`similar-deps ${o.family} ${r} ${t.depType} ${t.depName} ${t.specifier}
24
+ `)}},"printMinimal"),ro=y((e,s,o,t)=>{const r=y(a=>({...a,packageJsonPath:P(s,a.packageJsonPath)}),"relativize"),n={fixed:o};if(t.workspaceProtocol){const a=(e.workspaceProtocol??[]).map(i=>r(i));n.workspaceProtocol={issues:a,total:a.length}}if(t.redefineRoot){const a=(e.redefineRoot??[]).map(i=>r(i));n.redefineRoot={issues:a,total:a.length}}if(t.workspaceVersions){const a=(e.workspaceVersions??[]).map(i=>r(i));n.workspaceVersions={issues:a,total:a.length}}if(t.customTypes){const a=(e.customTypes??[]).map(i=>r(i));n.customTypes={issues:a,total:a.length}}if(t.bannedDeps){const a=(e.bannedDeps??[]).map(i=>r(i));n.bannedDeps={issues:a,total:a.length}}if(e.catalogProposals!==void 0){const a=e.catalogProposals;n.catalogProposals={proposals:a,total:a.length}}if(t.emptyDeps){const a=(e.emptyDeps??[]).map(i=>r(i));n.emptyDeps={issues:a,total:a.length}}if(t.rootPrivate){const a=(e.rootPrivate??[]).map(i=>r(i));n.rootPrivate={issues:a,total:a.length}}if(t.rootPackageManager){const a=(e.rootPackageManager??[]).map(i=>r(i));n.rootPackageManager={issues:a,total:a.length}}if(t.rootDeps){const a=(e.rootDeps??[]).map(i=>r(i));n.rootDeps={issues:a,total:a.length}}if(t.missingPackageJson){const a=e.missingPackageJson??[];n.missingPackageJson={issues:a,total:a.length}}if(t.deadWorkspacePatterns){const a=e.deadWorkspacePatterns??[];n.deadWorkspacePatterns={issues:a,total:a.length}}if(t.typesInDeps){const a=(e.typesInDeps??[]).map(i=>r(i));n.typesInDeps={issues:a,total:a.length}}if(t.similarDeps){const a=(e.similarDeps??[]).map(i=>({...i,members:i.members.map(p=>({...p,packageJsonPath:P(s,p.packageJsonPath)}))}));n.similarDeps={issues:a,total:a.length}}process.stdout.write(`${JSON.stringify(n,void 0,2)}
25
+ `)},"printJson"),T=y((e,s,o)=>{const t=e[s];return typeof t=="boolean"?t:e[o]===!0},"flag"),io=y(e=>{const s=e,o=(e.ban?.length??0)>0,t=(e.pin?.length??0)>0,r=T(s,"workspaceProtocol","workspace-protocol"),n=T(s,"redefineRoot","redefine-root"),a=T(s,"bannedDeps","banned-deps"),i=T(s,"workspaceVersions","workspace-versions"),p=T(s,"customTypes","custom-types"),c=T(s,"emptyDeps","empty-deps"),f=T(s,"rootPrivate","root-private"),m=T(s,"rootPackageManager","root-package-manager"),d=T(s,"rootDeps","root-deps"),g=T(s,"missingPackageJson","missing-package-json"),h=T(s,"deadWorkspacePatterns","dead-workspace-patterns"),$=T(s,"typesInDeps","types-in-deps"),S=T(s,"similarDeps","similar-deps");return r||n||a||i||p||c||f||m||d||g||h||$||S||o||t?{bannedDeps:a||o,customTypes:p,deadWorkspacePatterns:h,emptyDeps:c,missingPackageJson:g,redefineRoot:n,rootDeps:d,rootPackageManager:m,rootPrivate:f,similarDeps:S,typesInDeps:$,workspaceProtocol:r,workspaceVersions:i||t}:{bannedDeps:!0,customTypes:!0,deadWorkspacePatterns:!0,emptyDeps:!0,missingPackageJson:!0,redefineRoot:!0,rootDeps:!0,rootPackageManager:!0,rootPrivate:!0,similarDeps:!0,typesInDeps:!0,workspaceProtocol:!0,workspaceVersions:!0}},"resolveSelection"),po=y(e=>{const s=new Map;for(const o of e??[]){const t=o.lastIndexOf("@");if(t<=0||t===o.length-1)throw new Error(`Invalid --pin "${o}". Use: name@<specifier> (e.g. react@^18.2.0).`);const r=o.slice(0,t),n=o.slice(t+1);s.set(r,n)}return s},"parsePinFlags"),co=new Set(["catalog","highest","lowest"]),fo=y(e=>{if(e===void 0)return"highest";if(!co.has(e))throw new Error(`Invalid --resolve "${e}". Use: highest, lowest, or catalog.`);return e},"parseResolveStrategy"),F=y((e,s)=>e?s===void 0||s===!0:!1,"isAutofixAllowed"),lo={"custom-types":"policy.customTypes.autofix","workspace-protocol":"policy.workspaceProtocol.autofix","workspace-versions":"policy.workspaceVersions.autofix"},Y=y((e,s,o,t)=>{const r=lo[s],n=o==="prompt"?`${r} = "prompt" (interactive mode not yet implemented; report-only)`:`${r} = false`,a=`Set "${r}": true (or remove it) to enable rewrites.`;e.warn(`${s}: ${String(t)} issue${t===1?"":"s"} not rewritten — ${n}. ${a}`)},"warnAutofixDenied"),Po=y(async({logger:e,options:s,visConfig:o,workspaceRoot:t})=>{if(!t)throw new Error("Could not determine workspace root. Run this command inside a monorepo.");const r=t,n=s.fix??!1,a=s.format??"human",i=s.quiet??!1;if(!["human","json","minimal"].includes(a))throw new Error(`Invalid --format "${a}". Use: human, json, or minimal.`);const p=io(s),c=o?.policy??{},f=o?.editorconfig??!0,m=po(s.pin),d=s.ban??[];(s.dep!==void 0||s.resolve!==void 0)&&!p.workspaceVersions&&!i&&e.warn("--dep / --resolve only apply to --workspace-versions; ignored.");const g=be(r),h={},$={catalogProposals:!1,customTypes:!1,deadWorkspacePatterns:!1,emptyDeps:!1,rootDeps:!1,rootPackageManager:!1,rootPrivate:!1,typesInDeps:!1,workspaceProtocol:!1,workspaceVersions:!1},S=zt(r);let N=0;if(p.workspaceProtocol){const l=Tt(g,{fixSpecifier:s.fixSpecifier}),k=F(n,c.workspaceProtocol?.autofix);k&&l.length>0&&(Wt(l,{useEditorconfig:f}),$.workspaceProtocol=!0),h.workspaceProtocol=l,k||(N+=l.length),n&&!k&&l.length>0&&!i&&Y(e,"workspace-protocol",c.workspaceProtocol?.autofix,l.length)}if(p.redefineRoot){const l=rt(g,{ignoreDeps:c.redefineRoot?.ignore});h.redefineRoot=l,N+=l.length}if(p.workspaceVersions){const l=fo(s.resolve??c.workspaceVersions?.resolve),k=l==="catalog"?xe(r):void 0;l==="catalog"&&(!k||k.size===0)&&!i&&e.warn("--resolve catalog: no catalog found in pnpm-workspace.yaml or root package.json — nothing to align.");const J=Ot(g,{catalogs:k,dep:s.dep,ignoreDeps:c.workspaceVersions?.ignore,pinned:m.size>0?m:void 0,resolve:l}),H=F(n,c.workspaceVersions?.autofix);if(H&&J.length>0&&(Ht(J,{useEditorconfig:f}),$.workspaceVersions=!0),h.workspaceVersions=J,H||(N+=J.length),n&&!H&&J.length>0&&!i&&Y(e,"workspace-versions",c.workspaceVersions?.autofix,J.length),s.proposeMin!==void 0){if(l!=="catalog"&&!i)e.warn("--propose-min only runs under --resolve catalog; ignored.");else if(l==="catalog"){const M=He(g,{catalogs:k,ignoreDeps:c.workspaceVersions?.ignore,min:s.proposeMin});H&&M.length>0&&(_e(r,M),$.catalogProposals=!0),h.catalogProposals=M}}}if(p.customTypes){const l=c.customTypes?.extraTypes,k=Ne(l);if(k.length>0){for(const me of k)e.error(`policy.customTypes.${me}`);process.exitCode=1;return}const J=Je(r,l),H=(s.resolve??c.customTypes?.resolve)==="lowest"?"lowest":"highest",M=Se(J,{dep:s.dep,ignoreDeps:c.customTypes?.ignore,resolve:H}),G=F(n,c.customTypes?.autofix);G&&M.length>0&&(Te(M,{useEditorconfig:f}),$.customTypes=!0),h.customTypes=M,G||(N+=M.length),n&&!G&&M.length>0&&!i&&Y(e,"custom-types",c.customTypes?.autofix,M.length)}if(p.bannedDeps){const l={...c.bannedDeps};for(const J of d)l[J]={reason:"banned via --ban CLI flag"};Object.keys(l).length===0&&s.bannedDeps&&!i&&e.warn("--banned-deps: no policy.bannedDeps in vis config, nothing to check.");const k=Ie(g,l);h.bannedDeps=k,N+=k.length}if(p.emptyDeps){const l=Ke(r,{ignoreBlocks:c.emptyDeps?.ignoreBlocks}),k=F(n,c.emptyDeps?.autofix);k&&l.length>0&&(Qe(l,{useEditorconfig:f}),$.emptyDeps=!0),h.emptyDeps=l,k||(N+=l.length)}if(p.rootPrivate){const l=mt(r,S),k=F(n,c.rootPrivate?.autofix);k&&l.length>0&&(ht(l,{useEditorconfig:f}),$.rootPrivate=!0),h.rootPrivate=l,k||(N+=l.length)}if(p.rootPackageManager){const l=gt(r,S,{suggested:c.rootPackageManager?.suggested}),k=F(n,c.rootPackageManager?.autofix);k&&l.some(J=>J.suggested!==void 0)&&(dt(l,{useEditorconfig:f}),$.rootPackageManager=!0),h.rootPackageManager=l,(!k||!$.rootPackageManager)&&(N+=l.filter(J=>J.suggested===void 0||!$.rootPackageManager).length)}if(p.rootDeps){const l=pt(r,S),k=F(n,c.rootDeps?.autofix);k&&l.length>0&&(ct(l,{useEditorconfig:f}),$.rootDeps=!0),h.rootDeps=l,k||(N+=l.length)}if(p.missingPackageJson){const l=ot(r);h.missingPackageJson=l,N+=l.length}if(p.deadWorkspacePatterns){const l=Le(r),k=F(n,c.deadWorkspacePatterns?.autofix);k&&l.length>0&&(qe(l,{useEditorconfig:f}),$.deadWorkspacePatterns=!0),h.deadWorkspacePatterns=l,k||(N+=l.length)}if(p.typesInDeps){const l=xt(g,{ignoreDeps:c.typesInDeps?.ignore}),k=F(n,c.typesInDeps?.autofix);k&&l.length>0&&(Nt(l,{useEditorconfig:f}),$.typesInDeps=!0),h.typesInDeps=l,k||(N+=l.length)}if(p.similarDeps){const l=wt(g,{extraFamilies:c.similarDeps?.extraFamilies,ignoreFamilies:c.similarDeps?.ignoreFamilies});h.similarDeps=l,N+=l.length}i||(a==="json"?ro(h,r,$,p):a==="minimal"?no(h,r):ao(h,r,$,p,e)),N>0&&(process.exitCode=1)},"execute");export{Po as default};