@flisk-sh/flisk 0.0.0-stage → 1.3.189

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 (440) hide show
  1. package/CHANGELOG.md +492 -0
  2. package/LICENSE +21 -0
  3. package/README.md +479 -2
  4. package/Tiltfile +151 -0
  5. package/cli/bin/flisk.js +42 -0
  6. package/cli/dist/agent-runtimes/custom.js +335 -0
  7. package/cli/dist/agent-runtimes/registry.js +34 -0
  8. package/cli/dist/backend-frameworks/elysia.js +38 -0
  9. package/cli/dist/backend-frameworks/express.js +49 -0
  10. package/cli/dist/backend-frameworks/fastify.js +43 -0
  11. package/cli/dist/backend-frameworks/h3.js +37 -0
  12. package/cli/dist/backend-frameworks/koa.js +47 -0
  13. package/cli/dist/backend-frameworks/mcp.js +86 -0
  14. package/cli/dist/backend-frameworks/nestjs.js +55 -0
  15. package/cli/dist/backend-frameworks/registry.js +134 -0
  16. package/cli/dist/backend-frameworks/types.js +1 -0
  17. package/cli/dist/backend-languages/bun.js +76 -0
  18. package/cli/dist/backend-languages/go.js +125 -0
  19. package/cli/dist/backend-languages/node.js +122 -0
  20. package/cli/dist/backend-languages/python.js +94 -0
  21. package/cli/dist/backend-languages/registry.js +37 -0
  22. package/cli/dist/backend-languages/rust.js +82 -0
  23. package/cli/dist/backend-languages/types.js +1 -0
  24. package/cli/dist/cli.js +29 -0
  25. package/cli/dist/commands/__tests__/project-e2e-helpers.js +49 -0
  26. package/cli/dist/commands/__tests__/ui-loading-screen.test.js +16 -0
  27. package/cli/dist/commands/agent.js +182 -0
  28. package/cli/dist/commands/completion-candidates.js +116 -0
  29. package/cli/dist/commands/completion.js +843 -0
  30. package/cli/dist/commands/config.js +362 -0
  31. package/cli/dist/commands/doctor.js +1504 -0
  32. package/cli/dist/commands/down.js +114 -0
  33. package/cli/dist/commands/eject.js +108 -0
  34. package/cli/dist/commands/help.js +152 -0
  35. package/cli/dist/commands/import.js +164 -0
  36. package/cli/dist/commands/logs.js +119 -0
  37. package/cli/dist/commands/maintainers.js +102 -0
  38. package/cli/dist/commands/mcp.js +28 -0
  39. package/cli/dist/commands/networks.js +306 -0
  40. package/cli/dist/commands/project.js +447 -0
  41. package/cli/dist/commands/projects.js +103 -0
  42. package/cli/dist/commands/registry.js +53 -0
  43. package/cli/dist/commands/resource.js +992 -0
  44. package/cli/dist/commands/resources.js +127 -0
  45. package/cli/dist/commands/restart.js +89 -0
  46. package/cli/dist/commands/runtime.js +75 -0
  47. package/cli/dist/commands/stack.js +171 -0
  48. package/cli/dist/commands/stacks.js +71 -0
  49. package/cli/dist/commands/status.js +211 -0
  50. package/cli/dist/commands/ui.js +725 -0
  51. package/cli/dist/commands/up.js +609 -0
  52. package/cli/dist/commands/upgrade.js +840 -0
  53. package/cli/dist/commands/version.js +10 -0
  54. package/cli/dist/components/Accessible.js +11 -0
  55. package/cli/dist/components/BaseTooltip.js +30 -0
  56. package/cli/dist/components/DetailPanel.js +33 -0
  57. package/cli/dist/components/FileTree.js +51 -0
  58. package/cli/dist/components/ResourceSelectInput.js +42 -0
  59. package/cli/dist/components/ResourceTable.js +35 -0
  60. package/cli/dist/components/ServiceIssues.js +13 -0
  61. package/cli/dist/components/TUIHeader.js +27 -0
  62. package/cli/dist/components/TabBar.js +27 -0
  63. package/cli/dist/components/Tooltip.js +13 -0
  64. package/cli/dist/components/index.js +11 -0
  65. package/cli/dist/components/ui-theme.js +120 -0
  66. package/cli/dist/config/platform-standards.js +182 -0
  67. package/cli/dist/frontend-frameworks/lit.js +64 -0
  68. package/cli/dist/frontend-frameworks/preact.js +62 -0
  69. package/cli/dist/frontend-frameworks/qwik.js +61 -0
  70. package/cli/dist/frontend-frameworks/react.js +69 -0
  71. package/cli/dist/frontend-frameworks/registry.js +83 -0
  72. package/cli/dist/frontend-frameworks/solid.js +62 -0
  73. package/cli/dist/frontend-frameworks/svelte.js +67 -0
  74. package/cli/dist/frontend-frameworks/tanstack-router.js +101 -0
  75. package/cli/dist/frontend-frameworks/types.js +1 -0
  76. package/cli/dist/frontend-frameworks/vanilla.js +59 -0
  77. package/cli/dist/frontend-frameworks/vue.js +72 -0
  78. package/cli/dist/generator/__tests__/golden-helpers.js +43 -0
  79. package/cli/dist/generator/extension-fetch.js +291 -0
  80. package/cli/dist/generator/resource-templates.js +133 -0
  81. package/cli/dist/generator/template-engine.js +801 -0
  82. package/cli/dist/index.js +10 -0
  83. package/cli/dist/mcp/server.js +101 -0
  84. package/cli/dist/mcp/tools.js +351 -0
  85. package/cli/dist/program.js +37 -0
  86. package/cli/dist/types/index.js +38 -0
  87. package/cli/dist/utils/agent-host.js +36 -0
  88. package/cli/dist/utils/atomic-write.js +36 -0
  89. package/cli/dist/utils/bun-floor.js +9 -0
  90. package/cli/dist/utils/bun-requirement.js +10 -0
  91. package/cli/dist/utils/cache.js +63 -0
  92. package/cli/dist/utils/cold-preflight.js +257 -0
  93. package/cli/dist/utils/command-helpers.js +57 -0
  94. package/cli/dist/utils/constants.js +160 -0
  95. package/cli/dist/utils/discovery-context.js +71 -0
  96. package/cli/dist/utils/discovery-paths.js +93 -0
  97. package/cli/dist/utils/docker.js +34 -0
  98. package/cli/dist/utils/doctor-crashloop.js +161 -0
  99. package/cli/dist/utils/doctor-fix.js +391 -0
  100. package/cli/dist/utils/doctor-host.js +66 -0
  101. package/cli/dist/utils/doctor-report.js +78 -0
  102. package/cli/dist/utils/doctor-runtime.js +880 -0
  103. package/cli/dist/utils/doctor-starlark.js +132 -0
  104. package/cli/dist/utils/doctor-wiring.js +698 -0
  105. package/cli/dist/utils/down-cleanup.js +125 -0
  106. package/cli/dist/utils/env-validator.js +266 -0
  107. package/cli/dist/utils/errors.js +175 -0
  108. package/cli/dist/utils/exec-async.js +31 -0
  109. package/cli/dist/utils/file-helpers.js +104 -0
  110. package/cli/dist/utils/formatting.js +165 -0
  111. package/cli/dist/utils/generated-paths.js +51 -0
  112. package/cli/dist/utils/help-command.js +11 -0
  113. package/cli/dist/utils/host-port-config.js +140 -0
  114. package/cli/dist/utils/host-port-plan.js +100 -0
  115. package/cli/dist/utils/json-output.js +29 -0
  116. package/cli/dist/utils/machine-output.js +33 -0
  117. package/cli/dist/utils/paths.js +43 -0
  118. package/cli/dist/utils/platform.js +58 -0
  119. package/cli/dist/utils/port-assignment.js +75 -0
  120. package/cli/dist/utils/port-conflict-fix.js +25 -0
  121. package/cli/dist/utils/port-fallback-notice.js +17 -0
  122. package/cli/dist/utils/port-listener-probe.js +20 -0
  123. package/cli/dist/utils/project-config-defaults.js +17 -0
  124. package/cli/dist/utils/project-features.js +91 -0
  125. package/cli/dist/utils/project-templates.js +32 -0
  126. package/cli/dist/utils/prompt.js +97 -0
  127. package/cli/dist/utils/resource-features.js +184 -0
  128. package/cli/dist/utils/resource-kind.js +9 -0
  129. package/cli/dist/utils/schema-version-gate.js +45 -0
  130. package/cli/dist/utils/search-status.js +12 -0
  131. package/cli/dist/utils/secret-redaction.js +73 -0
  132. package/cli/dist/utils/service-config-checks.js +189 -0
  133. package/cli/dist/utils/service-manifest.js +254 -0
  134. package/cli/dist/utils/service-runtime-state.js +66 -0
  135. package/cli/dist/utils/service-urls.js +112 -0
  136. package/cli/dist/utils/services.js +360 -0
  137. package/cli/dist/utils/shared-infra.js +45 -0
  138. package/cli/dist/utils/shared-platform-postgres.js +232 -0
  139. package/cli/dist/utils/single-flight.js +21 -0
  140. package/cli/dist/utils/smoke.js +401 -0
  141. package/cli/dist/utils/spinner.js +40 -0
  142. package/cli/dist/utils/stack-features.js +15 -0
  143. package/cli/dist/utils/stack-status.js +14 -0
  144. package/cli/dist/utils/startup-report.js +78 -0
  145. package/cli/dist/utils/status-message.js +21 -0
  146. package/cli/dist/utils/status-ports.js +33 -0
  147. package/cli/dist/utils/suggestions.js +63 -0
  148. package/cli/dist/utils/tar.js +46 -0
  149. package/cli/dist/utils/tdk-version.js +51 -0
  150. package/cli/dist/utils/terminal-layout.js +19 -0
  151. package/cli/dist/utils/tilt-events.js +194 -0
  152. package/cli/dist/utils/tilt-logs.js +61 -0
  153. package/cli/dist/utils/tilt-port-owner.js +63 -0
  154. package/cli/dist/utils/tilt-process.js +54 -0
  155. package/cli/dist/utils/tilt-resource-state.js +32 -0
  156. package/cli/dist/utils/tilt-startup.js +49 -0
  157. package/cli/dist/utils/tilt-unreachable-message.js +9 -0
  158. package/cli/dist/utils/tilt.js +104 -0
  159. package/cli/dist/utils/ui-keymap.js +129 -0
  160. package/cli/dist/utils/ui-service-state.js +34 -0
  161. package/cli/dist/utils/up-only.js +28 -0
  162. package/cli/dist/utils/up-readiness.js +118 -0
  163. package/cli/dist/utils/url-actions.js +103 -0
  164. package/cli/dist/utils/validation.js +62 -0
  165. package/cli/dist/utils/which.js +56 -0
  166. package/cli/dist/utils/windows-doctor.js +176 -0
  167. package/cli/dist-bundle/chunk-6EA4DGCY.js +2 -0
  168. package/cli/dist-bundle/chunk-BZ4H7MM4.js +3 -0
  169. package/cli/dist-bundle/chunk-QWYXYAX6.js +510 -0
  170. package/cli/dist-bundle/cli.js +2017 -0
  171. package/cli/dist-bundle/cold-preflight-MM46GFQA.js +2 -0
  172. package/cli/dist-bundle/devtools-ZUHXW33W.js +5 -0
  173. package/cli/dist-bundle/extension-fetch-J4FFH7TJ.js +2 -0
  174. package/cli/package.json +83 -0
  175. package/cli/shared-platform-engineering/docker-templates/bun-hoisted-symlink-fix.sh +25 -0
  176. package/cli/shared-platform-engineering/docker-templates/infisical-entrypoint.sh +466 -0
  177. package/cli/shared-platform-engineering/docker-templates/install-deps.sh +154 -0
  178. package/cli/shared-platform-engineering/docker-templates/prisma-bun-client-link-fix.sh +14 -0
  179. package/cli/shared-platform-engineering/docker-templates/prisma-normalize-client.sh +58 -0
  180. package/cli/templates/.tiltignore.hbs +72 -0
  181. package/cli/templates/TILT_RESOURCE_DEFAULTS.star.hbs +220 -0
  182. package/cli/templates/TILT_TECH_STACK.star.hbs +149 -0
  183. package/cli/templates/Tiltfile.hbs +381 -0
  184. package/cli/templates/spec.master.hbs +192 -0
  185. package/discovery/AGENTS.md +362 -0
  186. package/discovery/TILT_DISCOVERY.star +429 -0
  187. package/discovery/Tiltfile +58 -0
  188. package/discovery/config.star +265 -0
  189. package/discovery/constants.star +167 -0
  190. package/discovery/deterministic.star +213 -0
  191. package/discovery/discovery_daemon.star +193 -0
  192. package/discovery/discovery_orchestrator.star +492 -0
  193. package/discovery/filtering.star +7 -0
  194. package/discovery/identity-management-backend/service.yaml +26 -0
  195. package/discovery/identity-management-frontend/service.yaml +29 -0
  196. package/discovery/identity-sdk/service.yaml +9 -0
  197. package/discovery/incremental_discovery.star +215 -0
  198. package/discovery/indexing.star +9 -0
  199. package/discovery/json_manifest_scanner.star +57 -0
  200. package/discovery/libraries.star +48 -0
  201. package/discovery/loading.star +25 -0
  202. package/discovery/manifest/constants.star +106 -0
  203. package/discovery/manifest/loading.star +418 -0
  204. package/discovery/manifest/normalize.star +289 -0
  205. package/discovery/manifest/validation.star +110 -0
  206. package/discovery/normalization.star +13 -0
  207. package/discovery/registry.star +562 -0
  208. package/discovery/resource_snapshot.py +384 -0
  209. package/discovery/resource_snapshot.star +145 -0
  210. package/discovery/schemas.star +3 -0
  211. package/discovery/services/platform/cdc/debezium/nats-connector/service.yaml +9 -0
  212. package/discovery/services/platform/cdc/nats-http-bridge/service.yaml +9 -0
  213. package/discovery/services/platform/orchestrator/orchestrator-glue-backend/service.yaml +26 -0
  214. package/discovery/services/platform/synthetic-monitor/service.yaml +9 -0
  215. package/discovery/test_resource_snapshot.py +253 -0
  216. package/discovery/validation.star +80 -0
  217. package/discovery/versioning.star +3 -0
  218. package/engine/AGENTS.md +706 -0
  219. package/engine/README.md +31 -0
  220. package/engine/REFACTORING_MANIFEST_SYSTEM.md +974 -0
  221. package/engine/Tiltfile +86 -0
  222. package/engine/assets/docker/golden-layers.Dockerfile +204 -0
  223. package/engine/assets/docker/wake-gateway/Dockerfile +28 -0
  224. package/engine/assets/docker/wake-gateway/decide.test.ts +177 -0
  225. package/engine/assets/docker/wake-gateway/decide.ts +135 -0
  226. package/engine/assets/docker/wake-gateway/server.ts +363 -0
  227. package/engine/assets/scripts/cleanup-completed-migrator-images.sh +39 -0
  228. package/engine/assets/scripts/cleanup-tilt-images.sh +122 -0
  229. package/engine/assets/scripts/mac-m1-resource-monitor.sh +123 -0
  230. package/engine/config.star +5 -0
  231. package/engine/entrypoint.star +101 -0
  232. package/engine/lifecycle/orchestrator.star +263 -0
  233. package/engine/paths.star +719 -0
  234. package/engine/schemas/project-schema.json +147 -0
  235. package/engine/schemas/service-schema.json +537 -0
  236. package/engine/scripts/README.md +89 -0
  237. package/engine/scripts/build-and-publish-lib.sh +207 -0
  238. package/engine/scripts/check-lib-publish.sh +87 -0
  239. package/engine/scripts/debug-container.sh +265 -0
  240. package/engine/scripts/generate-manifest.sh +200 -0
  241. package/engine/spec.master +702 -0
  242. package/engine/spec.master.example +341 -0
  243. package/engine/topologies/README.md +33 -0
  244. package/engine/topologies/platform/docker/AGENTS.md +59 -0
  245. package/engine/topologies/platform/docker/auth/auth.star +207 -0
  246. package/engine/topologies/platform/docker/build/frontend_build_plan.star +65 -0
  247. package/engine/topologies/platform/docker/build/golden_image.star +37 -0
  248. package/engine/topologies/platform/docker/build/golden_image_build.star +144 -0
  249. package/engine/topologies/platform/docker/build/golden_image_constants.star +100 -0
  250. package/engine/topologies/platform/docker/build/golden_image_dockerfile.star +263 -0
  251. package/engine/topologies/platform/docker/compose/compose.star +637 -0
  252. package/engine/topologies/platform/docker/compose/compose_helpers.star +54 -0
  253. package/engine/topologies/platform/docker/compose/traefik_standalone.star +171 -0
  254. package/engine/topologies/platform/docker/config/bunignore.star +144 -0
  255. package/engine/topologies/platform/docker/config/dockerignore.star +152 -0
  256. package/engine/topologies/platform/docker/config/healthcheck.star +58 -0
  257. package/engine/topologies/platform/docker/constants.star +211 -0
  258. package/engine/topologies/platform/docker/dockerfile/dockerfile.star +56 -0
  259. package/engine/topologies/platform/docker/dockerfile/language_dockerfile.star +264 -0
  260. package/engine/topologies/platform/docker/generators/golden_docker_generator_v2.star +76 -0
  261. package/engine/topologies/platform/docker/index.star +40 -0
  262. package/engine/topologies/platform/docker/layers/bun_helpers.star +25 -0
  263. package/engine/topologies/platform/docker/layers/infisical/index.star +42 -0
  264. package/engine/topologies/platform/docker/layers/infisical/infisical_docker.star +264 -0
  265. package/engine/topologies/platform/docker/layers/l1_base_layers.star +89 -0
  266. package/engine/topologies/platform/docker/layers/l2_dependency_layers.star +147 -0
  267. package/engine/topologies/platform/docker/layers/l3_builder_layers.star +229 -0
  268. package/engine/topologies/platform/docker/layers/l4_runtime_layers.star +282 -0
  269. package/engine/topologies/platform/docker/layers/prisma/constants.star +138 -0
  270. package/engine/topologies/platform/docker/layers/prisma/db_readiness.star +98 -0
  271. package/engine/topologies/platform/docker/layers/prisma/filesystem_setup.star +131 -0
  272. package/engine/topologies/platform/docker/layers/prisma/migrate_script.star +100 -0
  273. package/engine/topologies/platform/docker/layers/prisma/prisma_build.star +128 -0
  274. package/engine/topologies/platform/docker/layers/prisma/prisma_runtime.star +205 -0
  275. package/engine/topologies/platform/docker/layers/prisma/secret_resolution.star +121 -0
  276. package/engine/topologies/platform/docker/networking/api_path_constants.star +206 -0
  277. package/engine/topologies/platform/docker/networking/sablier_container_cycle.star +31 -0
  278. package/engine/topologies/platform/docker/networking/traefik.star +266 -0
  279. package/engine/topologies/platform/docker/networking/traefik_constants.star +148 -0
  280. package/engine/topologies/platform/docker/networking/traefik_helpers.star +162 -0
  281. package/engine/topologies/platform/docker/networking/traefik_static_routes.star +165 -0
  282. package/engine/topologies/platform/docker/secrets/env_secrets.star +74 -0
  283. package/engine/topologies/platform/docker/secrets/infisical.star +109 -0
  284. package/engine/topologies/platform/net/AGENTS.md +64 -0
  285. package/engine/topologies/platform/net/proxy.star +6 -0
  286. package/engine/topologies/platform/registries/local.star +6 -0
  287. package/engine/topologies/platform/registries/verdaccio_loader.star +20 -0
  288. package/engine/topologies/platform/security/AGENTS.md +49 -0
  289. package/engine/topologies/platform/security/secrets.star +241 -0
  290. package/engine/topologies/platform/state/AGENTS.md +64 -0
  291. package/engine/topologies/platform/state/relational.star +6 -0
  292. package/engine/topologies/tilt/common/AGENTS.md +59 -0
  293. package/engine/topologies/tilt/common/utils.star +409 -0
  294. package/engine/topologies/tilt/common/utils_debug.star +71 -0
  295. package/engine/topologies/tilt/common/utils_deterministic.star +170 -0
  296. package/engine/topologies/tilt/common/utils_docker_networks.star +54 -0
  297. package/engine/topologies/tilt/common/utils_env.star +92 -0
  298. package/engine/topologies/tilt/common/utils_output.star +99 -0
  299. package/engine/topologies/tilt/config/AGENTS.md +68 -0
  300. package/engine/topologies/tilt/config/defaults.star +5 -0
  301. package/engine/topologies/tilt/config/feature_flags.star +12 -0
  302. package/engine/topologies/tilt/config/global.star +57 -0
  303. package/engine/topologies/tilt/config/profiles.star +398 -0
  304. package/engine/topologies/tilt/generators/agents_md.star +64 -0
  305. package/engine/topologies/tilt/generators/api_gateway.star +147 -0
  306. package/engine/topologies/tilt/generators/index.star +126 -0
  307. package/engine/topologies/tilt/generators/infisical/README.md +424 -0
  308. package/engine/topologies/tilt/generators/infisical/examples.star +314 -0
  309. package/engine/topologies/tilt/generators/infisical/index.star +59 -0
  310. package/engine/topologies/tilt/generators/infisical/machine_identity.star +422 -0
  311. package/engine/topologies/tilt/generators/infisical/organization.star +536 -0
  312. package/engine/topologies/tilt/generators/infisical/path_manager.star +478 -0
  313. package/engine/topologies/tilt/generators/infisical/secrets_generator.star +422 -0
  314. package/engine/topologies/tilt/generators/nginx_conf.star +145 -0
  315. package/engine/topologies/tilt/generators/npmrc.star +132 -0
  316. package/engine/topologies/tilt/generators/tsconfig.star +25 -0
  317. package/engine/topologies/tilt/generators/typescript/backend_library_tsconfig.star +73 -0
  318. package/engine/topologies/tilt/generators/typescript/backend_tsconfig.star +137 -0
  319. package/engine/topologies/tilt/generators/typescript/common.star +9 -0
  320. package/engine/topologies/tilt/generators/typescript/frontend_library_tsconfig.star +78 -0
  321. package/engine/topologies/tilt/generators/typescript/frontend_tsconfig.star +140 -0
  322. package/engine/topologies/tilt/generators/typescript/include.star +25 -0
  323. package/engine/topologies/tilt/generators/typescript/playwright_config.star +42 -0
  324. package/engine/topologies/tilt/generators/typescript/prisma_config.star +51 -0
  325. package/engine/topologies/tilt/generators/validators.star +868 -0
  326. package/engine/topologies/tilt/generators/vite/backend.star +80 -0
  327. package/engine/topologies/tilt/generators/vite/frameworks/lit.star +92 -0
  328. package/engine/topologies/tilt/generators/vite/frameworks/preact.star +94 -0
  329. package/engine/topologies/tilt/generators/vite/frameworks/qwik.star +94 -0
  330. package/engine/topologies/tilt/generators/vite/frameworks/react.star +128 -0
  331. package/engine/topologies/tilt/generators/vite/frameworks/solid.star +94 -0
  332. package/engine/topologies/tilt/generators/vite/frameworks/svelte.star +94 -0
  333. package/engine/topologies/tilt/generators/vite/frameworks/vanilla.star +92 -0
  334. package/engine/topologies/tilt/generators/vite/frameworks/vue.star +94 -0
  335. package/engine/topologies/tilt/generators/vite/frontend.star +153 -0
  336. package/engine/topologies/tilt/generators/vite/generators.star +45 -0
  337. package/engine/topologies/tilt/generators/vite/helpers.star +256 -0
  338. package/engine/topologies/tilt/generators/vite/library.star +67 -0
  339. package/engine/topologies/tilt/generators/vite/sdk.star +75 -0
  340. package/engine/topologies/tilt/generators/vite/templates.star +460 -0
  341. package/engine/topologies/tilt/generators/vite_config.star +24 -0
  342. package/engine/topologies/tilt/manifest/AGENTS.md +61 -0
  343. package/engine/topologies/tilt/manifest/README.md +679 -0
  344. package/engine/topologies/tilt/manifest/__init__.star +220 -0
  345. package/engine/topologies/tilt/manifest/constants.star +262 -0
  346. package/engine/topologies/tilt/manifest/errors.star +268 -0
  347. package/engine/topologies/tilt/manifest/integration.star +348 -0
  348. package/engine/topologies/tilt/manifest/json_to_yaml.py +61 -0
  349. package/engine/topologies/tilt/manifest/loader.star +239 -0
  350. package/engine/topologies/tilt/manifest/mapper.star +229 -0
  351. package/engine/topologies/tilt/manifest/parser.star +490 -0
  352. package/engine/topologies/tilt/manifest/schema.star +502 -0
  353. package/engine/topologies/tilt/manifest/tests/test_integration.star +278 -0
  354. package/engine/topologies/tilt/manifest/tests/test_loader_parser.star +475 -0
  355. package/engine/topologies/tilt/manifest/tests/test_manifest.star +319 -0
  356. package/engine/topologies/tilt/manifest/tests/test_validator.star +472 -0
  357. package/engine/topologies/tilt/manifest/validator.star +744 -0
  358. package/engine/topologies/tilt/manifest/yaml_to_json.py +20 -0
  359. package/engine/topologies/tilt/resources/AGENTS.md +71 -0
  360. package/engine/topologies/tilt/resources/build_concurrency.star +56 -0
  361. package/engine/topologies/tilt/resources/conditions.star +5 -0
  362. package/engine/topologies/tilt/resources/databases.star +246 -0
  363. package/engine/topologies/tilt/resources/declaration.star +5 -0
  364. package/engine/topologies/tilt/resources/deps.star +5 -0
  365. package/engine/topologies/tilt/resources/infra-loader.star +504 -0
  366. package/engine/topologies/tilt/resources/libs.star +32 -0
  367. package/engine/topologies/tilt/resources/libs_discovery.star +104 -0
  368. package/engine/topologies/tilt/resources/libs_paths.star +81 -0
  369. package/engine/topologies/tilt/resources/libs_resource_definition.star +153 -0
  370. package/engine/topologies/tilt/resources/libs_setup.star +166 -0
  371. package/engine/topologies/tilt/resources/libs_tsconfig.star +39 -0
  372. package/engine/topologies/tilt/resources/orchestrator/apply.star +79 -0
  373. package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +864 -0
  374. package/engine/topologies/tilt/resources/orchestrator/apply_manifest_orchestration.star +90 -0
  375. package/engine/topologies/tilt/resources/orchestrator/apply_migrator_orchestration.star +150 -0
  376. package/engine/topologies/tilt/resources/orchestrator/apply_resource_validation.star +38 -0
  377. package/engine/topologies/tilt/resources/orchestrator/apply_runtime_flags.star +46 -0
  378. package/engine/topologies/tilt/resources/orchestrator/builders/typescript.star +193 -0
  379. package/engine/topologies/tilt/resources/orchestrator/generators/dependencies/AGENTS.md +75 -0
  380. package/engine/topologies/tilt/resources/orchestrator/generators/dependencies/index.star +23 -0
  381. package/engine/topologies/tilt/resources/orchestrator/generators/dependencies/resolver.star +136 -0
  382. package/engine/topologies/tilt/resources/orchestrator/generators/env.star +219 -0
  383. package/engine/topologies/tilt/resources/orchestrator/generators/frontend.star +172 -0
  384. package/engine/topologies/tilt/resources/orchestrator/generators/manifest_resource.star +577 -0
  385. package/engine/topologies/tilt/resources/orchestrator/helpers.star +35 -0
  386. package/engine/topologies/tilt/resources/orchestrator.star +56 -0
  387. package/engine/topologies/tilt/resources/ordering.star +5 -0
  388. package/engine/topologies/tilt/resources/shared-platform-postgres.star +30 -0
  389. package/engine/topologies/tilt/resources/triggers.star +5 -0
  390. package/engine/topologies/tilt/resources/watchers.star +304 -0
  391. package/engine/where-things-live.star +719 -0
  392. package/examples/tdk-example/LICENSE +21 -0
  393. package/examples/tdk-example/README.md +23 -0
  394. package/examples/tdk-example/TILT_SERVICE_DEFAULTS.star +42 -0
  395. package/examples/tdk-example/TILT_TECH_STACK.star +67 -0
  396. package/examples/tdk-example/Tiltfile +25 -0
  397. package/examples/tdk-example/services/platform/messaging/docker-compose.yml +26 -0
  398. package/examples/tdk-example/services/shop/orders-api/package.json +20 -0
  399. package/examples/tdk-example/services/shop/orders-api/service.json +14 -0
  400. package/examples/tdk-example/services/shop/orders-api/src/index.ts +81 -0
  401. package/examples/tdk-example/services/shop/orders-app/index.html +12 -0
  402. package/examples/tdk-example/services/shop/orders-app/package.json +23 -0
  403. package/examples/tdk-example/services/shop/orders-app/public/.gitkeep +0 -0
  404. package/examples/tdk-example/services/shop/orders-app/service.json +9 -0
  405. package/examples/tdk-example/services/shop/orders-app/src/main.tsx +9 -0
  406. package/examples/tdk-example/services/shop/orders-app/src/orders.tsx +67 -0
  407. package/examples/tdk-example/services/shop/orders-app/src/style.css +15 -0
  408. package/examples/tdk-example/services/shop/orders-worker/package.json +18 -0
  409. package/examples/tdk-example/services/shop/orders-worker/service.json +13 -0
  410. package/examples/tdk-example/services/shop/orders-worker/src/index.ts +33 -0
  411. package/ext/Tiltfile +66 -0
  412. package/ext/ide-components/README.md +221 -0
  413. package/ext/ide-components/code_viewer/server.py +208 -0
  414. package/ext/ide-components/config_inspector/server.py +1052 -0
  415. package/ext/ide-components/file_browser/server.py +287 -0
  416. package/ext/ide-components/shared/config_service.py +471 -0
  417. package/ext/ide-components/shared/file_utils.py +300 -0
  418. package/ext/ide-components/shared/http_utils.py +234 -0
  419. package/ext/ide-components/shared/static/styles.css +1334 -0
  420. package/ext/ide-components/shared/syntax_highlighter.py +142 -0
  421. package/ext/ide-components/shared/template_engine.py +125 -0
  422. package/ext/ide-components/shared/templates/base.html +121 -0
  423. package/ext/ide-components/shared/test_config_service.py +328 -0
  424. package/ext/ide-components/shared/tilt_integration.py +160 -0
  425. package/ext/ui-enhancements/cron-status-server.sh +51 -0
  426. package/ext/ui-enhancements/cron-status-tracker.sh +148 -0
  427. package/ext/ui-enhancements/extension.yaml +36 -0
  428. package/ext/ui-enhancements/tiltfile.py +101 -0
  429. package/ext/ui-enhancements/ui/cron-jobs-tab.css +396 -0
  430. package/ext/ui-enhancements/ui/help-panel.css +279 -0
  431. package/ext/ui-enhancements/ui/icons.css +125 -0
  432. package/ext/ui-enhancements/ui/tooltips.css +114 -0
  433. package/package.json +94 -4
  434. package/shared-platform-engineering/docker-templates/bun-hoisted-symlink-fix.sh +25 -0
  435. package/shared-platform-engineering/docker-templates/infisical-entrypoint.sh +466 -0
  436. package/shared-platform-engineering/docker-templates/install-deps.sh +154 -0
  437. package/shared-platform-engineering/docker-templates/prisma-bun-client-link-fix.sh +14 -0
  438. package/shared-platform-engineering/docker-templates/prisma-normalize-client.sh +58 -0
  439. package/specs/specs/TILT_SERVICE_DEFAULTS.star +207 -0
  440. package/specs/specs/TILT_TECH_STACK.star +161 -0
@@ -0,0 +1,1052 @@
1
+ #!/usr/bin/env python3
2
+ # Copyright (c) 2026 Flisk Landscape contributors
3
+ # SPDX-License-Identifier: MIT
4
+ """
5
+ Tilt IDE Config Inspector
6
+ Compare master configs and auto-generated outputs at http://localhost:9767
7
+
8
+ Features:
9
+ - Multi-pane comparison (1-3 configs side-by-side)
10
+ - Edit mode with safe file saving
11
+ - Sync scroll support
12
+ - Keyboard shortcuts (Ctrl+S, Ctrl+Shift+S, Ctrl+E)
13
+ """
14
+
15
+ import json
16
+ import os
17
+ import secrets
18
+ import sys
19
+ import urllib.parse
20
+ from pathlib import Path
21
+ from typing import Optional, Tuple
22
+
23
+ # Add shared modules
24
+ sys.path.insert(0, str(Path(__file__).parent.parent / 'shared'))
25
+ from file_utils import FileUtils, PROJECT_ROOT
26
+ from config_service import ConfigService
27
+ from http_utils import BaseIDEHandler, run_server
28
+
29
+ PORT = 9767
30
+
31
+
32
+ class ConfigInspectorHandler(BaseIDEHandler):
33
+ """HTTP request handler for config inspector"""
34
+
35
+ # CSRF token storage (in-memory, per-server-instance)
36
+ csrf_tokens = set()
37
+
38
+ # Master configs allowed at project root
39
+ ALLOWED_ROOT_CONFIGS = [
40
+ 'TILT_RESOURCE_DEFAULTS.star',
41
+ 'spec.master',
42
+ 'engine/spec.master',
43
+ ]
44
+
45
+ def generate_csrf_token(self) -> str:
46
+ """Generate and store a new CSRF token"""
47
+ token = secrets.token_urlsafe(32)
48
+ self.csrf_tokens.add(token)
49
+ # Keep token set from growing too large
50
+ if len(self.csrf_tokens) > 1000:
51
+ self.csrf_tokens.clear()
52
+ return token
53
+
54
+ def validate_csrf_token(self, token: str) -> bool:
55
+ """Validate a CSRF token"""
56
+ if not token:
57
+ return False
58
+ referer = self.headers.get('Referer', '')
59
+ origin = self.headers.get('Origin', '')
60
+ is_valid, _ = ConfigService.validate_csrf_token(token, referer, origin)
61
+ return is_valid
62
+
63
+ def discover_master_configs(self) -> list:
64
+ """Discover all .star files in engine/topologies/ and project root master configs"""
65
+ configs = []
66
+
67
+ # First, add project root master configs
68
+ for config_name in self.ALLOWED_ROOT_CONFIGS:
69
+ config_path = PROJECT_ROOT / config_name
70
+ if config_path.exists():
71
+ configs.append({
72
+ 'path': config_name,
73
+ 'name': config_path.name,
74
+ 'category': 'Project Root',
75
+ })
76
+
77
+ # Then discover from engine/topologies/
78
+ topologies_dir = PROJECT_ROOT / 'engine' / 'topologies'
79
+
80
+ if topologies_dir.exists():
81
+ for root, dirs, files in os.walk(topologies_dir):
82
+ # Skip __pycache__ and hidden
83
+ dirs[:] = [d for d in dirs if not d.startswith('.') and d != '__pycache__']
84
+
85
+ for file in sorted(files):
86
+ if file.endswith('.star'):
87
+ file_path = Path(root) / file
88
+ rel_path = file_path.relative_to(PROJECT_ROOT)
89
+ configs.append({
90
+ 'path': str(rel_path),
91
+ 'name': file,
92
+ 'category': self._categorize_config(rel_path),
93
+ })
94
+
95
+ return sorted(configs, key=lambda x: (x['category'], x['path']))
96
+
97
+ def _categorize_config(self, path: Path) -> str:
98
+ """Categorize a config file by its location"""
99
+ path_str = str(path).lower()
100
+ if 'platform/docker' in path_str:
101
+ return 'Docker'
102
+ elif 'platform/' in path_str:
103
+ return 'Platform'
104
+ elif 'tilt/' in path_str:
105
+ return 'Tilt'
106
+ elif 'generators' in path_str:
107
+ return 'Generators'
108
+ elif 'discovery' in path_str:
109
+ return 'Discovery'
110
+ else:
111
+ return 'Other'
112
+
113
+ def discover_autogenerated_configs(self) -> list:
114
+ """Discover all autogenerated config files"""
115
+ configs = []
116
+
117
+ for root, dirs, files in os.walk(PROJECT_ROOT):
118
+ # Skip certain directories
119
+ dirs[:] = [d for d in dirs if not d.startswith('.') and
120
+ d not in ('node_modules', 'dist', 'build', '__pycache__')]
121
+
122
+ if 'autogenerated' in root:
123
+ for file in sorted(files):
124
+ if 'autogenerated' in file:
125
+ file_path = Path(root) / file
126
+ rel_path = file_path.relative_to(PROJECT_ROOT)
127
+
128
+ # Determine service name from path
129
+ parts = str(rel_path).split('/')
130
+ resource_name = 'unknown'
131
+ for i, part in enumerate(parts):
132
+ if part == 'services' and i + 2 < len(parts):
133
+ resource_name = parts[i + 2]
134
+ break
135
+
136
+ configs.append({
137
+ 'path': str(rel_path),
138
+ 'name': file,
139
+ 'service': resource_name,
140
+ 'type': self._get_autogen_type(file),
141
+ })
142
+
143
+ return sorted(configs, key=lambda x: (x['service'], x['type'], x['path']))
144
+
145
+ def _get_autogen_type(self, filename: str) -> str:
146
+ """Determine autogenerated config type from filename"""
147
+ if 'docker-compose' in filename:
148
+ return 'Docker Compose'
149
+ elif 'Dockerfile' in filename:
150
+ return 'Dockerfile'
151
+ elif 'vite.config' in filename:
152
+ return 'Vite Config'
153
+ elif 'tsconfig' in filename:
154
+ return 'TypeScript'
155
+ elif 'nginx' in filename:
156
+ return 'Nginx'
157
+ elif 'prisma' in filename:
158
+ return 'Prisma'
159
+ elif 'env' in filename:
160
+ return 'Environment'
161
+ else:
162
+ return 'Other'
163
+
164
+ def read_file_content(self, path: str) -> Tuple[str, Optional[str]]:
165
+ """
166
+ Read file content safely.
167
+ Returns: (content, content_hash)
168
+ """
169
+ resolved = FileUtils.validate_path(path)
170
+ if not resolved or not resolved.exists():
171
+ return f"# File not found: {path}", None
172
+
173
+ try:
174
+ with open(resolved, 'r', encoding='utf-8', errors='replace') as f:
175
+ content = f.read()
176
+
177
+ # Compute hash for conflict detection
178
+ content_hash = ConfigService.compute_content_hash(content)
179
+ return content, content_hash
180
+ except Exception as e:
181
+ return f"# Error reading file: {e}", None
182
+
183
+ def validate_url_path(self, path: str) -> Tuple[bool, str]:
184
+ """
185
+ Validate a path from URL parameter to prevent path traversal.
186
+ Returns: (is_valid, error_message)
187
+ """
188
+ if not path:
189
+ return False, "Empty path"
190
+
191
+ # Check for path traversal sequences
192
+ decoded = urllib.parse.unquote(path)
193
+ if '..' in decoded or '%2e' in path.lower():
194
+ return False, "Path traversal not allowed"
195
+
196
+ # Validate using ConfigService
197
+ resolved = ConfigService.validate_write_path(path)
198
+ if not resolved:
199
+ return False, "Path not in allowed directories"
200
+
201
+ # Validate file type
202
+ is_valid, msg = ConfigService.validate_file_type(path)
203
+ if not is_valid:
204
+ return False, msg
205
+
206
+ return True, "Valid"
207
+
208
+ def parse_multi_master_params(self, query: dict) -> Tuple[list, str]:
209
+ """
210
+ Parse and validate master1, master2, master3 URL parameters.
211
+ Returns: (list_of_paths, error_message)
212
+ """
213
+ masters = []
214
+
215
+ for i in range(1, 4):
216
+ param_name = f'master{i}'
217
+ if param_name in query and query[param_name][0]:
218
+ path = query[param_name][0]
219
+
220
+ # Check for sequential parameter gap
221
+ if i > 1 and not masters:
222
+ return [], f"Invalid URL parameters: {param_name} specified without prior masters"
223
+
224
+ # Validate path
225
+ is_valid, msg = self.validate_url_path(path)
226
+ if not is_valid:
227
+ return [], f"Invalid path in {param_name}: {msg}"
228
+
229
+ # Check for duplicates
230
+ if path in masters:
231
+ return [], f"Duplicate file: {path} cannot be selected multiple times"
232
+
233
+ masters.append(path)
234
+
235
+ return masters, ""
236
+
237
+ def render_config_tree(self, configs: list, selected_paths: list = None) -> str:
238
+ """Render config tree navigation with multi-select support."""
239
+ if selected_paths is None:
240
+ selected_paths = []
241
+
242
+ current_category = None
243
+ html = ['<ul class="config-tree">']
244
+
245
+ for cfg in configs:
246
+ # Category header
247
+ if cfg['category'] != current_category:
248
+ if current_category:
249
+ html.append('</ul></li>')
250
+ current_category = cfg['category']
251
+ html.append(f'<li style="margin-top: 1rem;"><strong style="color: var(--text-muted); font-size: 0.8rem; text-transform: uppercase;">{current_category}</strong><ul style="list-style: none; padding-left: 0.5rem;">')
252
+
253
+ # Check if this config is selected and get its order
254
+ is_selected = cfg['path'] in selected_paths
255
+ selection_order = ''
256
+ if is_selected:
257
+ order = selected_paths.index(cfg['path']) + 1
258
+ selection_order = f'<span class="selection-order">{order}</span>'
259
+
260
+ active_class = 'active' if is_selected else ''
261
+ encoded_path = urllib.parse.quote(cfg['path'])
262
+
263
+ # Build onclick handler for multi-select
264
+ if selected_paths:
265
+ # If we already have selections, append to them
266
+ if is_selected:
267
+ # Deselect if already selected
268
+ new_selection = [p for p in selected_paths if p != cfg['path']]
269
+ else:
270
+ # Add to selection if less than 3
271
+ if len(selected_paths) < 3:
272
+ new_selection = selected_paths + [cfg['path']]
273
+ else:
274
+ new_selection = selected_paths # Max reached
275
+
276
+ # Build new URL
277
+ params = []
278
+ for i, p in enumerate(new_selection, 1):
279
+ params.append(f"master{i}={urllib.parse.quote(p)}")
280
+ new_url = '/compare?' + '&'.join(params) if params else '/'
281
+
282
+ html.append(f'''
283
+ <li class="tree-item {active_class}" onclick="handleConfigClick(event, '{encoded_path}', {len(selected_paths)})">
284
+ <span>⭐</span>
285
+ <span>{cfg['name']}</span>
286
+ {selection_order}
287
+ </li>
288
+ ''')
289
+ else:
290
+ # Single select mode
291
+ html.append(f'''
292
+ <li class="tree-item {active_class}" onclick="handleConfigClick(event, '{encoded_path}', 0)">
293
+ <span>⭐</span>
294
+ <span>{cfg['name']}</span>
295
+ {selection_order}
296
+ </li>
297
+ ''')
298
+
299
+ if current_category:
300
+ html.append('</ul></li>')
301
+
302
+ html.append('</ul>')
303
+ return ''.join(html)
304
+
305
+ def render_multi_pane_view(self, master_paths: list, edit_mode: bool, sync_scroll: bool, csrf_token: str) -> str:
306
+ """Render multi-pane view for 1-3 master configs."""
307
+ num_panes = len(master_paths)
308
+ if num_panes == 0:
309
+ return '<div class="empty-state"><div class="empty-state-icon">⚙️</div><p>No master configs selected</p></div>'
310
+
311
+ # Determine grid layout class
312
+ if num_panes == 1:
313
+ grid_class = 'single-pane'
314
+ elif num_panes == 2:
315
+ grid_class = 'dual-pane'
316
+ else:
317
+ grid_class = 'triple-pane'
318
+
319
+ # Build panes
320
+ panes_html = []
321
+ pane_data = []
322
+
323
+ for i, path in enumerate(master_paths, 1):
324
+ content, content_hash = self.read_file_content(path)
325
+ pane_data.append({
326
+ 'path': path,
327
+ 'content': content,
328
+ 'hash': content_hash,
329
+ 'index': i
330
+ })
331
+
332
+ # Determine unsaved status (will be tracked client-side)
333
+ unsaved_indicator = '<span class="unsaved-indicator hidden">●</span>'
334
+
335
+ # Render content based on edit mode
336
+ if edit_mode:
337
+ # Editable textarea with HTML-escaped content
338
+ escaped_content = self.escape_html(content)
339
+ content_html = f'''
340
+ <textarea class="config-editor"
341
+ data-path="{self.escape_html(path)}"
342
+ data-hash="{content_hash or ''}"
343
+ data-pane="{i}"
344
+ spellcheck="false">{escaped_content}</textarea>
345
+ '''
346
+ else:
347
+ # Read-only pre/code view
348
+ content_html = f'<pre class="config-viewer" data-pane="{i}"><code>{self.escape_html(content)}</code></pre>'
349
+
350
+ pane_html = f'''
351
+ <div class="panel pane-{i}" data-pane="{i}">
352
+ <div class="panel-header">
353
+ <div class="panel-header-left">
354
+ <span class="panel-title">⭐ Master Config {i}/{num_panes}</span>
355
+ <span class="text-muted panel-path">{path}</span>
356
+ {unsaved_indicator}
357
+ </div>
358
+ {f'<button class="save-pane-btn hidden" data-pane="{i}" onclick="savePane({i})">💾 Save</button>' if edit_mode else ''}
359
+ </div>
360
+ <div class="panel-content" data-pane="{i}">
361
+ {content_html}
362
+ </div>
363
+ </div>
364
+ '''
365
+ panes_html.append(pane_html)
366
+
367
+ # Generate pane data for JavaScript
368
+ pane_data_json = json.dumps(pane_data)
369
+
370
+ # Build the multi-pane container
371
+ content_html = f'''
372
+ <div class="multi-pane-view {grid_class}" id="multiPaneView" data-sync="{str(sync_scroll).lower()}">
373
+ {''.join(panes_html)}
374
+ </div>
375
+
376
+ <script>
377
+ // Store pane data for JavaScript
378
+ window.PANE_DATA = {pane_data_json};
379
+ window.EDIT_MODE = {'true' if edit_mode else 'false'};
380
+ window.SYNC_SCROLL = {'true' if sync_scroll else 'false'};
381
+ window.CSRF_TOKEN = '{csrf_token}';
382
+ </script>
383
+ '''
384
+
385
+ return content_html
386
+
387
+ def check_resource_status(self) -> dict:
388
+ """Check if other IDE services are running"""
389
+ import socket
390
+ services = {
391
+ 'files': {'host': 'localhost', 'port': 9765},
392
+ 'viewer': {'host': 'localhost', 'port': 9766},
393
+ 'terminal': {'host': 'localhost', 'port': 9768},
394
+ }
395
+ status = {}
396
+ for name, info in services.items():
397
+ try:
398
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
399
+ sock.settimeout(0.5)
400
+ result = sock.connect_ex(('127.0.0.1', info['port']))
401
+ sock.close()
402
+ status[name] = result == 0
403
+ except:
404
+ status[name] = False
405
+ return status
406
+
407
+ def get_main_javascript(self, edit_mode: bool) -> str:
408
+ """Get the main JavaScript code for the Config Inspector"""
409
+ # This is a long JavaScript string - could be moved to a separate file
410
+ # For now, keeping inline to avoid breaking existing functionality
411
+ return '''
412
+ <script>
413
+ // Track dirty state per pane
414
+ window.DIRTY_PANES = new Set();
415
+
416
+ // Handle config tree click with multi-select support
417
+ function handleConfigClick(event, path, currentSelectionCount) {
418
+ const isMultiSelect = event.ctrlKey || event.metaKey;
419
+
420
+ if (isMultiSelect) {
421
+ event.preventDefault();
422
+
423
+ // Build new selection
424
+ let currentPaths = window.PANE_DATA ? window.PANE_DATA.map(p => p.path) : [];
425
+ const index = currentPaths.indexOf(path);
426
+
427
+ if (index >= 0) {
428
+ // Deselect
429
+ currentPaths.splice(index, 1);
430
+ } else if (currentPaths.length < 3) {
431
+ // Add to selection (max 3)
432
+ currentPaths.push(path);
433
+ }
434
+
435
+ // Navigate to new URL
436
+ const params = currentPaths.map((p, i) => `master${i+1}=${encodeURIComponent(p)}`);
437
+ const newUrl = params.length > 0 ? '/compare?' + params.join('&') : '/';
438
+ window.location.href = newUrl;
439
+ } else {
440
+ // Single select - just navigate
441
+ window.location.href = '/compare?master1=' + encodeURIComponent(path);
442
+ }
443
+ }
444
+
445
+ // Clear all selections
446
+ function clearSelection() {
447
+ window.location.href = '/';
448
+ }
449
+
450
+ // Toggle edit mode
451
+ function toggleEditMode() {
452
+ if (window.DIRTY_PANES.size > 0) {
453
+ const files = Array.from(window.DIRTY_PANES).map(pane => {
454
+ const paneData = window.PANE_DATA.find(p => p.index === pane);
455
+ return paneData ? paneData.path : 'Unknown';
456
+ });
457
+ if (!confirm(`You have unsaved changes in:\n${files.join('\\n')}\n\nDiscard changes and exit edit mode?`)) {
458
+ return;
459
+ }
460
+ }
461
+
462
+ const url = new URL(window.location);
463
+ const currentEdit = url.searchParams.get('edit') === 'true';
464
+ url.searchParams.set('edit', (!currentEdit).toString());
465
+ window.location.href = url.toString();
466
+ }
467
+
468
+ // Toggle sync scroll
469
+ function toggleSyncScroll() {
470
+ const checkbox = document.getElementById('syncScrollCheckbox');
471
+ const url = new URL(window.location);
472
+ url.searchParams.set('sync', checkbox.checked.toString());
473
+ window.location.href = url.toString();
474
+ }
475
+
476
+ // Save a single pane
477
+ async function savePane(paneIndex) {
478
+ const textarea = document.querySelector(`textarea[data-pane="${paneIndex}"]`);
479
+ if (!textarea) return;
480
+
481
+ const path = textarea.dataset.path;
482
+ const content = textarea.value;
483
+ const originalHash = textarea.dataset.hash;
484
+
485
+ await saveFile(path, content, originalHash, paneIndex);
486
+ }
487
+
488
+ // Save all panes
489
+ async function saveAllPanes() {
490
+ const textareas = document.querySelectorAll('textarea.config-editor');
491
+ const results = [];
492
+
493
+ for (const textarea of textareas) {
494
+ const paneIndex = parseInt(textarea.dataset.pane);
495
+ const path = textarea.dataset.path;
496
+ const content = textarea.value;
497
+ const originalHash = textarea.dataset.hash;
498
+
499
+ const result = await saveFile(path, content, originalHash, paneIndex, false);
500
+ results.push({ paneIndex, success: result.success });
501
+ }
502
+
503
+ // Show summary notification
504
+ const successCount = results.filter(r => r.success).length;
505
+ const totalCount = results.length;
506
+
507
+ if (successCount === totalCount) {
508
+ showNotification(`All ${totalCount} files saved successfully`, 'success');
509
+ window.DIRTY_PANES.clear();
510
+ updateUnsavedIndicators();
511
+ updateSaveAllButton();
512
+ } else {
513
+ showNotification(`${successCount}/${totalCount} files saved`, 'warning');
514
+ }
515
+ }
516
+
517
+ // Save a file via API
518
+ async function saveFile(path, content, originalHash, paneIndex, showToast = true) {
519
+ try {
520
+ const response = await fetch('/save', {
521
+ method: 'POST',
522
+ headers: {
523
+ 'Content-Type': 'application/json',
524
+ 'X-CSRF-Token': window.CSRF_TOKEN
525
+ },
526
+ body: JSON.stringify({
527
+ path: path,
528
+ content: content,
529
+ original_hash: originalHash,
530
+ csrf_token: window.CSRF_TOKEN
531
+ })
532
+ });
533
+
534
+ const result = await response.json();
535
+
536
+ if (result.success) {
537
+ if (showToast) {
538
+ showNotification(`Saved: ${path}`, 'success');
539
+ }
540
+
541
+ // Trigger save success animation on the panel
542
+ const panel = document.querySelector(`.pane-${paneIndex}`);
543
+ if (panel) {
544
+ panel.classList.add('save-success');
545
+ setTimeout(() => panel.classList.remove('save-success'), 1000);
546
+ }
547
+
548
+ // Clear dirty state for this pane
549
+ window.DIRTY_PANES.delete(paneIndex);
550
+ updateUnsavedIndicators();
551
+ updateSaveAllButton();
552
+
553
+ // Update the hash in the textarea
554
+ const textarea = document.querySelector(`textarea[data-pane="${paneIndex}"]`);
555
+ if (textarea && result.new_hash) {
556
+ textarea.dataset.hash = result.new_hash;
557
+ }
558
+
559
+ return { success: true };
560
+ } else {
561
+ if (showToast) {
562
+ showNotification(`Failed: ${result.message}`, 'error');
563
+ }
564
+ return { success: false, message: result.message };
565
+ }
566
+ } catch (error) {
567
+ if (showToast) {
568
+ showNotification(`Error: ${error.message}`, 'error');
569
+ }
570
+ return { success: false, message: error.message };
571
+ }
572
+ }
573
+
574
+ // Show notification toast
575
+ function showNotification(message, type = 'info') {
576
+ const existing = document.querySelector('.notification-toast');
577
+ if (existing) existing.remove();
578
+
579
+ const toast = document.createElement('div');
580
+ toast.className = `notification-toast ${type}`;
581
+ toast.textContent = message;
582
+ document.body.appendChild(toast);
583
+
584
+ setTimeout(() => {
585
+ toast.classList.add('show');
586
+ }, 10);
587
+
588
+ setTimeout(() => {
589
+ toast.classList.remove('show');
590
+ setTimeout(() => toast.remove(), 300);
591
+ }, 3000);
592
+ }
593
+
594
+ // Update unsaved change indicators
595
+ function updateUnsavedIndicators() {
596
+ document.querySelectorAll('.unsaved-indicator').forEach(indicator => {
597
+ const pane = indicator.closest('.panel').dataset.pane;
598
+ if (window.DIRTY_PANES.has(parseInt(pane))) {
599
+ indicator.classList.remove('hidden');
600
+ } else {
601
+ indicator.classList.add('hidden');
602
+ }
603
+ });
604
+ }
605
+
606
+ // Update save all button visibility
607
+ function updateSaveAllButton() {
608
+ const btn = document.getElementById('saveAllBtn');
609
+ if (window.EDIT_MODE && window.DIRTY_PANES.size > 0) {
610
+ btn.classList.remove('hidden');
611
+ } else {
612
+ btn.classList.add('hidden');
613
+ }
614
+ }
615
+
616
+ // Track content changes
617
+ function onContentChange(paneIndex) {
618
+ window.DIRTY_PANES.add(paneIndex);
619
+ updateUnsavedIndicators();
620
+ updateSaveAllButton();
621
+
622
+ // Show save button for this pane
623
+ const saveBtn = document.querySelector(`.save-pane-btn[data-pane="${paneIndex}"]`);
624
+ if (saveBtn) saveBtn.classList.remove('hidden');
625
+ }
626
+
627
+ function setupSyncScroll() {
628
+ if (!window.SYNC_SCROLL) return;
629
+
630
+ const panes = document.querySelectorAll('.panel-content');
631
+ let isSyncing = false;
632
+
633
+ panes.forEach(sourcePane => {
634
+ sourcePane.addEventListener('scroll', () => {
635
+ if (isSyncing || !window.SYNC_SCROLL) return;
636
+
637
+ isSyncing = true;
638
+ requestAnimationFrame(() => {
639
+ const scrollTop = sourcePane.scrollTop;
640
+ const scrollLeft = sourcePane.scrollLeft;
641
+
642
+ panes.forEach(targetPane => {
643
+ if (targetPane !== sourcePane) {
644
+ targetPane.scrollTop = scrollTop;
645
+ targetPane.scrollLeft = scrollLeft;
646
+ }
647
+ });
648
+
649
+ isSyncing = false;
650
+ });
651
+ });
652
+ });
653
+ }
654
+
655
+ // Setup keyboard shortcuts
656
+ function setupKeyboardShortcuts() {
657
+ document.addEventListener('keydown', (e) => {
658
+ const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
659
+ const cmdKey = isMac ? e.metaKey : e.ctrlKey;
660
+
661
+ if (!cmdKey) return;
662
+
663
+ // Ctrl/Cmd+S: Save focused pane
664
+ if (e.key === 's' && !e.shiftKey) {
665
+ e.preventDefault();
666
+ if (!window.EDIT_MODE) return;
667
+
668
+ const activeElement = document.activeElement;
669
+ if (activeElement && activeElement.classList.contains('config-editor')) {
670
+ const paneIndex = parseInt(activeElement.dataset.pane);
671
+ savePane(paneIndex);
672
+ }
673
+ }
674
+
675
+ // Ctrl/Cmd+Shift+S: Save all panes
676
+ if (e.key === 'S' || (e.key === 's' && e.shiftKey)) {
677
+ e.preventDefault();
678
+ if (!window.EDIT_MODE || window.DIRTY_PANES.size === 0) return;
679
+ saveAllPanes();
680
+ }
681
+
682
+ // Ctrl/Cmd+E: Toggle edit mode
683
+ if (e.key === 'e' || e.key === 'E') {
684
+ e.preventDefault();
685
+ toggleEditMode();
686
+ }
687
+ });
688
+ }
689
+
690
+ // Warn about unsaved changes on page unload
691
+ function setupBeforeUnload() {
692
+ window.addEventListener('beforeunload', (e) => {
693
+ if (window.DIRTY_PANES.size > 0) {
694
+ e.preventDefault();
695
+ e.returnValue = '';
696
+ return '';
697
+ }
698
+ });
699
+ }
700
+
701
+ document.addEventListener('DOMContentLoaded', () => {
702
+ document.querySelectorAll('textarea.config-editor').forEach(textarea => {
703
+ const paneIndex = parseInt(textarea.dataset.pane);
704
+ textarea.addEventListener('input', () => onContentChange(paneIndex));
705
+ });
706
+ setupSyncScroll();
707
+ setupKeyboardShortcuts();
708
+ setupBeforeUnload();
709
+ updateSaveAllButton();
710
+ });
711
+ </script>
712
+ '''
713
+
714
+ def _get_template_path(self) -> Path:
715
+ """Override to use shared template"""
716
+ return Path(__file__).parent.parent / 'shared' / 'templates' / 'base.html'
717
+
718
+ def do_GET(self):
719
+ """Handle GET requests"""
720
+ parsed = urllib.parse.urlparse(self.path)
721
+ path = parsed.path
722
+ query = urllib.parse.parse_qs(parsed.query)
723
+
724
+ # Health check
725
+ if path == '/health':
726
+ self.handle_health_check('config-inspector', ['multi-pane', 'edit-mode', 'sync-scroll'])
727
+ return
728
+
729
+ # API: List all master configs
730
+ if path == '/api/masters':
731
+ master_configs = self.discover_master_configs()
732
+ self.send_json_response({
733
+ 'status': 'ok',
734
+ 'count': len(master_configs),
735
+ 'configs': master_configs
736
+ })
737
+ return
738
+
739
+ # API: Get specific config content
740
+ if path == '/api/config':
741
+ config_path = query.get('path', [''])[0]
742
+ if not config_path:
743
+ self.send_json_response({
744
+ 'status': 'error',
745
+ 'message': 'Missing path parameter'
746
+ }, 400)
747
+ return
748
+
749
+ content, content_hash = self.read_file_content(config_path)
750
+ if content is None:
751
+ self.send_json_response({
752
+ 'status': 'error',
753
+ 'message': f'Config not found: {config_path}'
754
+ }, 404)
755
+ return
756
+
757
+ self.send_json_response({
758
+ 'status': 'ok',
759
+ 'path': config_path,
760
+ 'content': content,
761
+ 'hash': content_hash
762
+ })
763
+ return
764
+
765
+ # Static files
766
+ if path.startswith('/static/'):
767
+ static_path = Path(__file__).parent.parent / 'shared' / path[1:]
768
+ if static_path.exists():
769
+ with open(static_path, 'r', encoding='utf-8') as f:
770
+ content = f.read()
771
+ self.send_css_response(content)
772
+ else:
773
+ self.send_error_response(404, 'Not found')
774
+ return
775
+
776
+ # Main page or compare view
777
+ if path in ('/', '/compare'):
778
+ master_configs = self.discover_master_configs()
779
+
780
+ # Parse multi-master parameters
781
+ selected_paths, error_msg = self.parse_multi_master_params(query)
782
+
783
+ if error_msg:
784
+ sidebar = self.render_config_tree(master_configs)
785
+ error_html = f'<div class="empty-state"><div class="empty-state-icon">❌</div><p>{self.escape_html(error_msg)}</p></div>'
786
+ csrf_token = self.generate_csrf_token()
787
+ self.send_html_response(self.render_page(
788
+ 'Config Inspector',
789
+ error_html,
790
+ icon='⚙️',
791
+ sidebar=sidebar,
792
+ scripts=f'<script>window.CSRF_TOKEN = "{csrf_token}";</script>'
793
+ ))
794
+ return
795
+
796
+ # Get edit and sync params
797
+ edit_mode = query.get('edit', ['false'])[0].lower() == 'true'
798
+ sync_scroll = query.get('sync', ['false'])[0].lower() == 'true'
799
+
800
+ # If no selection, use first config as default
801
+ if not selected_paths and master_configs:
802
+ # Prefer Project Root configs first
803
+ root_configs = [c for c in master_configs if c['category'] == 'Project Root']
804
+ if root_configs:
805
+ selected_paths = [root_configs[0]['path']]
806
+ else:
807
+ selected_paths = [master_configs[0]['path']]
808
+
809
+ # Build sidebar with multi-select support
810
+ sidebar = self.render_config_tree(master_configs, selected_paths)
811
+
812
+ # Generate CSRF token
813
+ csrf_token = self.generate_csrf_token()
814
+
815
+ # Build header controls
816
+ edit_toggle_class = 'active' if edit_mode else ''
817
+ sync_checked = 'checked' if sync_scroll else ''
818
+ has_selection = selected_paths and len(selected_paths) > 0
819
+
820
+ header_controls = f'''
821
+ <div class="header-controls">
822
+ <label class="sync-scroll-label">
823
+ <input type="checkbox" id="syncScrollCheckbox" {sync_checked} onchange="toggleSyncScroll()">
824
+ <span>🔗 Sync Scroll</span>
825
+ </label>
826
+ <button class="edit-mode-btn {edit_toggle_class}" id="editModeBtn" onclick="toggleEditMode()" title="Ctrl+E">
827
+ ✏️ Edit Mode
828
+ </button>
829
+ <button class="save-all-btn hidden" id="saveAllBtn" onclick="saveAllPanes()" title="Ctrl+Shift+S">
830
+ 💾 Save All
831
+ </button>
832
+ {f'<button class="clear-selection-btn" onclick="clearSelection()">❌ Clear</button>' if has_selection else ''}
833
+ </div>
834
+ '''
835
+
836
+ # Build multi-pane content
837
+ if selected_paths:
838
+ content_html = self.render_multi_pane_view(
839
+ selected_paths, edit_mode, sync_scroll, csrf_token
840
+ )
841
+ else:
842
+ content_html = '<div class="empty-state"><div class="empty-state-icon">⚙️</div><p>No master configs found</p></div>'
843
+
844
+ # Check service status for other IDE components
845
+ resource_status = self.check_resource_status()
846
+
847
+ # Get JavaScript
848
+ scripts = self.get_main_javascript(edit_mode)
849
+
850
+ # Build the complete page
851
+ page = self.render_page(
852
+ 'Config Inspector',
853
+ content_html,
854
+ icon='⚙️',
855
+ sidebar=sidebar,
856
+ controls=header_controls,
857
+ scripts=scripts
858
+ )
859
+
860
+ # Add resource status placeholders if they exist in template
861
+ page = page.replace('{files_available}', 'true' if resource_status.get('files') else 'false')
862
+ page = page.replace('{viewer_available}', 'true' if resource_status.get('viewer') else 'false')
863
+ page = page.replace('{terminal_available}', 'true' if resource_status.get('terminal') else 'false')
864
+
865
+ self.send_html_response(page)
866
+ return
867
+
868
+ # 404
869
+ self.send_html_response(self.render_page(
870
+ 'Not Found',
871
+ '<div class="empty-state"><div class="empty-state-icon">❓</div>Page not found</div>',
872
+ icon='❓'
873
+ ), 404)
874
+
875
+ def do_POST(self):
876
+ """Handle POST requests for save operations"""
877
+ parsed = urllib.parse.urlparse(self.path)
878
+ path = parsed.path
879
+
880
+ # Validate Content-Length header for DoS prevention
881
+ content_length = self.headers.get('Content-Length')
882
+ is_valid, msg = ConfigService.validate_request_content_length(content_length)
883
+ if not is_valid:
884
+ self.send_json_response({
885
+ 'success': False,
886
+ 'message': f'Invalid request: {msg}'
887
+ }, 400)
888
+ return
889
+
890
+ # Read request body
891
+ content_length = int(content_length)
892
+ body = self.rfile.read(content_length).decode('utf-8')
893
+
894
+ try:
895
+ data = json.loads(body)
896
+ except json.JSONDecodeError:
897
+ self.send_json_response({
898
+ 'success': False,
899
+ 'message': 'Invalid JSON in request body'
900
+ }, 400)
901
+ return
902
+
903
+ # Validate CSRF token
904
+ csrf_token = data.get('csrf_token', '')
905
+ if not self.validate_csrf_token(csrf_token):
906
+ self.send_json_response({
907
+ 'success': False,
908
+ 'message': 'Invalid CSRF token'
909
+ }, 403)
910
+ return
911
+
912
+ # Handle save endpoints
913
+ if path == '/save':
914
+ self._handle_save_single(data)
915
+ elif path == '/save-all':
916
+ self._handle_save_all(data)
917
+ else:
918
+ self.send_json_response({
919
+ 'success': False,
920
+ 'message': 'Unknown endpoint'
921
+ }, 404)
922
+
923
+ def _handle_save_single(self, data: dict):
924
+ """Handle single file save"""
925
+ file_path = data.get('path', '')
926
+ content = data.get('content', '')
927
+ original_hash = data.get('original_hash', '')
928
+
929
+ # Validate path
930
+ is_valid, msg = self.validate_url_path(file_path)
931
+ if not is_valid:
932
+ self.send_json_response({
933
+ 'success': False,
934
+ 'message': f'Invalid path: {msg}'
935
+ }, 400)
936
+ return
937
+
938
+ # Validate file type
939
+ is_valid, msg = ConfigService.validate_file_type(file_path)
940
+ if not is_valid:
941
+ self.send_json_response({
942
+ 'success': False,
943
+ 'message': msg
944
+ }, 400)
945
+ return
946
+
947
+ # Resolve path
948
+ resolved = ConfigService.validate_write_path(file_path)
949
+ if not resolved:
950
+ self.send_json_response({
951
+ 'success': False,
952
+ 'message': 'Path validation failed'
953
+ }, 400)
954
+ return
955
+
956
+ # Perform atomic write
957
+ result = ConfigService.atomic_write(resolved, content, original_hash, create_backup=True)
958
+
959
+ # Compute new hash if successful
960
+ new_hash = None
961
+ if result.success:
962
+ new_hash = ConfigService.compute_content_hash(content)
963
+
964
+ self.send_json_response({
965
+ 'success': result.success,
966
+ 'message': result.message,
967
+ 'new_hash': new_hash,
968
+ 'backup_path': str(result.backup_path) if result.backup_path else None
969
+ }, 200 if result.success else 409)
970
+
971
+ def _handle_save_all(self, data: dict):
972
+ """Handle batch save of multiple files"""
973
+ files = data.get('files', [])
974
+
975
+ if not isinstance(files, list):
976
+ self.send_json_response({
977
+ 'success': False,
978
+ 'message': 'Invalid request: files must be an array'
979
+ }, 400)
980
+ return
981
+
982
+ results = []
983
+ all_success = True
984
+
985
+ for file_data in files:
986
+ file_path = file_data.get('path', '')
987
+ content = file_data.get('content', '')
988
+ original_hash = file_data.get('original_hash', '')
989
+
990
+ # Validate
991
+ is_valid, msg = self.validate_url_path(file_path)
992
+ if not is_valid:
993
+ results.append({
994
+ 'path': file_path,
995
+ 'success': False,
996
+ 'message': f'Invalid path: {msg}'
997
+ })
998
+ all_success = False
999
+ continue
1000
+
1001
+ is_valid, msg = ConfigService.validate_file_type(file_path)
1002
+ if not is_valid:
1003
+ results.append({
1004
+ 'path': file_path,
1005
+ 'success': False,
1006
+ 'message': msg
1007
+ })
1008
+ all_success = False
1009
+ continue
1010
+
1011
+ # Save
1012
+ resolved = ConfigService.validate_write_path(file_path)
1013
+ if not resolved:
1014
+ results.append({
1015
+ 'path': file_path,
1016
+ 'success': False,
1017
+ 'message': 'Path validation failed'
1018
+ })
1019
+ all_success = False
1020
+ continue
1021
+
1022
+ result = ConfigService.atomic_write(resolved, content, original_hash, create_backup=True)
1023
+
1024
+ results.append({
1025
+ 'path': file_path,
1026
+ 'success': result.success,
1027
+ 'message': result.message,
1028
+ 'new_hash': ConfigService.compute_content_hash(content) if result.success else None
1029
+ })
1030
+
1031
+ if not result.success:
1032
+ all_success = False
1033
+
1034
+ self.send_json_response({
1035
+ 'success': all_success,
1036
+ 'message': f'{sum(1 for r in results if r["success"])}/{len(results)} files saved',
1037
+ 'results': results
1038
+ }, 200 if all_success else 207) # 207 Multi-Status for partial success
1039
+
1040
+
1041
+ def main():
1042
+ """Run the config inspector server"""
1043
+ run_server(
1044
+ ConfigInspectorHandler,
1045
+ PORT,
1046
+ '⚙️ Tilt IDE Config Inspector',
1047
+ '🔍 Compare master .star configs with auto-generated outputs\n ✏️ Multi-pane editing with Ctrl+S, Ctrl+Shift+S, Ctrl+E'
1048
+ )
1049
+
1050
+
1051
+ if __name__ == '__main__':
1052
+ main()