groundwork-method 0.0.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +781 -0
- package/LICENSE +21 -0
- package/README.md +44 -29
- package/bin/groundwork.js +1654 -0
- package/dist/src/generators/add-capability/generator.d.ts +8 -0
- package/dist/src/generators/add-capability/generator.js +60 -0
- package/dist/src/generators/add-capability/generator.js.map +1 -0
- package/dist/src/generators/cli-app/generator.d.ts +9 -0
- package/dist/src/generators/cli-app/generator.js +140 -0
- package/dist/src/generators/cli-app/generator.js.map +1 -0
- package/dist/src/generators/docs-site/generator.d.ts +5 -0
- package/dist/src/generators/docs-site/generator.js +441 -0
- package/dist/src/generators/docs-site/generator.js.map +1 -0
- package/dist/src/generators/electron-app/generator.d.ts +6 -0
- package/dist/src/generators/electron-app/generator.js +261 -0
- package/dist/src/generators/electron-app/generator.js.map +1 -0
- package/dist/src/generators/flutter-app/generator.d.ts +6 -0
- package/dist/src/generators/flutter-app/generator.js +314 -0
- package/dist/src/generators/flutter-app/generator.js.map +1 -0
- package/dist/src/generators/go-microservice/generator.d.ts +8 -0
- package/dist/src/generators/go-microservice/generator.js +232 -0
- package/dist/src/generators/go-microservice/generator.js.map +1 -0
- package/dist/src/generators/nextjs-app/generator.d.ts +8 -0
- package/dist/src/generators/nextjs-app/generator.js +294 -0
- package/dist/src/generators/nextjs-app/generator.js.map +1 -0
- package/dist/src/generators/python-microservice/generator.d.ts +13 -0
- package/dist/src/generators/python-microservice/generator.js +265 -0
- package/dist/src/generators/python-microservice/generator.js.map +1 -0
- package/dist/src/generators/shared/brand-tokens.d.ts +89 -0
- package/dist/src/generators/shared/brand-tokens.js +308 -0
- package/dist/src/generators/shared/brand-tokens.js.map +1 -0
- package/dist/src/generators/shared/capabilities.d.ts +101 -0
- package/dist/src/generators/shared/capabilities.js +279 -0
- package/dist/src/generators/shared/capabilities.js.map +1 -0
- package/dist/src/generators/shared/provenance.d.ts +2 -0
- package/dist/src/generators/shared/provenance.js +85 -0
- package/dist/src/generators/shared/provenance.js.map +1 -0
- package/dist/src/generators/shared/scaffold-helpers.d.ts +72 -0
- package/dist/src/generators/shared/scaffold-helpers.js +309 -0
- package/dist/src/generators/shared/scaffold-helpers.js.map +1 -0
- package/dist/src/generators/system-test-runner/generator.d.ts +23 -0
- package/dist/src/generators/system-test-runner/generator.js +125 -0
- package/dist/src/generators/system-test-runner/generator.js.map +1 -0
- package/dist/src/generators/workspace-dev-cli/generator.d.ts +7 -0
- package/dist/src/generators/workspace-dev-cli/generator.js +138 -0
- package/dist/src/generators/workspace-dev-cli/generator.js.map +1 -0
- package/generators.json +57 -0
- package/lib/repo-map/grammars/tree-sitter-c.wasm +0 -0
- package/lib/repo-map/grammars/tree-sitter-cpp.wasm +0 -0
- package/lib/repo-map/grammars/tree-sitter-csharp.wasm +0 -0
- package/lib/repo-map/grammars/tree-sitter-dart.wasm +0 -0
- package/lib/repo-map/grammars/tree-sitter-go.wasm +0 -0
- package/lib/repo-map/grammars/tree-sitter-java.wasm +0 -0
- package/lib/repo-map/grammars/tree-sitter-javascript.wasm +0 -0
- package/lib/repo-map/grammars/tree-sitter-kotlin.wasm +0 -0
- package/lib/repo-map/grammars/tree-sitter-lua.wasm +0 -0
- package/lib/repo-map/grammars/tree-sitter-php.wasm +0 -0
- package/lib/repo-map/grammars/tree-sitter-python.wasm +0 -0
- package/lib/repo-map/grammars/tree-sitter-ruby.wasm +0 -0
- package/lib/repo-map/grammars/tree-sitter-rust.wasm +0 -0
- package/lib/repo-map/grammars/tree-sitter-scala.wasm +0 -0
- package/lib/repo-map/grammars/tree-sitter-swift.wasm +0 -0
- package/lib/repo-map/grammars/tree-sitter-tsx.wasm +0 -0
- package/lib/repo-map/grammars/tree-sitter-typescript.wasm +0 -0
- package/lib/repo-map/index.js +386 -0
- package/lib/repo-map/languages.js +514 -0
- package/lib/repo-map/pagerank.js +59 -0
- package/migrations/README.md +60 -0
- package/migrations/_template/cli-migration.js +27 -0
- package/migrations/gw-bet-prose-redesign.js +105 -0
- package/migrations/gw-drop-test-manifest.js +37 -0
- package/migrations/gw-register-serena-mcp.js +42 -0
- package/migrations/gw-relocate-hidden-skills.js +40 -0
- package/migrations/gw-seed-config-toml.js +24 -0
- package/migrations/index.json +40 -0
- package/package.json +70 -6
- package/src/AGENTS.md +36 -0
- package/src/config/config.toml +30 -0
- package/src/config/groundwork-state.json +5 -0
- package/src/docs/llms.txt +72 -0
- package/src/docs/principles/ai-native/agent-native-systems.md +90 -0
- package/src/docs/principles/ai-native/agentic-systems.md +78 -0
- package/src/docs/principles/ai-native/ai-engineering.md +100 -0
- package/src/docs/principles/ai-native/ai-native-product.md +76 -0
- package/src/docs/principles/delivery/cost-engineering.md +89 -0
- package/src/docs/principles/delivery/day-2-operational-baseline.md +57 -0
- package/src/docs/principles/delivery/devex.md +88 -0
- package/src/docs/principles/delivery/platform.md +101 -0
- package/src/docs/principles/delivery/progressive-delivery.md +92 -0
- package/src/docs/principles/design/ai-native-design.md +73 -0
- package/src/docs/principles/design/design-foundations.md +80 -0
- package/src/docs/principles/design/design-systems-and-tokens.md +72 -0
- package/src/docs/principles/design/interaction-and-motion.md +69 -0
- package/src/docs/principles/design/layout-and-space.md +72 -0
- package/src/docs/principles/design/usability-and-ux.md +68 -0
- package/src/docs/principles/design/visual-design.md +84 -0
- package/src/docs/principles/foundations/code-craft.md +86 -0
- package/src/docs/principles/foundations/continuous-discovery.md +75 -0
- package/src/docs/principles/foundations/documentation.md +102 -0
- package/src/docs/principles/foundations/prioritization-and-appetite.md +78 -0
- package/src/docs/principles/foundations/product-engineering.md +90 -0
- package/src/docs/principles/foundations/product-risks.md +89 -0
- package/src/docs/principles/foundations/requirements-and-specs.md +80 -0
- package/src/docs/principles/foundations/success-metrics.md +66 -0
- package/src/docs/principles/foundations/testing.md +82 -0
- package/src/docs/principles/index.md +23 -0
- package/src/docs/principles/quality/accessibility.md +88 -0
- package/src/docs/principles/quality/observability.md +84 -0
- package/src/docs/principles/quality/performance.md +84 -0
- package/src/docs/principles/quality/privacy.md +92 -0
- package/src/docs/principles/quality/reliability.md +89 -0
- package/src/docs/principles/quality/security.md +78 -0
- package/src/docs/principles/stack/postgres.md +100 -0
- package/src/docs/principles/system-design/api-design.md +86 -0
- package/src/docs/principles/system-design/architecture-decisions.md +81 -0
- package/src/docs/principles/system-design/code-structure.md +104 -0
- package/src/docs/principles/system-design/data-engineering.md +87 -0
- package/src/docs/principles/system-design/durable-execution.md +89 -0
- package/src/docs/principles/system-design/evolutionary-architecture.md +81 -0
- package/src/docs/principles/system-design/identity-and-access.md +76 -0
- package/src/docs/principles/system-design/integration-patterns.md +84 -0
- package/src/docs/principles/system-design/real-time.md +83 -0
- package/src/docs/principles/system-design/surface-architecture.md +74 -0
- package/src/docs/ways-of-working/documentation.md +69 -0
- package/src/docs/ways-of-working/how-we-work.md +76 -0
- package/src/docs/ways-of-working/units-of-work.md +40 -0
- package/src/engineer-skills/groundwork-electron-engineer/SKILL.md +118 -0
- package/src/engineer-skills/groundwork-electron-engineer/references/ipc-contracts.md +138 -0
- package/src/engineer-skills/groundwork-electron-engineer/references/packaging-and-updates.md +82 -0
- package/src/engineer-skills/groundwork-electron-engineer/references/process-model.md +94 -0
- package/src/engineer-skills/groundwork-electron-engineer/references/security.md +107 -0
- package/src/engineer-skills/groundwork-electron-engineer/references/testing-and-smoke.md +107 -0
- package/src/engineer-skills/groundwork-electron-engineer/references/theming-and-tokens.md +74 -0
- package/src/engineer-skills/groundwork-electron-engineer/sync-anchor.md +14 -0
- package/src/engineer-skills/groundwork-flutter-engineer/SKILL.md +108 -0
- package/src/engineer-skills/groundwork-flutter-engineer/references/accessibility.md +92 -0
- package/src/engineer-skills/groundwork-flutter-engineer/references/architecture.md +189 -0
- package/src/engineer-skills/groundwork-flutter-engineer/references/data-and-contracts.md +136 -0
- package/src/engineer-skills/groundwork-flutter-engineer/references/navigation.md +122 -0
- package/src/engineer-skills/groundwork-flutter-engineer/references/platform-channels.md +93 -0
- package/src/engineer-skills/groundwork-flutter-engineer/references/releases-and-distribution.md +84 -0
- package/src/engineer-skills/groundwork-flutter-engineer/references/state-management.md +166 -0
- package/src/engineer-skills/groundwork-flutter-engineer/references/testing.md +135 -0
- package/src/engineer-skills/groundwork-flutter-engineer/references/theming-and-design-tokens.md +109 -0
- package/src/engineer-skills/groundwork-flutter-engineer/references/widgets-and-composition.md +123 -0
- package/src/engineer-skills/groundwork-flutter-engineer/sync-anchor.md +15 -0
- package/src/engineer-skills/groundwork-go-engineer/SKILL.md +171 -0
- package/src/engineer-skills/groundwork-go-engineer/references/api-design.md +82 -0
- package/src/engineer-skills/groundwork-go-engineer/references/architecture.md +42 -0
- package/src/engineer-skills/groundwork-go-engineer/references/capability-ports.md +50 -0
- package/src/engineer-skills/groundwork-go-engineer/references/code-craft-security.md +34 -0
- package/src/engineer-skills/groundwork-go-engineer/references/concurrency.md +108 -0
- package/src/engineer-skills/groundwork-go-engineer/references/go-services.md +77 -0
- package/src/engineer-skills/groundwork-go-engineer/references/http-handlers.md +172 -0
- package/src/engineer-skills/groundwork-go-engineer/references/implementation-patterns.md +156 -0
- package/src/engineer-skills/groundwork-go-engineer/references/integration-realtime-data.md +57 -0
- package/src/engineer-skills/groundwork-go-engineer/references/observability.md +49 -0
- package/src/engineer-skills/groundwork-go-engineer/references/postgres.md +41 -0
- package/src/engineer-skills/groundwork-go-engineer/references/reliability-performance.md +105 -0
- package/src/engineer-skills/groundwork-go-engineer/references/testing.md +139 -0
- package/src/engineer-skills/groundwork-go-engineer/sync-anchor.md +11 -0
- package/src/engineer-skills/groundwork-nextjs-engineer/SKILL.md +107 -0
- package/src/engineer-skills/groundwork-nextjs-engineer/references/architecture.md +323 -0
- package/src/engineer-skills/groundwork-nextjs-engineer/references/data-fetching.md +458 -0
- package/src/engineer-skills/groundwork-nextjs-engineer/references/documentation.md +324 -0
- package/src/engineer-skills/groundwork-nextjs-engineer/references/error-boundaries.md +383 -0
- package/src/engineer-skills/groundwork-nextjs-engineer/references/mutations-and-forms.md +396 -0
- package/src/engineer-skills/groundwork-nextjs-engineer/references/performance-and-deployment.md +947 -0
- package/src/engineer-skills/groundwork-nextjs-engineer/references/routing-and-navigation.md +405 -0
- package/src/engineer-skills/groundwork-nextjs-engineer/references/server-components.md +394 -0
- package/src/engineer-skills/groundwork-nextjs-engineer/references/tailwind-and-styling.md +134 -0
- package/src/engineer-skills/groundwork-nextjs-engineer/references/testing.md +433 -0
- package/src/engineer-skills/groundwork-nextjs-engineer/references/type-system.md +368 -0
- package/src/engineer-skills/groundwork-nextjs-engineer/references/ux-principles.md +278 -0
- package/src/engineer-skills/groundwork-nextjs-engineer/references/visual-language.md +69 -0
- package/src/engineer-skills/groundwork-nextjs-engineer/sync-anchor.md +9 -0
- package/src/engineer-skills/groundwork-python-engineer/SKILL.md +196 -0
- package/src/engineer-skills/groundwork-python-engineer/references/api-standards.md +88 -0
- package/src/engineer-skills/groundwork-python-engineer/references/architecture.md +57 -0
- package/src/engineer-skills/groundwork-python-engineer/references/async-patterns.md +103 -0
- package/src/engineer-skills/groundwork-python-engineer/references/capability-ports.md +44 -0
- package/src/engineer-skills/groundwork-python-engineer/references/database.md +88 -0
- package/src/engineer-skills/groundwork-python-engineer/references/documentation-mcp.md +167 -0
- package/src/engineer-skills/groundwork-python-engineer/references/implementation-patterns.md +166 -0
- package/src/engineer-skills/groundwork-python-engineer/references/ml-pipelines.md +119 -0
- package/src/engineer-skills/groundwork-python-engineer/references/ml-systems-ai-engineering.md +74 -0
- package/src/engineer-skills/groundwork-python-engineer/references/observability.md +57 -0
- package/src/engineer-skills/groundwork-python-engineer/references/resilience.md +126 -0
- package/src/engineer-skills/groundwork-python-engineer/references/testing.md +177 -0
- package/src/engineer-skills/groundwork-python-engineer/sync-anchor.md +13 -0
- package/src/generators/add-capability/generator.ts +70 -0
- package/src/generators/add-capability/schema.json +30 -0
- package/src/generators/capabilities/llm/capability.json +28 -0
- package/src/generators/capabilities/llm/providers/anthropic/footprint.json +13 -0
- package/src/generators/capabilities/llm/providers/anthropic/stacks/go/internal/llm/llm.go.template +102 -0
- package/src/generators/capabilities/llm/providers/anthropic/stacks/python/src/__packageName__/adapters/llm.py.template +61 -0
- package/src/generators/capabilities/llm/providers/local/footprint.json +13 -0
- package/src/generators/capabilities/llm/providers/local/stacks/go/internal/llm/llm.go.template +102 -0
- package/src/generators/capabilities/llm/providers/local/stacks/python/src/__packageName__/adapters/llm.py.template +53 -0
- package/src/generators/capabilities/llm/providers/localai/footprint.json +29 -0
- package/src/generators/capabilities/llm/providers/localai/stacks/go/internal/llm/llm.go.template +102 -0
- package/src/generators/capabilities/llm/providers/localai/stacks/python/src/__packageName__/adapters/llm.py.template +53 -0
- package/src/generators/capabilities/llm/providers/none/footprint.json +9 -0
- package/src/generators/capabilities/llm/providers/none/stacks/go/internal/llm/llm.go.template +35 -0
- package/src/generators/capabilities/llm/providers/none/stacks/python/src/__packageName__/adapters/llm.py.template +25 -0
- package/src/generators/capabilities/llm/providers/ollama/footprint.json +20 -0
- package/src/generators/capabilities/llm/providers/ollama/stacks/go/internal/llm/llm.go.template +102 -0
- package/src/generators/capabilities/llm/providers/ollama/stacks/python/src/__packageName__/adapters/llm.py.template +53 -0
- package/src/generators/capabilities/llm/providers/openai/footprint.json +13 -0
- package/src/generators/capabilities/llm/providers/openai/stacks/go/internal/llm/llm.go.template +98 -0
- package/src/generators/capabilities/llm/providers/openai/stacks/python/src/__packageName__/adapters/llm.py.template +60 -0
- package/src/generators/capabilities/llm/stacks/go/internal/core/service/llm.go.template +12 -0
- package/src/generators/capabilities/llm/stacks/go/internal/llm/llm_test.go.template +33 -0
- package/src/generators/capabilities/llm/stacks/python/src/__packageName__/core/llm.py.template +15 -0
- package/src/generators/capabilities/llm/stacks/python/tests/contracts/test_llm.py.template +37 -0
- package/src/generators/cli-app/files/README.md.template +76 -0
- package/src/generators/cli-app/files/build.mjs.template +15 -0
- package/src/generators/cli-app/files/package.json.template +21 -0
- package/src/generators/cli-app/files/src/cli.ts.template +67 -0
- package/src/generators/cli-app/files/src/commands/hello.ts.template +17 -0
- package/src/generators/cli-app/files/src/commands/status.ts.template +23 -0
- package/src/generators/cli-app/files/src/core/client.test.ts.template +80 -0
- package/src/generators/cli-app/files/src/core/client.ts.template +64 -0
- package/src/generators/cli-app/files/src/registry.test.ts.template +35 -0
- package/src/generators/cli-app/files/src/registry.ts.template +31 -0
- package/src/generators/cli-app/files/tsconfig.json.template +16 -0
- package/src/generators/cli-app/files/tsconfig.test.json.template +11 -0
- package/src/generators/cli-app/generator.ts +138 -0
- package/src/generators/cli-app/schema.json +24 -0
- package/src/generators/docs-site/files/.gitignore.ejs +40 -0
- package/src/generators/docs-site/files/app/docs/__slug__/page.tsx +101 -0
- package/src/generators/docs-site/files/app/docs/layout.tsx +14 -0
- package/src/generators/docs-site/files/app/docs.css +43 -0
- package/src/generators/docs-site/files/app/layout.tsx +24 -0
- package/src/generators/docs-site/files/app/page.tsx +135 -0
- package/src/generators/docs-site/files/app/source.ts +8 -0
- package/src/generators/docs-site/files/components/mermaid.tsx +67 -0
- package/src/generators/docs-site/files/next.config.mjs +10 -0
- package/src/generators/docs-site/files/package.json +32 -0
- package/src/generators/docs-site/files/pnpm-workspace.yaml +7 -0
- package/src/generators/docs-site/files/postcss.config.mjs +6 -0
- package/src/generators/docs-site/files/source.config.ts +77 -0
- package/src/generators/docs-site/files/tailwind.config.js +10 -0
- package/src/generators/docs-site/files/tsconfig.json +27 -0
- package/src/generators/docs-site/generator.ts +476 -0
- package/src/generators/docs-site/schema.json +17 -0
- package/src/generators/electron-app/docs/principles/stack/electron/index.md +47 -0
- package/src/generators/electron-app/docs/principles/stack/electron/ipc-contracts.md +71 -0
- package/src/generators/electron-app/docs/principles/stack/electron/packaging-and-updates.md +59 -0
- package/src/generators/electron-app/docs/principles/stack/electron/process-model.md +53 -0
- package/src/generators/electron-app/docs/principles/stack/electron/security.md +70 -0
- package/src/generators/electron-app/docs/principles/stack/typescript/frontend.md +65 -0
- package/src/generators/electron-app/files/.gitignore.template +20 -0
- package/src/generators/electron-app/files/README.md.template +125 -0
- package/src/generators/electron-app/files/electron.vite.config.ts +31 -0
- package/src/generators/electron-app/files/eslint.config.mjs +92 -0
- package/src/generators/electron-app/files/forge.config.ts.template +44 -0
- package/src/generators/electron-app/files/package.json.template +54 -0
- package/src/generators/electron-app/files/playwright.config.ts +18 -0
- package/src/generators/electron-app/files/project.json.template +65 -0
- package/src/generators/electron-app/files/src/main/core-client.test.ts +81 -0
- package/src/generators/electron-app/files/src/main/core-client.ts +55 -0
- package/src/generators/electron-app/files/src/main/index.ts +157 -0
- package/src/generators/electron-app/files/src/main/ipc.ts +52 -0
- package/src/generators/electron-app/files/src/main/policy.test.ts +71 -0
- package/src/generators/electron-app/files/src/main/policy.ts +73 -0
- package/src/generators/electron-app/files/src/preload/index.ts +23 -0
- package/src/generators/electron-app/files/src/renderer/index.html.template +20 -0
- package/src/generators/electron-app/files/src/renderer/src/App.test.tsx +61 -0
- package/src/generators/electron-app/files/src/renderer/src/App.tsx.template +43 -0
- package/src/generators/electron-app/files/src/renderer/src/assets/main.css +40 -0
- package/src/generators/electron-app/files/src/renderer/src/env.d.ts +14 -0
- package/src/generators/electron-app/files/src/renderer/src/main.tsx +25 -0
- package/src/generators/electron-app/files/src/shared/ipc.ts +54 -0
- package/src/generators/electron-app/files/tests/smoke/app.spec.ts.template +68 -0
- package/src/generators/electron-app/files/tool/electron_exec.sh.template +83 -0
- package/src/generators/electron-app/files/tsconfig.json +7 -0
- package/src/generators/electron-app/files/tsconfig.node.json +27 -0
- package/src/generators/electron-app/files/tsconfig.web.json +22 -0
- package/src/generators/electron-app/files/vitest.config.ts +32 -0
- package/src/generators/electron-app/files/vitest.setup.ts +1 -0
- package/src/generators/electron-app/generator.ts +288 -0
- package/src/generators/electron-app/schema.json +23 -0
- package/src/generators/flutter-app/docs/principles/stack/flutter/architecture.md +78 -0
- package/src/generators/flutter-app/docs/principles/stack/flutter/index.md +38 -0
- package/src/generators/flutter-app/docs/principles/stack/flutter/platform-channels.md +51 -0
- package/src/generators/flutter-app/docs/principles/stack/flutter/releases-and-distribution.md +59 -0
- package/src/generators/flutter-app/docs/principles/stack/flutter/state-management.md +85 -0
- package/src/generators/flutter-app/docs/principles/stack/flutter/testing.md +74 -0
- package/src/generators/flutter-app/docs/principles/stack/flutter/widgets-and-composition.md +69 -0
- package/src/generators/flutter-app/files/.gitignore.template +30 -0
- package/src/generators/flutter-app/files/README.md.template +100 -0
- package/src/generators/flutter-app/files/analysis_options.yaml.template +18 -0
- package/src/generators/flutter-app/files/integration_test/app_test.dart.template +30 -0
- package/src/generators/flutter-app/files/lib/app.dart.template +24 -0
- package/src/generators/flutter-app/files/lib/config/app_config.dart +15 -0
- package/src/generators/flutter-app/files/lib/data/repositories/status_repository.dart +36 -0
- package/src/generators/flutter-app/files/lib/data/services/api_client.dart +71 -0
- package/src/generators/flutter-app/files/lib/domain/models/health_status.dart +23 -0
- package/src/generators/flutter-app/files/lib/main.dart +11 -0
- package/src/generators/flutter-app/files/lib/router.dart +23 -0
- package/src/generators/flutter-app/files/lib/ui/core/theme/app_theme.dart +110 -0
- package/src/generators/flutter-app/files/lib/ui/home/home_view.dart +89 -0
- package/src/generators/flutter-app/files/lib/ui/home/home_view_model.dart.template +38 -0
- package/src/generators/flutter-app/files/project.json.template +51 -0
- package/src/generators/flutter-app/files/pubspec.yaml.template +47 -0
- package/src/generators/flutter-app/files/test/api_client_test.dart.template +63 -0
- package/src/generators/flutter-app/files/test/fakes/fake_status_repository.dart.template +19 -0
- package/src/generators/flutter-app/files/test/home_view_test.dart.template +58 -0
- package/src/generators/flutter-app/files/tool/flutter_exec.sh.template +60 -0
- package/src/generators/flutter-app/generator.ts +362 -0
- package/src/generators/flutter-app/schema.json +23 -0
- package/src/generators/go-microservice/docs/principles/stack/go/concurrency.md +123 -0
- package/src/generators/go-microservice/docs/principles/stack/go/index.md +70 -0
- package/src/generators/go-microservice/docs/principles/stack/go/testing.md +152 -0
- package/src/generators/go-microservice/files/.air.toml.template +38 -0
- package/src/generators/go-microservice/files/.env.template +4 -0
- package/src/generators/go-microservice/files/.golangci.yml.template +82 -0
- package/src/generators/go-microservice/files/Dockerfile.dev.template +12 -0
- package/src/generators/go-microservice/files/asyncapi-pubsub.yaml.template +33 -0
- package/src/generators/go-microservice/files/asyncapi-ws.yaml.template +34 -0
- package/src/generators/go-microservice/files/cmd/api/main.go.template +149 -0
- package/src/generators/go-microservice/files/cmd/api/main_test.go.template +99 -0
- package/src/generators/go-microservice/files/cmd/worker/cleanup/main.go.template +39 -0
- package/src/generators/go-microservice/files/db/schema.sql.template +24 -0
- package/src/generators/go-microservice/files/go.mod.template +39 -0
- package/src/generators/go-microservice/files/internal/config/config.go.template +52 -0
- package/src/generators/go-microservice/files/internal/config/otel.go.template +93 -0
- package/src/generators/go-microservice/files/internal/core/domain/errors.go.template +16 -0
- package/src/generators/go-microservice/files/internal/core/domain/model.go.template +28 -0
- package/src/generators/go-microservice/files/internal/core/domain/user.go.template +13 -0
- package/src/generators/go-microservice/files/internal/core/pagination.go.template +16 -0
- package/src/generators/go-microservice/files/internal/core/service/app_service.go.template +79 -0
- package/src/generators/go-microservice/files/internal/core/service/event_hub.go.template +9 -0
- package/src/generators/go-microservice/files/internal/core/service/message_queue.go.template +10 -0
- package/src/generators/go-microservice/files/internal/core/service/outbox_repository.go.template +31 -0
- package/src/generators/go-microservice/files/internal/core/service/repository.go.template +23 -0
- package/src/generators/go-microservice/files/internal/core/service/user_repository.go.template +15 -0
- package/src/generators/go-microservice/files/internal/core/service/user_service.go.template +43 -0
- package/src/generators/go-microservice/files/internal/entrypoints/api/app_handler.go.template +108 -0
- package/src/generators/go-microservice/files/internal/entrypoints/api/auth_middleware_test.go.template +52 -0
- package/src/generators/go-microservice/files/internal/entrypoints/api/clerk_webhook.go.template +202 -0
- package/src/generators/go-microservice/files/internal/entrypoints/api/clerk_webhook_test.go.template +82 -0
- package/src/generators/go-microservice/files/internal/entrypoints/api/health_handler.go.template +80 -0
- package/src/generators/go-microservice/files/internal/entrypoints/api/idempotency/middleware.go.template +87 -0
- package/src/generators/go-microservice/files/internal/entrypoints/api/idempotency/middleware_test.go.template +76 -0
- package/src/generators/go-microservice/files/internal/entrypoints/api/idempotency/repository.go.template +37 -0
- package/src/generators/go-microservice/files/internal/entrypoints/api/middleware_auth.go.template +40 -0
- package/src/generators/go-microservice/files/internal/entrypoints/api/middleware_loadshed.go.template +38 -0
- package/src/generators/go-microservice/files/internal/entrypoints/api/middleware_logging.go.template +40 -0
- package/src/generators/go-microservice/files/internal/entrypoints/api/middleware_ratelimit.go.template +48 -0
- package/src/generators/go-microservice/files/internal/entrypoints/api/middleware_test.go.template +81 -0
- package/src/generators/go-microservice/files/internal/entrypoints/api/router.go.template +105 -0
- package/src/generators/go-microservice/files/internal/entrypoints/api/types.go.template +70 -0
- package/src/generators/go-microservice/files/internal/entrypoints/api/websocket_handler.go.template +39 -0
- package/src/generators/go-microservice/files/internal/httpclient/http_client.go.template +87 -0
- package/src/generators/go-microservice/files/internal/kafka/kafka.go.template +34 -0
- package/src/generators/go-microservice/files/internal/postgres/postgres.go.template +195 -0
- package/src/generators/go-microservice/files/internal/postgres/postgres_test.go.template +156 -0
- package/src/generators/go-microservice/files/internal/postgres/user_repository.go.template +56 -0
- package/src/generators/go-microservice/files/internal/pubsub/gcp_pubsub.go.template +35 -0
- package/src/generators/go-microservice/files/internal/websocket/client.go.template +151 -0
- package/src/generators/go-microservice/files/internal/websocket/hub.go.template +261 -0
- package/src/generators/go-microservice/files/scripts/apply-schema.sh.template +21 -0
- package/src/generators/go-microservice/files/tools/tools.go.template +10 -0
- package/src/generators/go-microservice/generator.ts +240 -0
- package/src/generators/go-microservice/schema.json +63 -0
- package/src/generators/nextjs-app/docs/principles/stack/typescript/frontend.md +65 -0
- package/src/generators/nextjs-app/files/.dockerignore.template +7 -0
- package/src/generators/nextjs-app/files/.env.example.template +24 -0
- package/src/generators/nextjs-app/files/.gitignore.template +5 -0
- package/src/generators/nextjs-app/files/Dockerfile +53 -0
- package/src/generators/nextjs-app/files/app/(auth)/sign-in/__sign-in__/page.tsx.template +9 -0
- package/src/generators/nextjs-app/files/app/(auth)/sign-up/__sign-up__/page.tsx.template +9 -0
- package/src/generators/nextjs-app/files/app/api/config/route.ts.template +39 -0
- package/src/generators/nextjs-app/files/app/api/healthz/route.test.ts +15 -0
- package/src/generators/nextjs-app/files/app/api/healthz/route.ts +5 -0
- package/src/generators/nextjs-app/files/app/api/proxy/__path__/route.test.ts.template +55 -0
- package/src/generators/nextjs-app/files/app/api/proxy/__path__/route.ts.template +126 -0
- package/src/generators/nextjs-app/files/app/error.tsx +39 -0
- package/src/generators/nextjs-app/files/app/global-error.tsx +68 -0
- package/src/generators/nextjs-app/files/app/globals.css +105 -0
- package/src/generators/nextjs-app/files/app/layout.tsx +59 -0
- package/src/generators/nextjs-app/files/app/loading.tsx +13 -0
- package/src/generators/nextjs-app/files/app/not-found.tsx +30 -0
- package/src/generators/nextjs-app/files/app/page.tsx +20 -0
- package/src/generators/nextjs-app/files/components/providers/default.tsx +19 -0
- package/src/generators/nextjs-app/files/components/providers/production.tsx +32 -0
- package/src/generators/nextjs-app/files/components/providers/telemetry.tsx +76 -0
- package/src/generators/nextjs-app/files/components/render-smoke.test.tsx +29 -0
- package/src/generators/nextjs-app/files/components/theme-provider.tsx +11 -0
- package/src/generators/nextjs-app/files/components.json +21 -0
- package/src/generators/nextjs-app/files/eslint.config.mjs +120 -0
- package/src/generators/nextjs-app/files/hooks/use-toast.ts +7 -0
- package/src/generators/nextjs-app/files/instrumentation.ts +90 -0
- package/src/generators/nextjs-app/files/lib/api/fetcher.ts.template +130 -0
- package/src/generators/nextjs-app/files/lib/config.ts +21 -0
- package/src/generators/nextjs-app/files/lib/logger.ts +29 -0
- package/src/generators/nextjs-app/files/lib/schemas/index.ts +19 -0
- package/src/generators/nextjs-app/files/lib/utils.ts +6 -0
- package/src/generators/nextjs-app/files/next.config.mjs +9 -0
- package/src/generators/nextjs-app/files/package.json +70 -0
- package/src/generators/nextjs-app/files/postcss.config.mjs +8 -0
- package/src/generators/nextjs-app/files/proxy.test.ts.template +30 -0
- package/src/generators/nextjs-app/files/proxy.ts +31 -0
- package/src/generators/nextjs-app/files/public/.gitkeep +1 -0
- package/src/generators/nextjs-app/files/tsconfig.json +42 -0
- package/src/generators/nextjs-app/files/vitest.config.mts +15 -0
- package/src/generators/nextjs-app/files/vitest.setup.ts +7 -0
- package/src/generators/nextjs-app/generator.ts +307 -0
- package/src/generators/nextjs-app/schema.json +44 -0
- package/src/generators/python-microservice/docs/principles/stack/python/async.md +168 -0
- package/src/generators/python-microservice/docs/principles/stack/python/documentation.md +240 -0
- package/src/generators/python-microservice/docs/principles/stack/python/mcp.md +147 -0
- package/src/generators/python-microservice/docs/principles/stack/python/resilience.md +193 -0
- package/src/generators/python-microservice/docs/principles/stack/python/testing.md +281 -0
- package/src/generators/python-microservice/files/.env.example.template +30 -0
- package/src/generators/python-microservice/files/Dockerfile.template +36 -0
- package/src/generators/python-microservice/files/db/schema.sql.template +19 -0
- package/src/generators/python-microservice/files/pyproject.toml.template +76 -0
- package/src/generators/python-microservice/files/scripts/apply-schema.sh.template +25 -0
- package/src/generators/python-microservice/files/src/__packageName__/adapters/comfyui.py.template +87 -0
- package/src/generators/python-microservice/files/src/__packageName__/adapters/config.py.template +48 -0
- package/src/generators/python-microservice/files/src/__packageName__/adapters/database.py.template +21 -0
- package/src/generators/python-microservice/files/src/__packageName__/adapters/message_queue.py.template +29 -0
- package/src/generators/python-microservice/files/src/__packageName__/adapters/repository.py.template +130 -0
- package/src/generators/python-microservice/files/src/__packageName__/adapters/telemetry.py.template +68 -0
- package/src/generators/python-microservice/files/src/__packageName__/adapters/websocket_hub.py.template +36 -0
- package/src/generators/python-microservice/files/src/__packageName__/core/domain/entities.py.template +22 -0
- package/src/generators/python-microservice/files/src/__packageName__/core/domain/exceptions.py.template +43 -0
- package/src/generators/python-microservice/files/src/__packageName__/core/ports.py.template +42 -0
- package/src/generators/python-microservice/files/src/__packageName__/core/service/example_service.py.template +68 -0
- package/src/generators/python-microservice/files/src/__packageName__/entrypoints/api/dependencies.py.template +50 -0
- package/src/generators/python-microservice/files/src/__packageName__/entrypoints/api/middleware.py.template +131 -0
- package/src/generators/python-microservice/files/src/__packageName__/entrypoints/api/router.py.template +37 -0
- package/src/generators/python-microservice/files/src/__packageName__/entrypoints/api/websocket_handler.py.template +20 -0
- package/src/generators/python-microservice/files/src/__packageName__/entrypoints/worker/cleanup.py.template +35 -0
- package/src/generators/python-microservice/files/src/__packageName__/entrypoints/worker/worker.py.template +28 -0
- package/src/generators/python-microservice/files/src/__packageName__/main.py.template +108 -0
- package/src/generators/python-microservice/files/tests/test_main.py.template +74 -0
- package/src/generators/python-microservice/files/tests/test_middleware.py.template +109 -0
- package/src/generators/python-microservice/files/tests/test_worker.py.template +16 -0
- package/src/generators/python-microservice/generator.ts +286 -0
- package/src/generators/python-microservice/schema.json +86 -0
- package/src/generators/shared/brand-tokens.ts +301 -0
- package/src/generators/shared/capabilities.ts +349 -0
- package/src/generators/shared/provenance.ts +61 -0
- package/src/generators/shared/scaffold-helpers.ts +309 -0
- package/src/generators/system-test-runner/files/tests/bets/.gitkeep +0 -0
- package/src/generators/system-test-runner/files/tests/bets/_archive/.gitkeep +0 -0
- package/src/generators/system-test-runner/files/tests/conftest.py.template +503 -0
- package/src/generators/system-test-runner/files/tests/pyproject.toml.template +20 -0
- package/src/generators/system-test-runner/files/tests/system/pages/__init__.py.template +9 -0
- package/src/generators/system-test-runner/files/tests/system/pages/base_page.py.template +36 -0
- package/src/generators/system-test-runner/files/tests/system/test_a11y_smoke.py.template +132 -0
- package/src/generators/system-test-runner/files/tests/system/test_contract_conformance.py.template +140 -0
- package/src/generators/system-test-runner/files/tests/system/test_layout_geometry.py.template +109 -0
- package/src/generators/system-test-runner/files/tests/system/test_render_smoke.py.template +227 -0
- package/src/generators/system-test-runner/files/tests/system/test_system.py.template +158 -0
- package/src/generators/system-test-runner/files/tests/system/test_token_conformance.py.template +206 -0
- package/src/generators/system-test-runner/files/tests/system/test_visual_regression.py.template +104 -0
- package/src/generators/system-test-runner/generator.ts +142 -0
- package/src/generators/system-test-runner/schema.json +24 -0
- package/src/generators/workspace-dev-cli/cli-src/build.mjs +42 -0
- package/src/generators/workspace-dev-cli/cli-src/dist/dev-bundle.js +2168 -0
- package/src/generators/workspace-dev-cli/cli-src/src/commands/bet.ts +442 -0
- package/src/generators/workspace-dev-cli/cli-src/src/commands/completion.ts +87 -0
- package/src/generators/workspace-dev-cli/cli-src/src/commands/doctor.ts +139 -0
- package/src/generators/workspace-dev-cli/cli-src/src/commands/lifecycle.ts +548 -0
- package/src/generators/workspace-dev-cli/cli-src/src/commands/quality.ts +127 -0
- package/src/generators/workspace-dev-cli/cli-src/src/commands/surface.ts +214 -0
- package/src/generators/workspace-dev-cli/cli-src/src/index.ts +127 -0
- package/src/generators/workspace-dev-cli/cli-src/src/registry.ts +194 -0
- package/src/generators/workspace-dev-cli/cli-src/src/theme/color.ts +130 -0
- package/src/generators/workspace-dev-cli/cli-src/src/theme/render.ts +158 -0
- package/src/generators/workspace-dev-cli/cli-src/src/theme/tokens.ts +122 -0
- package/src/generators/workspace-dev-cli/cli-src/src/util/context.ts +43 -0
- package/src/generators/workspace-dev-cli/cli-src/src/util/extensions.ts +99 -0
- package/src/generators/workspace-dev-cli/cli-src/src/util/paths.ts +46 -0
- package/src/generators/workspace-dev-cli/cli-src/src/util/proc.ts +106 -0
- package/src/generators/workspace-dev-cli/cli-src/src/util/prompt.ts +108 -0
- package/src/generators/workspace-dev-cli/cli-src/src/util/runners.ts +70 -0
- package/src/generators/workspace-dev-cli/cli-src/src/util/services.ts +221 -0
- package/src/generators/workspace-dev-cli/cli-src/src/util/version.ts +21 -0
- package/src/generators/workspace-dev-cli/cli-src/tsconfig.json +16 -0
- package/src/generators/workspace-dev-cli/files/.agents/skills/workspace-cli/SKILL.md.template +74 -0
- package/src/generators/workspace-dev-cli/files/dev.template +16 -0
- package/src/generators/workspace-dev-cli/files/docker-compose.yml.template +20 -0
- package/src/generators/workspace-dev-cli/files/scripts/cli/templates/milestone-test.pytmpl.template +46 -0
- package/src/generators/workspace-dev-cli/files/scripts/cli/templates/slice-test.pytmpl.template +38 -0
- package/src/generators/workspace-dev-cli/generator.ts +136 -0
- package/src/generators/workspace-dev-cli/schema.json +22 -0
- package/src/hidden-skills/code-intelligence.md +129 -0
- package/src/hidden-skills/groundwork-architect/SKILL.md +114 -0
- package/src/hidden-skills/groundwork-architect/references/agentic-systems.md +44 -0
- package/src/hidden-skills/groundwork-architect/references/ai-native-architecture.md +37 -0
- package/src/hidden-skills/groundwork-architect/references/api-and-contracts.md +45 -0
- package/src/hidden-skills/groundwork-architect/references/core-and-boundaries.md +45 -0
- package/src/hidden-skills/groundwork-architect/references/data-architecture.md +33 -0
- package/src/hidden-skills/groundwork-architect/references/decision-records.md +34 -0
- package/src/hidden-skills/groundwork-architect/references/durable-execution.md +45 -0
- package/src/hidden-skills/groundwork-architect/references/evolutionary-architecture.md +37 -0
- package/src/hidden-skills/groundwork-architect/references/identity-and-access.md +41 -0
- package/src/hidden-skills/groundwork-architect/references/integration-patterns.md +39 -0
- package/src/hidden-skills/groundwork-architect/references/observability.md +36 -0
- package/src/hidden-skills/groundwork-architect/references/performance-and-scale.md +41 -0
- package/src/hidden-skills/groundwork-architect/references/platform-and-delivery.md +47 -0
- package/src/hidden-skills/groundwork-architect/references/realtime-and-async.md +28 -0
- package/src/hidden-skills/groundwork-architect/references/reliability.md +31 -0
- package/src/hidden-skills/groundwork-architect/references/security-and-trust.md +47 -0
- package/src/hidden-skills/groundwork-architect/references/surface-architecture.md +40 -0
- package/src/hidden-skills/groundwork-architect/sync-anchor.md +34 -0
- package/src/hidden-skills/groundwork-architecture/architecture-template.md +50 -0
- package/src/hidden-skills/groundwork-architecture/instructions.md +139 -0
- package/src/hidden-skills/groundwork-architecture/phases/01-context-ingestion.md +18 -0
- package/src/hidden-skills/groundwork-architecture/phases/02-technical-constraints.md +27 -0
- package/src/hidden-skills/groundwork-architecture/phases/03-service-design.md +19 -0
- package/src/hidden-skills/groundwork-architecture/phases/04-data-flow-communication.md +23 -0
- package/src/hidden-skills/groundwork-architecture/phases/05-component-boundaries-contracts.md +17 -0
- package/src/hidden-skills/groundwork-architecture/phases/06-draft-review-present.md +38 -0
- package/src/hidden-skills/groundwork-architecture/phases/07-commit.md +33 -0
- package/src/hidden-skills/groundwork-architecture/templates/architecture-cache.md +43 -0
- package/src/hidden-skills/groundwork-architecture-extract/instructions.md +163 -0
- package/src/hidden-skills/groundwork-architecture-extract/templates/architecture-extract-cache.md +21 -0
- package/src/hidden-skills/groundwork-bet/briefs/slice-worker.md +191 -0
- package/src/hidden-skills/groundwork-bet/instructions.md +88 -0
- package/src/hidden-skills/groundwork-bet/templates/bet-progress-test.md +126 -0
- package/src/hidden-skills/groundwork-bet/templates/change-proposal.md +38 -0
- package/src/hidden-skills/groundwork-bet/templates/decomposition/meta.json +4 -0
- package/src/hidden-skills/groundwork-bet/templates/decomposition/milestone-index.md +35 -0
- package/src/hidden-skills/groundwork-bet/templates/decomposition/slice.md +35 -0
- package/src/hidden-skills/groundwork-bet/templates/pitch.md +45 -0
- package/src/hidden-skills/groundwork-bet/templates/technical-design/01-ui-design.md +51 -0
- package/src/hidden-skills/groundwork-bet/templates/technical-design/02-data-flows.md +36 -0
- package/src/hidden-skills/groundwork-bet/templates/technical-design/03-api-design.md +90 -0
- package/src/hidden-skills/groundwork-bet/templates/technical-design/04-data-design.md +29 -0
- package/src/hidden-skills/groundwork-bet/workflows/01-discovery.md +198 -0
- package/src/hidden-skills/groundwork-bet/workflows/02-design.md +168 -0
- package/src/hidden-skills/groundwork-bet/workflows/03-decomposition.md +246 -0
- package/src/hidden-skills/groundwork-bet/workflows/04-delivery.md +193 -0
- package/src/hidden-skills/groundwork-bet/workflows/05-validation.md +199 -0
- package/src/hidden-skills/groundwork-design-system/instructions.md +125 -0
- package/src/hidden-skills/groundwork-design-system/templates/brand-tokens.md +182 -0
- package/src/hidden-skills/groundwork-design-system/templates/design-system-cache.md +64 -0
- package/src/hidden-skills/groundwork-design-system/tracks/_foundation.md +136 -0
- package/src/hidden-skills/groundwork-design-system/tracks/agentic-protocol.md +269 -0
- package/src/hidden-skills/groundwork-design-system/tracks/cli.md +355 -0
- package/src/hidden-skills/groundwork-design-system/tracks/graphical-ui.md +330 -0
- package/src/hidden-skills/groundwork-design-system-extract/instructions.md +124 -0
- package/src/hidden-skills/groundwork-design-system-extract/templates/design-system-extract-cache.md +19 -0
- package/src/hidden-skills/groundwork-designer/SKILL.md +108 -0
- package/src/hidden-skills/groundwork-designer/references/accessibility.md +33 -0
- package/src/hidden-skills/groundwork-designer/references/ai-native-design.md +37 -0
- package/src/hidden-skills/groundwork-designer/references/design-review.md +29 -0
- package/src/hidden-skills/groundwork-designer/references/design-systems-and-tokens.md +33 -0
- package/src/hidden-skills/groundwork-designer/references/interaction-and-motion.md +37 -0
- package/src/hidden-skills/groundwork-designer/references/layout-and-space.md +33 -0
- package/src/hidden-skills/groundwork-designer/references/usability-and-ux.md +33 -0
- package/src/hidden-skills/groundwork-designer/references/visual-craft.md +49 -0
- package/src/hidden-skills/groundwork-designer/sync-anchor.md +20 -0
- package/src/hidden-skills/groundwork-doc-sync/instructions.md +100 -0
- package/src/hidden-skills/groundwork-elicit/instructions.md +66 -0
- package/src/hidden-skills/groundwork-elicit/methods.md +65 -0
- package/src/hidden-skills/groundwork-infra-adopt/instructions.md +168 -0
- package/src/hidden-skills/groundwork-infra-adopt/templates/infra-adopt-cache.md +21 -0
- package/src/hidden-skills/groundwork-mvp/instructions.md +223 -0
- package/src/hidden-skills/groundwork-mvp/templates/mvp-cache.md +9 -0
- package/src/hidden-skills/groundwork-patch/instructions.md +40 -0
- package/src/hidden-skills/groundwork-persona/instructions.md +54 -0
- package/src/hidden-skills/groundwork-product/SKILL.md +102 -0
- package/src/hidden-skills/groundwork-product/references/ai-native-product.md +45 -0
- package/src/hidden-skills/groundwork-product/references/discovery-and-opportunity.md +38 -0
- package/src/hidden-skills/groundwork-product/references/product-risks.md +52 -0
- package/src/hidden-skills/groundwork-product/references/requirements-and-specs.md +39 -0
- package/src/hidden-skills/groundwork-product/references/scope-and-sequencing.md +35 -0
- package/src/hidden-skills/groundwork-product/references/shaping-and-appetite.md +48 -0
- package/src/hidden-skills/groundwork-product/references/success-metrics-and-signals.md +37 -0
- package/src/hidden-skills/groundwork-product/sync-anchor.md +19 -0
- package/src/hidden-skills/groundwork-product-brief/instructions.md +231 -0
- package/src/hidden-skills/groundwork-product-brief-extract/instructions.md +139 -0
- package/src/hidden-skills/groundwork-product-brief-extract/templates/product-brief-extract-cache.md +17 -0
- package/src/hidden-skills/groundwork-review/checklists/architecture.md +93 -0
- package/src/hidden-skills/groundwork-review/checklists/bet-pitch.md +94 -0
- package/src/hidden-skills/groundwork-review/checklists/decomposition.md +135 -0
- package/src/hidden-skills/groundwork-review/checklists/design-system.md +85 -0
- package/src/hidden-skills/groundwork-review/checklists/domain-entity.md +66 -0
- package/src/hidden-skills/groundwork-review/checklists/implementation-readiness.md +46 -0
- package/src/hidden-skills/groundwork-review/checklists/infrastructure.md +68 -0
- package/src/hidden-skills/groundwork-review/checklists/maturity.md +71 -0
- package/src/hidden-skills/groundwork-review/checklists/product-brief.md +69 -0
- package/src/hidden-skills/groundwork-review/checklists/technical-design.md +112 -0
- package/src/hidden-skills/groundwork-review/instructions.md +181 -0
- package/src/hidden-skills/groundwork-scaffold/instructions.md +254 -0
- package/src/hidden-skills/groundwork-scaffold/phases/01-ingestion-service-mapping.md +87 -0
- package/src/hidden-skills/groundwork-scaffold/phases/02-scaffolding-execution.md +15 -0
- package/src/hidden-skills/groundwork-scaffold/phases/03-service-documentation-api-stubs.md +100 -0
- package/src/hidden-skills/groundwork-scaffold/phases/04-infrastructure-verification.md +17 -0
- package/src/hidden-skills/groundwork-scaffold/phases/05-draft-review.md +19 -0
- package/src/hidden-skills/groundwork-scaffold/phases/06-commit.md +19 -0
- package/src/hidden-skills/groundwork-scaffold/templates/scaffold-cache.md +23 -0
- package/src/hidden-skills/groundwork-scan/instructions.md +164 -0
- package/src/hidden-skills/groundwork-scan/references/digest-schema.md +66 -0
- package/src/hidden-skills/groundwork-scan/references/exclusions.md +44 -0
- package/src/hidden-skills/groundwork-scan/templates/architecture-findings.md +42 -0
- package/src/hidden-skills/groundwork-scan/templates/design-findings.md +23 -0
- package/src/hidden-skills/groundwork-scan/templates/overview.md +26 -0
- package/src/hidden-skills/groundwork-scan/templates/product-findings.md +23 -0
- package/src/hidden-skills/groundwork-scan/templates/scan-state.json +19 -0
- package/src/hidden-skills/groundwork-stack-forge/instructions.md +150 -0
- package/src/hidden-skills/groundwork-stack-forge/references/authoring-engineer-skills.md +107 -0
- package/src/hidden-skills/groundwork-surface-activation/instructions.md +138 -0
- package/src/hidden-skills/groundwork-update/briefs/reconcile-worker.md +196 -0
- package/src/hidden-skills/groundwork-update/instructions.md +200 -0
- package/src/hidden-skills/groundwork-writer/SKILL.md +278 -0
- package/src/hidden-skills/maturity-model.md +125 -0
- package/src/hidden-skills/operating-contract.md +400 -0
- package/src/hidden-skills/repo-map-schema.md +90 -0
- package/src/hidden-skills/templates/adr.md +57 -0
- package/src/hidden-skills/templates/capability-ports.md +71 -0
- package/src/hidden-skills/templates/discovery-notes.md +33 -0
- package/src/hidden-skills/templates/domain-entity.md +80 -0
- package/src/hidden-skills/templates/gap-ledger.md +21 -0
- package/src/hidden-skills/templates/handoff.md +37 -0
- package/src/hidden-skills/templates/maturity.md +39 -0
- package/src/hidden-skills/templates/surfaces.md +207 -0
- package/src/skills/groundwork-check/SKILL.md +56 -0
- package/src/skills/groundwork-check/instructions.md +70 -0
- package/src/skills/groundwork-orchestrator/SKILL.md +176 -0
- package/src/skills/groundwork-orchestrator/workflow-index.md +50 -0
|
@@ -0,0 +1,2168 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/generators/workspace-dev-cli/cli-src/src/index.ts
|
|
27
|
+
var fs11 = __toESM(require("fs"));
|
|
28
|
+
|
|
29
|
+
// src/generators/workspace-dev-cli/cli-src/src/registry.ts
|
|
30
|
+
var path9 = __toESM(require("path"));
|
|
31
|
+
|
|
32
|
+
// src/generators/workspace-dev-cli/cli-src/src/commands/lifecycle.ts
|
|
33
|
+
var fs4 = __toESM(require("fs"));
|
|
34
|
+
|
|
35
|
+
// src/generators/workspace-dev-cli/cli-src/src/util/context.ts
|
|
36
|
+
var CliError = class extends Error {
|
|
37
|
+
constructor(message, action) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.action = action;
|
|
40
|
+
this.name = "CliError";
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
var UsageError = class extends Error {
|
|
44
|
+
constructor(message) {
|
|
45
|
+
super(message);
|
|
46
|
+
this.name = "UsageError";
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
function elapsedSince(startMs) {
|
|
50
|
+
return `${Math.round((Date.now() - startMs) / 1e3)}s`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/generators/workspace-dev-cli/cli-src/src/util/proc.ts
|
|
54
|
+
var import_child_process = require("child_process");
|
|
55
|
+
var http = __toESM(require("http"));
|
|
56
|
+
function run(cmd, args, opts = {}) {
|
|
57
|
+
const r = (0, import_child_process.spawnSync)(cmd, args, { stdio: "inherit", ...opts });
|
|
58
|
+
return r.status ?? 1;
|
|
59
|
+
}
|
|
60
|
+
function capture(cmd, args, opts = {}) {
|
|
61
|
+
const r = (0, import_child_process.spawnSync)(cmd, args, { encoding: "utf8", ...opts });
|
|
62
|
+
return {
|
|
63
|
+
status: r.status ?? (r.error ? 127 : 1),
|
|
64
|
+
stdout: r.stdout ?? "",
|
|
65
|
+
stderr: r.stderr ?? ""
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function commandExists(cmd) {
|
|
69
|
+
const safe = cmd.replace(/[^a-zA-Z0-9._-]/g, "");
|
|
70
|
+
const probe = process.platform === "win32" ? `where ${safe}` : `command -v ${safe}`;
|
|
71
|
+
const r = (0, import_child_process.spawnSync)(probe, { shell: true, stdio: "ignore" });
|
|
72
|
+
return (r.status ?? 1) === 0;
|
|
73
|
+
}
|
|
74
|
+
var COMPOSE = ["compose"];
|
|
75
|
+
function dockerComposeCapture(args) {
|
|
76
|
+
return capture("docker", [...COMPOSE, ...args]);
|
|
77
|
+
}
|
|
78
|
+
function dockerComposeRun(args) {
|
|
79
|
+
return run("docker", [...COMPOSE, ...args]);
|
|
80
|
+
}
|
|
81
|
+
function spawnBackground(command, logStream, opts = {}) {
|
|
82
|
+
const child = (0, import_child_process.spawn)("bash", ["-c", command], {
|
|
83
|
+
stdio: ["ignore", logStream, logStream],
|
|
84
|
+
detached: true,
|
|
85
|
+
cwd: opts.cwd,
|
|
86
|
+
env: opts.env
|
|
87
|
+
});
|
|
88
|
+
child.unref();
|
|
89
|
+
return child.pid ?? -1;
|
|
90
|
+
}
|
|
91
|
+
function sleep(ms) {
|
|
92
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
93
|
+
}
|
|
94
|
+
function httpProbe(url, timeoutMs = 3e3) {
|
|
95
|
+
return new Promise((resolve) => {
|
|
96
|
+
let settled = false;
|
|
97
|
+
const done = (code) => {
|
|
98
|
+
if (settled) return;
|
|
99
|
+
settled = true;
|
|
100
|
+
resolve(code);
|
|
101
|
+
};
|
|
102
|
+
try {
|
|
103
|
+
const req = http.get(url, (res) => {
|
|
104
|
+
const code = res.statusCode ?? 0;
|
|
105
|
+
res.resume();
|
|
106
|
+
done(code);
|
|
107
|
+
});
|
|
108
|
+
req.setTimeout(timeoutMs, () => {
|
|
109
|
+
req.destroy();
|
|
110
|
+
done(0);
|
|
111
|
+
});
|
|
112
|
+
req.on("error", () => done(0));
|
|
113
|
+
} catch {
|
|
114
|
+
done(0);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/generators/workspace-dev-cli/cli-src/src/util/services.ts
|
|
120
|
+
var fs3 = __toESM(require("fs"));
|
|
121
|
+
var path2 = __toESM(require("path"));
|
|
122
|
+
|
|
123
|
+
// src/generators/workspace-dev-cli/cli-src/src/util/paths.ts
|
|
124
|
+
var path = __toESM(require("path"));
|
|
125
|
+
var fs = __toESM(require("fs"));
|
|
126
|
+
var ROOT = (() => {
|
|
127
|
+
if (process.env.DEV_ROOT) return process.env.DEV_ROOT;
|
|
128
|
+
let dir = process.cwd();
|
|
129
|
+
for (let i = 0; i < 12; i += 1) {
|
|
130
|
+
if (fs.existsSync(path.join(dir, "dev")) || fs.existsSync(path.join(dir, "docker-compose.yml")) || fs.existsSync(path.join(dir, ".groundwork"))) {
|
|
131
|
+
return dir;
|
|
132
|
+
}
|
|
133
|
+
const parent = path.dirname(dir);
|
|
134
|
+
if (parent === dir) break;
|
|
135
|
+
dir = parent;
|
|
136
|
+
}
|
|
137
|
+
return process.cwd();
|
|
138
|
+
})();
|
|
139
|
+
var DEV_DIR = path.join(ROOT, ".dev");
|
|
140
|
+
var PID_DIR = path.join(DEV_DIR, "pids");
|
|
141
|
+
var LOG_DIR = path.join(DEV_DIR, "logs");
|
|
142
|
+
var SERVICES_DIR = path.join(ROOT, "services");
|
|
143
|
+
var TESTS_DIR = path.join(ROOT, "tests");
|
|
144
|
+
var DOCS_DIR = path.join(ROOT, "docs");
|
|
145
|
+
var CONFIG_PATH = path.join(DEV_DIR, "dev.config.json");
|
|
146
|
+
var GROUNDWORK_SURFACES_FILE = path.join(ROOT, ".groundwork", "surfaces.json");
|
|
147
|
+
function ensureDirs() {
|
|
148
|
+
fs.mkdirSync(PID_DIR, { recursive: true });
|
|
149
|
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
150
|
+
}
|
|
151
|
+
function pidFile(svc) {
|
|
152
|
+
return path.join(PID_DIR, `${svc}.pid`);
|
|
153
|
+
}
|
|
154
|
+
function logFile(svc) {
|
|
155
|
+
return path.join(LOG_DIR, `${svc}.log`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/generators/workspace-dev-cli/cli-src/src/util/runners.ts
|
|
159
|
+
var fs2 = __toESM(require("fs"));
|
|
160
|
+
function parseRunners(raw) {
|
|
161
|
+
if (!Array.isArray(raw)) return [];
|
|
162
|
+
const out = [];
|
|
163
|
+
for (const item of raw) {
|
|
164
|
+
if (!item || typeof item !== "object") continue;
|
|
165
|
+
const r = item;
|
|
166
|
+
if (typeof r.name !== "string" || typeof r.cmd !== "string") continue;
|
|
167
|
+
out.push({
|
|
168
|
+
name: r.name,
|
|
169
|
+
kind: r.kind === "surface" ? "surface" : r.kind === "sidecar" ? "sidecar" : void 0,
|
|
170
|
+
cmd: r.cmd,
|
|
171
|
+
cwd: typeof r.cwd === "string" ? r.cwd : void 0,
|
|
172
|
+
env: r.env && typeof r.env === "object" && !Array.isArray(r.env) ? r.env : void 0,
|
|
173
|
+
health: r.health ?? null,
|
|
174
|
+
autostart: r.autostart === false ? false : true
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
function loadRunners() {
|
|
180
|
+
try {
|
|
181
|
+
if (!fs2.existsSync(CONFIG_PATH)) return [];
|
|
182
|
+
const raw = JSON.parse(fs2.readFileSync(CONFIG_PATH, "utf8"));
|
|
183
|
+
return parseRunners(raw.runners);
|
|
184
|
+
} catch {
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function runnerNames() {
|
|
189
|
+
return new Set(loadRunners().map((r) => r.name));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/generators/workspace-dev-cli/cli-src/src/util/services.ts
|
|
193
|
+
var composeServicesCache;
|
|
194
|
+
function getComposeServices() {
|
|
195
|
+
if (composeServicesCache !== void 0) return composeServicesCache;
|
|
196
|
+
const r = capture("docker", ["compose", "config", "--services"]);
|
|
197
|
+
composeServicesCache = r.status === 0 ? new Set(
|
|
198
|
+
r.stdout.split("\n").map((s) => s.trim()).filter(Boolean)
|
|
199
|
+
) : null;
|
|
200
|
+
return composeServicesCache;
|
|
201
|
+
}
|
|
202
|
+
function getAppServices() {
|
|
203
|
+
if (!fs3.existsSync(SERVICES_DIR)) return [];
|
|
204
|
+
const runners = runnerNames();
|
|
205
|
+
const dirs = fs3.readdirSync(SERVICES_DIR, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).filter((d) => !runners.has(d)).sort();
|
|
206
|
+
const compose = getComposeServices();
|
|
207
|
+
if (compose === null) return dirs;
|
|
208
|
+
return dirs.filter((d) => compose.has(d));
|
|
209
|
+
}
|
|
210
|
+
function getInfraServices() {
|
|
211
|
+
const app = new Set(getAppServices());
|
|
212
|
+
const r = capture("docker", ["compose", "config", "--services"]);
|
|
213
|
+
if (r.status !== 0) return [];
|
|
214
|
+
return r.stdout.split("\n").map((s) => s.trim()).filter(Boolean).filter((s) => !app.has(s));
|
|
215
|
+
}
|
|
216
|
+
function serviceDir(svc) {
|
|
217
|
+
return path2.join(SERVICES_DIR, svc);
|
|
218
|
+
}
|
|
219
|
+
function detectType(svc) {
|
|
220
|
+
const dir = serviceDir(svc);
|
|
221
|
+
if (fs3.existsSync(path2.join(dir, ".air.toml"))) return "go";
|
|
222
|
+
if (fs3.existsSync(path2.join(dir, "package.json"))) return "node";
|
|
223
|
+
if (fs3.existsSync(path2.join(dir, "pyproject.toml"))) return "python";
|
|
224
|
+
return "unknown";
|
|
225
|
+
}
|
|
226
|
+
function findPythonConfig(dir) {
|
|
227
|
+
const src = path2.join(dir, "src");
|
|
228
|
+
if (!fs3.existsSync(src)) return null;
|
|
229
|
+
const stack = [src];
|
|
230
|
+
while (stack.length > 0) {
|
|
231
|
+
const cur = stack.pop();
|
|
232
|
+
let entries;
|
|
233
|
+
try {
|
|
234
|
+
entries = fs3.readdirSync(cur, { withFileTypes: true });
|
|
235
|
+
} catch {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
for (const e of entries) {
|
|
239
|
+
const full = path2.join(cur, e.name);
|
|
240
|
+
if (e.isDirectory()) stack.push(full);
|
|
241
|
+
else if (e.name === "config.py") return full;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
function servicePort(svc) {
|
|
247
|
+
const dir = serviceDir(svc);
|
|
248
|
+
switch (detectType(svc)) {
|
|
249
|
+
case "go": {
|
|
250
|
+
const env = path2.join(dir, ".env");
|
|
251
|
+
if (fs3.existsSync(env)) {
|
|
252
|
+
const m = fs3.readFileSync(env, "utf8").match(/^(?:PORT|SERVER_PORT)=(\d+)/m);
|
|
253
|
+
if (m) return parseInt(m[1], 10);
|
|
254
|
+
}
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
case "node": {
|
|
258
|
+
const pkg = path2.join(dir, "package.json");
|
|
259
|
+
if (fs3.existsSync(pkg)) {
|
|
260
|
+
const m = fs3.readFileSync(pkg, "utf8").match(/next dev --port (\d+)/);
|
|
261
|
+
if (m) return parseInt(m[1], 10);
|
|
262
|
+
}
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
case "python": {
|
|
266
|
+
const cfg = findPythonConfig(dir);
|
|
267
|
+
if (cfg) {
|
|
268
|
+
const m = fs3.readFileSync(cfg, "utf8").match(/server_port:\s*int\s*=\s*(\d+)/);
|
|
269
|
+
if (m) return parseInt(m[1], 10);
|
|
270
|
+
}
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
default:
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function serviceHealthPath(svc) {
|
|
278
|
+
return detectType(svc) === "node" ? "/api/healthz" : "/health";
|
|
279
|
+
}
|
|
280
|
+
function bootCommand(svc) {
|
|
281
|
+
const dir = serviceDir(svc);
|
|
282
|
+
switch (detectType(svc)) {
|
|
283
|
+
case "go":
|
|
284
|
+
return `cd ${JSON.stringify(dir)} && air`;
|
|
285
|
+
case "node":
|
|
286
|
+
return `cd ${JSON.stringify(dir)} && ([ -d node_modules ] || npm install --legacy-peer-deps) && npm run dev`;
|
|
287
|
+
case "python":
|
|
288
|
+
return `cd ${JSON.stringify(dir)} && uv run python src/main.py`;
|
|
289
|
+
default:
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function readPid(svc) {
|
|
294
|
+
const f = pidFile(svc);
|
|
295
|
+
if (!fs3.existsSync(f)) return null;
|
|
296
|
+
const n = parseInt(fs3.readFileSync(f, "utf8").trim(), 10);
|
|
297
|
+
return Number.isFinite(n) ? n : null;
|
|
298
|
+
}
|
|
299
|
+
function pidAlive(pid) {
|
|
300
|
+
try {
|
|
301
|
+
process.kill(pid, 0);
|
|
302
|
+
return true;
|
|
303
|
+
} catch {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function isRunning(svc) {
|
|
308
|
+
const pid = readPid(svc);
|
|
309
|
+
if (pid === null) return false;
|
|
310
|
+
if (pidAlive(pid)) return true;
|
|
311
|
+
fs3.rmSync(pidFile(svc), { force: true });
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
function isDead(svc) {
|
|
315
|
+
const pid = readPid(svc);
|
|
316
|
+
return pid !== null && !pidAlive(pid);
|
|
317
|
+
}
|
|
318
|
+
function writePid(svc, pid) {
|
|
319
|
+
fs3.mkdirSync(PID_DIR, { recursive: true });
|
|
320
|
+
fs3.writeFileSync(pidFile(svc), `${pid}
|
|
321
|
+
`);
|
|
322
|
+
}
|
|
323
|
+
function removePid(svc) {
|
|
324
|
+
fs3.rmSync(pidFile(svc), { force: true });
|
|
325
|
+
}
|
|
326
|
+
async function killTree(pid) {
|
|
327
|
+
const r = capture("pgrep", ["-P", String(pid)]);
|
|
328
|
+
const children = r.stdout.split("\n").map((s) => parseInt(s.trim(), 10)).filter((n) => Number.isFinite(n));
|
|
329
|
+
for (const child of children) {
|
|
330
|
+
await killTree(child);
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
process.kill(pid, "SIGTERM");
|
|
334
|
+
} catch {
|
|
335
|
+
}
|
|
336
|
+
await sleep(1e3);
|
|
337
|
+
if (pidAlive(pid)) {
|
|
338
|
+
try {
|
|
339
|
+
process.kill(pid, "SIGKILL");
|
|
340
|
+
} catch {
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// src/generators/workspace-dev-cli/cli-src/src/commands/lifecycle.ts
|
|
346
|
+
var path3 = __toESM(require("path"));
|
|
347
|
+
async function start(ctx) {
|
|
348
|
+
const { r } = ctx;
|
|
349
|
+
const docker = ctx.args.includes("--docker");
|
|
350
|
+
ensureDirs();
|
|
351
|
+
const startMs = Date.now();
|
|
352
|
+
if (docker) {
|
|
353
|
+
r.startSpinner("Starting ALL services via Docker");
|
|
354
|
+
if (dockerComposeRun(["up", "-d", "--build"]) !== 0) {
|
|
355
|
+
r.failSpinner("Failed to start services");
|
|
356
|
+
throw new CliError("docker compose up failed", "Run './dev doctor' to verify Docker is running.");
|
|
357
|
+
}
|
|
358
|
+
r.stopSpinner("All services started via Docker", elapsedSince(startMs));
|
|
359
|
+
return 0;
|
|
360
|
+
}
|
|
361
|
+
const infra = getInfraServices();
|
|
362
|
+
const services = getAppServices();
|
|
363
|
+
const autostartRunners = ctx.runners.filter((x) => x.autostart !== false);
|
|
364
|
+
if (infra.length === 0 && services.length === 0 && autostartRunners.length === 0) {
|
|
365
|
+
r.warn("Nothing to start: no containerized services, native services, or runners are registered.");
|
|
366
|
+
r.info("Wire this app into ./dev so `start` runs it: register a runner in .dev/dev.config.json");
|
|
367
|
+
r.info("(name + launch command), or add a project command under .dev/commands/. See docs/architecture/infrastructure.md.");
|
|
368
|
+
return 0;
|
|
369
|
+
}
|
|
370
|
+
if (infra.length > 0) {
|
|
371
|
+
r.startSpinner("Starting Infrastructure (Docker)");
|
|
372
|
+
if (dockerComposeRun(["up", "-d", ...infra]) !== 0) {
|
|
373
|
+
r.failSpinner("Failed to start infrastructure");
|
|
374
|
+
throw new CliError("docker compose up failed", "Run './dev doctor' to verify Docker is running.");
|
|
375
|
+
}
|
|
376
|
+
r.stopSpinner("Infrastructure started");
|
|
377
|
+
}
|
|
378
|
+
for (const svc of services) {
|
|
379
|
+
if (isRunning(svc)) {
|
|
380
|
+
r.substep(`${svc} is already running`);
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
const cmd = bootCommand(svc);
|
|
384
|
+
if (!cmd) {
|
|
385
|
+
r.warn(`Unknown service type for ${svc}. Skipping.`);
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
r.startSpinner(`Booting ${svc}`);
|
|
389
|
+
const fd = fs4.openSync(logFile(svc), "a");
|
|
390
|
+
const pid = spawnBackground(cmd, fd);
|
|
391
|
+
fs4.closeSync(fd);
|
|
392
|
+
writePid(svc, pid);
|
|
393
|
+
await sleep(500);
|
|
394
|
+
if (readPid(svc) !== null && !isRunning(svc)) {
|
|
395
|
+
r.failSpinner(`${svc} failed to start`);
|
|
396
|
+
r.errorCard(`${svc} exited immediately`, `Check .dev/logs/${svc}.log for the cause.`);
|
|
397
|
+
} else {
|
|
398
|
+
r.stopSpinner(`${svc} started natively (PID ${pid})`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
for (const runner of autostartRunners) {
|
|
402
|
+
if (isRunning(runner.name)) {
|
|
403
|
+
r.substep(`${runner.name} is already running`);
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
r.startSpinner(`Booting ${runner.name}`);
|
|
407
|
+
const fd = fs4.openSync(logFile(runner.name), "a");
|
|
408
|
+
const env = runner.env ? { ...process.env, ...runner.env } : process.env;
|
|
409
|
+
const cwd = runner.cwd ? path3.join(ROOT, runner.cwd) : ROOT;
|
|
410
|
+
const pid = spawnBackground(runner.cmd, fd, { cwd, env });
|
|
411
|
+
fs4.closeSync(fd);
|
|
412
|
+
writePid(runner.name, pid);
|
|
413
|
+
await sleep(500);
|
|
414
|
+
if (readPid(runner.name) !== null && !isRunning(runner.name)) {
|
|
415
|
+
r.failSpinner(`${runner.name} failed to start`);
|
|
416
|
+
r.errorCard(`${runner.name} exited immediately`, `Check .dev/logs/${runner.name}.log for the cause.`);
|
|
417
|
+
} else {
|
|
418
|
+
r.stopSpinner(`${runner.name} started natively (PID ${pid})`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const parts = [];
|
|
422
|
+
if (infra.length) parts.push(`${infra.length} infra`);
|
|
423
|
+
if (services.length) parts.push(`${services.length} service${services.length > 1 ? "s" : ""}`);
|
|
424
|
+
if (autostartRunners.length) parts.push(`${autostartRunners.length} runner${autostartRunners.length > 1 ? "s" : ""}`);
|
|
425
|
+
r.success(`Development environment started \u2014 ${parts.join(", ")}. (${elapsedSince(startMs)})`);
|
|
426
|
+
r.info("Run './dev logs' to read service output.");
|
|
427
|
+
return 0;
|
|
428
|
+
}
|
|
429
|
+
async function stop(ctx) {
|
|
430
|
+
const { r } = ctx;
|
|
431
|
+
const startMs = Date.now();
|
|
432
|
+
r.startSpinner("Stopping environment");
|
|
433
|
+
for (const svc of getAppServices()) {
|
|
434
|
+
const pid = readPid(svc);
|
|
435
|
+
if (pid !== null && isRunning(svc)) {
|
|
436
|
+
await killTree(pid);
|
|
437
|
+
}
|
|
438
|
+
removePid(svc);
|
|
439
|
+
}
|
|
440
|
+
for (const runner of ctx.runners) {
|
|
441
|
+
const pid = readPid(runner.name);
|
|
442
|
+
if (pid !== null && isRunning(runner.name)) {
|
|
443
|
+
await killTree(pid);
|
|
444
|
+
}
|
|
445
|
+
removePid(runner.name);
|
|
446
|
+
}
|
|
447
|
+
if (dockerComposeRun(["down"]) !== 0) {
|
|
448
|
+
}
|
|
449
|
+
r.stopSpinner("Environment stopped", elapsedSince(startMs));
|
|
450
|
+
return 0;
|
|
451
|
+
}
|
|
452
|
+
async function clean(ctx) {
|
|
453
|
+
const { r } = ctx;
|
|
454
|
+
const hard = ctx.args.includes("--hard");
|
|
455
|
+
const startMs = Date.now();
|
|
456
|
+
r.startSpinner("Stopping services & wiping native state");
|
|
457
|
+
for (const svc of getAppServices()) {
|
|
458
|
+
const pid = readPid(svc);
|
|
459
|
+
if (pid !== null && isRunning(svc)) {
|
|
460
|
+
await killTree(pid);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
for (const runner of ctx.runners) {
|
|
464
|
+
const pid = readPid(runner.name);
|
|
465
|
+
if (pid !== null && isRunning(runner.name)) {
|
|
466
|
+
await killTree(pid);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
fs4.rmSync(PID_DIR, { recursive: true, force: true });
|
|
470
|
+
fs4.rmSync(LOG_DIR, { recursive: true, force: true });
|
|
471
|
+
ensureDirs();
|
|
472
|
+
r.stopSpinner("Wiped native state (.dev/pids, .dev/logs)");
|
|
473
|
+
r.startSpinner("Cleaning Docker environment");
|
|
474
|
+
if (hard) {
|
|
475
|
+
dockerComposeRun(["down", "-v", "--remove-orphans"]);
|
|
476
|
+
r.stopSpinner("Docker environment destroyed (volumes wiped)");
|
|
477
|
+
} else {
|
|
478
|
+
dockerComposeRun(["down", "--remove-orphans"]);
|
|
479
|
+
r.stopSpinner("Docker environment stopped (volumes preserved)");
|
|
480
|
+
}
|
|
481
|
+
r.success(`Workspace clean complete. (${elapsedSince(startMs)})`);
|
|
482
|
+
return 0;
|
|
483
|
+
}
|
|
484
|
+
async function reset(ctx) {
|
|
485
|
+
const { r } = ctx;
|
|
486
|
+
r.logo("Resetting Environment");
|
|
487
|
+
r.info("Full cycle: stop \u2192 clean (wipe volumes) \u2192 start \u2192 migrate.");
|
|
488
|
+
await stop(ctx);
|
|
489
|
+
await clean({ ...ctx, args: ["--hard"] });
|
|
490
|
+
await start(ctx);
|
|
491
|
+
await migrate(ctx);
|
|
492
|
+
r.success("Environment reset complete.");
|
|
493
|
+
return 0;
|
|
494
|
+
}
|
|
495
|
+
async function probeService(svc) {
|
|
496
|
+
const port = servicePort(svc);
|
|
497
|
+
if (port === null) return { service: svc, port: null, status: "unknown", code: 0 };
|
|
498
|
+
const code = await httpProbe(`http://localhost:${port}${serviceHealthPath(svc)}`);
|
|
499
|
+
return { service: svc, port, status: code === 200 ? "healthy" : "down", code };
|
|
500
|
+
}
|
|
501
|
+
function healthLabel(row) {
|
|
502
|
+
if (row.status === "unknown") return [row.service, "unknown port", "skipped"];
|
|
503
|
+
if (row.status === "healthy") return [row.service, "healthy", row.port ? `:${row.port}` : ""];
|
|
504
|
+
const detail = row.code ? `down (${row.code})` : "down";
|
|
505
|
+
return [row.service, detail, row.port ? `:${row.port}` : ""];
|
|
506
|
+
}
|
|
507
|
+
async function health(ctx) {
|
|
508
|
+
const { r } = ctx;
|
|
509
|
+
const json = ctx.json || ctx.args.includes("--json");
|
|
510
|
+
const services = await Promise.all(getAppServices().map(probeService));
|
|
511
|
+
const jcode = await httpProbe("http://localhost:16686/api/services");
|
|
512
|
+
const jaeger = {
|
|
513
|
+
service: "jaeger",
|
|
514
|
+
port: 16686,
|
|
515
|
+
status: jcode === 200 ? "healthy" : "down",
|
|
516
|
+
code: jcode
|
|
517
|
+
};
|
|
518
|
+
const unhealthy = [...services, jaeger].filter((row) => row.status !== "healthy").length;
|
|
519
|
+
if (json) {
|
|
520
|
+
process.stdout.write(
|
|
521
|
+
JSON.stringify({ ok: unhealthy === 0, services, observability: [jaeger] }, null, 2) + "\n"
|
|
522
|
+
);
|
|
523
|
+
return unhealthy === 0 ? 0 : 1;
|
|
524
|
+
}
|
|
525
|
+
r.logo("Service Health");
|
|
526
|
+
r.table("App Services", services.length ? services.map(healthLabel) : []);
|
|
527
|
+
r.table("Observability", [healthLabel(jaeger)]);
|
|
528
|
+
if (unhealthy === 0) {
|
|
529
|
+
r.success("All services healthy.");
|
|
530
|
+
return 0;
|
|
531
|
+
}
|
|
532
|
+
r.errorCard(
|
|
533
|
+
`${unhealthy} endpoint(s) unhealthy.`,
|
|
534
|
+
"Run './dev start' (or './dev start --docker') and retry."
|
|
535
|
+
);
|
|
536
|
+
return 1;
|
|
537
|
+
}
|
|
538
|
+
async function migrate(ctx) {
|
|
539
|
+
const { r } = ctx;
|
|
540
|
+
const composeServices = capture("docker", ["compose", "config", "--services"]);
|
|
541
|
+
const hasDb = composeServices.status === 0 && composeServices.stdout.split("\n").map((s) => s.trim()).includes("db");
|
|
542
|
+
if (!hasDb) {
|
|
543
|
+
r.info("No database in this workspace; nothing to migrate.");
|
|
544
|
+
return 0;
|
|
545
|
+
}
|
|
546
|
+
const db = `${ctx.projectPrefix}-db`;
|
|
547
|
+
r.startSpinner(`Waiting for database (${db})`);
|
|
548
|
+
let ready = false;
|
|
549
|
+
for (let i = 0; i < 120; i += 1) {
|
|
550
|
+
const res = capture("docker", ["inspect", "--format={{.State.Health.Status}}", db]);
|
|
551
|
+
if (res.status === 0 && res.stdout.trim() === "healthy") {
|
|
552
|
+
ready = true;
|
|
553
|
+
break;
|
|
554
|
+
}
|
|
555
|
+
await sleep(1e3);
|
|
556
|
+
}
|
|
557
|
+
if (!ready) {
|
|
558
|
+
r.failSpinner("Database did not become healthy");
|
|
559
|
+
throw new CliError(`Database ${db} not healthy after 120s`, "Run './dev start' and check 'docker compose ps'.");
|
|
560
|
+
}
|
|
561
|
+
r.stopSpinner("Database ready");
|
|
562
|
+
for (const svc of getAppServices()) {
|
|
563
|
+
const dbName = svc;
|
|
564
|
+
r.step(`Migrating ${svc} (db: ${dbName})`);
|
|
565
|
+
const exists = capture("docker", [
|
|
566
|
+
"exec",
|
|
567
|
+
db,
|
|
568
|
+
"psql",
|
|
569
|
+
"-U",
|
|
570
|
+
"postgres",
|
|
571
|
+
"-tc",
|
|
572
|
+
`SELECT 1 FROM pg_database WHERE datname='${dbName}'`
|
|
573
|
+
]);
|
|
574
|
+
if (exists.status === 0 && /\b1\b/.test(exists.stdout)) {
|
|
575
|
+
r.substep(`Database '${dbName}' already exists.`);
|
|
576
|
+
} else {
|
|
577
|
+
const created = capture("docker", ["exec", db, "psql", "-U", "postgres", "-c", `CREATE DATABASE "${dbName}";`]);
|
|
578
|
+
if (created.status === 0) {
|
|
579
|
+
r.substep(`Created database '${dbName}'.`);
|
|
580
|
+
} else {
|
|
581
|
+
r.warn(`Could not create database '${dbName}' (continuing).`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
const schemaScript = path3.join(serviceDir(svc), "scripts", "apply-schema.sh");
|
|
585
|
+
if (fs4.existsSync(schemaScript)) {
|
|
586
|
+
r.startSpinner(`Applying schema for ${svc}`);
|
|
587
|
+
const url = `postgres://postgres:postgres@localhost:5432/${dbName}?sslmode=disable`;
|
|
588
|
+
const applied = run("bash", ["scripts/apply-schema.sh"], {
|
|
589
|
+
cwd: serviceDir(svc),
|
|
590
|
+
env: { ...process.env, DATABASE_URL: url }
|
|
591
|
+
});
|
|
592
|
+
if (applied === 0) {
|
|
593
|
+
r.stopSpinner(`Schema applied for ${svc}`);
|
|
594
|
+
} else {
|
|
595
|
+
r.failSpinner(`Schema failed for ${svc}`);
|
|
596
|
+
throw new CliError(`apply-schema.sh failed for ${svc}`, "Check the schema script and database connectivity.");
|
|
597
|
+
}
|
|
598
|
+
} else {
|
|
599
|
+
r.substep(`No apply-schema.sh for ${svc}; database created only.`);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
r.success("Migrations complete.");
|
|
603
|
+
return 0;
|
|
604
|
+
}
|
|
605
|
+
function composeServiceExists(name) {
|
|
606
|
+
const res = dockerComposeCapture(["config", "--services"]);
|
|
607
|
+
if (res.status !== 0) return false;
|
|
608
|
+
return res.stdout.split("\n").map((s) => s.trim()).includes(name);
|
|
609
|
+
}
|
|
610
|
+
async function logs(ctx) {
|
|
611
|
+
const { r } = ctx;
|
|
612
|
+
const follow = ctx.args.includes("--follow") || ctx.args.includes("-f");
|
|
613
|
+
const target = ctx.args.find((a) => !a.startsWith("-"));
|
|
614
|
+
if (target) {
|
|
615
|
+
const f = logFile(target);
|
|
616
|
+
if (fs4.existsSync(f)) {
|
|
617
|
+
if (follow) {
|
|
618
|
+
if (!r.painter.caps.isTTY) {
|
|
619
|
+
throw new CliError(
|
|
620
|
+
"logs --follow requires an interactive terminal",
|
|
621
|
+
`Omit --follow to print recent logs, or read .dev/logs/${target}.log directly.`
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
r.info(`Streaming logs: ${target} (Ctrl+C to stop)...`);
|
|
625
|
+
return run("tail", ["-f", f]);
|
|
626
|
+
}
|
|
627
|
+
const tail = fs4.readFileSync(f, "utf8").split("\n").slice(-40).join("\n");
|
|
628
|
+
r.step(`${target} (last 40 lines)`);
|
|
629
|
+
process.stdout.write(tail + "\n");
|
|
630
|
+
return 0;
|
|
631
|
+
}
|
|
632
|
+
if (composeServiceExists(target)) {
|
|
633
|
+
if (follow) {
|
|
634
|
+
if (!r.painter.caps.isTTY) {
|
|
635
|
+
throw new CliError(
|
|
636
|
+
"logs --follow requires an interactive terminal",
|
|
637
|
+
"Omit --follow to print recent logs and exit."
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
return dockerComposeRun(["logs", "-f", target]);
|
|
641
|
+
}
|
|
642
|
+
return dockerComposeRun(["logs", "--tail", "40", target]);
|
|
643
|
+
}
|
|
644
|
+
throw new CliError(`Unknown service: ${target}`, "Run './dev status' to list known services.");
|
|
645
|
+
}
|
|
646
|
+
if (!follow) {
|
|
647
|
+
for (const svc of [...getAppServices(), ...ctx.runners.map((x) => x.name)]) {
|
|
648
|
+
const f = logFile(svc);
|
|
649
|
+
if (fs4.existsSync(f)) {
|
|
650
|
+
const lines = fs4.readFileSync(f, "utf8").split("\n");
|
|
651
|
+
const tail = lines.slice(-40).join("\n");
|
|
652
|
+
r.step(`${svc} (last 40 lines)`);
|
|
653
|
+
process.stdout.write(tail + "\n");
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
dockerComposeRun(["logs", "--tail", "40"]);
|
|
657
|
+
return 0;
|
|
658
|
+
}
|
|
659
|
+
if (!r.painter.caps.isTTY) {
|
|
660
|
+
throw new CliError("logs --follow requires an interactive terminal", "Omit --follow to print recent logs and exit, or read .dev/logs/*.log directly.");
|
|
661
|
+
}
|
|
662
|
+
r.info("Streaming logs (Ctrl+C to stop)...");
|
|
663
|
+
return dockerComposeRun(["logs", "-f"]);
|
|
664
|
+
}
|
|
665
|
+
function collectStatus(runners) {
|
|
666
|
+
const composePs = dockerComposeCapture(["ps", "--format", "{{.Service}}|{{.Status}}|{{.Ports}}"]);
|
|
667
|
+
const docker = composePs.stdout.split("\n").map((l) => l.trim()).filter(Boolean).map((l) => {
|
|
668
|
+
const [service = "", stat = "", ports = ""] = l.split("|");
|
|
669
|
+
return { service, status: stat, ports };
|
|
670
|
+
});
|
|
671
|
+
const native = [];
|
|
672
|
+
for (const svc of getAppServices()) {
|
|
673
|
+
if (isRunning(svc)) native.push({ service: svc, status: "running", pid: readPid(svc) });
|
|
674
|
+
}
|
|
675
|
+
for (const runner of runners) {
|
|
676
|
+
if (isRunning(runner.name)) native.push({ service: runner.name, status: "running", pid: readPid(runner.name) });
|
|
677
|
+
}
|
|
678
|
+
return { docker, native };
|
|
679
|
+
}
|
|
680
|
+
function renderStatusTables(r, data, runners) {
|
|
681
|
+
r.table(
|
|
682
|
+
"Docker Containers",
|
|
683
|
+
data.docker.map((d) => {
|
|
684
|
+
const ports = d.ports.length > 15 ? d.ports.slice(0, 12) + "..." : d.ports;
|
|
685
|
+
return [d.service, d.status, ports];
|
|
686
|
+
})
|
|
687
|
+
);
|
|
688
|
+
const nativeRows = [];
|
|
689
|
+
const running = new Set(data.native.map((n) => n.service));
|
|
690
|
+
for (const n of data.native) nativeRows.push([n.service, `PID ${n.pid}`, "native"]);
|
|
691
|
+
for (const svc of getAppServices()) {
|
|
692
|
+
if (!running.has(svc) && isDead(svc)) nativeRows.push([svc, "dead", "native"]);
|
|
693
|
+
}
|
|
694
|
+
for (const runner of runners) {
|
|
695
|
+
if (running.has(runner.name)) continue;
|
|
696
|
+
const label = runner.kind ?? "runner";
|
|
697
|
+
if (isDead(runner.name)) nativeRows.push([runner.name, "dead", label]);
|
|
698
|
+
else if (runner.autostart === false) nativeRows.push([runner.name, "not started", label]);
|
|
699
|
+
else nativeRows.push([runner.name, "stopped", label]);
|
|
700
|
+
}
|
|
701
|
+
r.table("Native Processes", nativeRows);
|
|
702
|
+
}
|
|
703
|
+
async function status(ctx) {
|
|
704
|
+
const { r } = ctx;
|
|
705
|
+
const json = ctx.json || ctx.args.includes("--json");
|
|
706
|
+
const watch = ctx.args.includes("--watch");
|
|
707
|
+
if (json) {
|
|
708
|
+
const data = collectStatus(ctx.runners);
|
|
709
|
+
const runners = ctx.runners.map((rn) => ({
|
|
710
|
+
name: rn.name,
|
|
711
|
+
kind: rn.kind ?? null,
|
|
712
|
+
state: isRunning(rn.name) ? "running" : isDead(rn.name) ? "dead" : "stopped",
|
|
713
|
+
pid: isRunning(rn.name) ? readPid(rn.name) : null,
|
|
714
|
+
autostart: rn.autostart !== false
|
|
715
|
+
}));
|
|
716
|
+
process.stdout.write(JSON.stringify({ ...data, runners }, null, 2) + "\n");
|
|
717
|
+
return 0;
|
|
718
|
+
}
|
|
719
|
+
if (watch && r.painter.caps.isTTY) {
|
|
720
|
+
return watchStatus(ctx);
|
|
721
|
+
}
|
|
722
|
+
if (watch && !r.painter.caps.isTTY) {
|
|
723
|
+
r.info("--watch requires an interactive terminal; printing a single snapshot.");
|
|
724
|
+
}
|
|
725
|
+
const ro = r.asStream(process.stdout);
|
|
726
|
+
ro.logo("Local Status");
|
|
727
|
+
renderStatusTables(ro, collectStatus(ctx.runners), ctx.runners);
|
|
728
|
+
return 0;
|
|
729
|
+
}
|
|
730
|
+
async function watchStatus(ctx) {
|
|
731
|
+
const { r } = ctx;
|
|
732
|
+
const readline2 = await import("readline");
|
|
733
|
+
const out = process.stderr;
|
|
734
|
+
out.write("\x1B[?1049h");
|
|
735
|
+
out.write("\x1B[?25l");
|
|
736
|
+
readline2.emitKeypressEvents(process.stdin);
|
|
737
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
738
|
+
process.stdin.resume();
|
|
739
|
+
let stop2 = false;
|
|
740
|
+
const restore = () => {
|
|
741
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
742
|
+
process.stdin.pause();
|
|
743
|
+
out.write("\x1B[?25h");
|
|
744
|
+
out.write("\x1B[?1049l");
|
|
745
|
+
};
|
|
746
|
+
const onKey = (_s, key) => {
|
|
747
|
+
if (key.name === "q" || key.ctrl && key.name === "c") stop2 = true;
|
|
748
|
+
};
|
|
749
|
+
process.stdin.on("keypress", onKey);
|
|
750
|
+
try {
|
|
751
|
+
while (!stop2) {
|
|
752
|
+
out.write("\x1B[2J\x1B[H");
|
|
753
|
+
r.logo("Live Status");
|
|
754
|
+
renderStatusTables(r, collectStatus(ctx.runners), ctx.runners);
|
|
755
|
+
out.write(`
|
|
756
|
+
${r.painter.dim("Refreshing every 2s \u2014 press q or Ctrl+C to exit")}
|
|
757
|
+
`);
|
|
758
|
+
for (let i = 0; i < 20 && !stop2; i += 1) await sleep(100);
|
|
759
|
+
}
|
|
760
|
+
} finally {
|
|
761
|
+
process.stdin.removeListener("keypress", onKey);
|
|
762
|
+
restore();
|
|
763
|
+
}
|
|
764
|
+
return 0;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// src/generators/workspace-dev-cli/cli-src/src/commands/quality.ts
|
|
768
|
+
var fs5 = __toESM(require("fs"));
|
|
769
|
+
var path4 = __toESM(require("path"));
|
|
770
|
+
function hasSystemTests() {
|
|
771
|
+
return fs5.existsSync(path4.join(TESTS_DIR, "system"));
|
|
772
|
+
}
|
|
773
|
+
async function test(ctx) {
|
|
774
|
+
const { r } = ctx;
|
|
775
|
+
const positional = ctx.args.filter((a) => !a.startsWith("-"));
|
|
776
|
+
const keep = ctx.args.includes("--keep");
|
|
777
|
+
const integrationFlag = ctx.args.includes("--integration");
|
|
778
|
+
const mode = positional[0];
|
|
779
|
+
if (!hasSystemTests()) {
|
|
780
|
+
r.logo("Tests");
|
|
781
|
+
r.info("No system tests found.");
|
|
782
|
+
return 0;
|
|
783
|
+
}
|
|
784
|
+
if (mode === "integration") {
|
|
785
|
+
r.logo("Running Integration Tests");
|
|
786
|
+
await start(ctx);
|
|
787
|
+
await migrate(ctx);
|
|
788
|
+
r.step("Running System Tests (pytest, REQUIRE_* enabled)");
|
|
789
|
+
const code2 = run("uv", ["run", "pytest", "system/"], {
|
|
790
|
+
cwd: TESTS_DIR,
|
|
791
|
+
env: { ...process.env, GROUNDWORK_REQUIRE_SERVICES: "1", GROUNDWORK_REQUIRE_TRACES: "1" }
|
|
792
|
+
});
|
|
793
|
+
if (!keep) await stop(ctx);
|
|
794
|
+
if (code2 !== 0) throw new CliError("Integration tests failed", "Inspect the pytest output above.");
|
|
795
|
+
r.success("Integration tests passed.");
|
|
796
|
+
return 0;
|
|
797
|
+
}
|
|
798
|
+
if (mode === "bet") {
|
|
799
|
+
const slug = positional[1];
|
|
800
|
+
if (!slug) throw new CliError("Usage: ./dev test bet <slug>");
|
|
801
|
+
const betDir = path4.join(TESTS_DIR, "bets", slug);
|
|
802
|
+
if (!fs5.existsSync(betDir)) throw new CliError(`Bet suite not found: tests/bets/${slug}`);
|
|
803
|
+
if (integrationFlag) {
|
|
804
|
+
r.logo(`Running Bet Integration Tests \u2014 ${slug}`);
|
|
805
|
+
await start(ctx);
|
|
806
|
+
await migrate(ctx);
|
|
807
|
+
const usesPlaywright = fs5.readdirSync(betDir).filter((f) => f.endsWith(".py")).some((f) => /playwright/.test(fs5.readFileSync(path4.join(betDir, f), "utf8")));
|
|
808
|
+
if (usesPlaywright) {
|
|
809
|
+
r.step("Installing Playwright browser (chromium)");
|
|
810
|
+
run("uv", ["run", "playwright", "install", "chromium"], { cwd: TESTS_DIR });
|
|
811
|
+
}
|
|
812
|
+
r.step(`Running bet-progress suite: bets/${slug}/ (REQUIRE_SERVICES enabled)`);
|
|
813
|
+
const code3 = run("uv", ["run", "pytest", `bets/${slug}/`], {
|
|
814
|
+
cwd: TESTS_DIR,
|
|
815
|
+
env: { ...process.env, GROUNDWORK_REQUIRE_SERVICES: "1" }
|
|
816
|
+
});
|
|
817
|
+
if (!keep) await stop(ctx);
|
|
818
|
+
if (code3 !== 0) throw new CliError(`Bet integration tests failed: ${slug}`);
|
|
819
|
+
r.success(`Bet integration tests passed: ${slug}`);
|
|
820
|
+
return 0;
|
|
821
|
+
}
|
|
822
|
+
r.logo(`Running Bet Tests \u2014 ${slug}`);
|
|
823
|
+
r.step(`Running bet-progress suite: bets/${slug}/`);
|
|
824
|
+
const code2 = run("uv", ["run", "pytest", `bets/${slug}/`], { cwd: TESTS_DIR });
|
|
825
|
+
if (code2 === 0) r.success(`Bet tests passed: ${slug}`);
|
|
826
|
+
return code2;
|
|
827
|
+
}
|
|
828
|
+
r.logo("Running Tests");
|
|
829
|
+
r.step("Running System Tests (pytest)");
|
|
830
|
+
const code = run("uv", ["run", "pytest", "system/"], { cwd: TESTS_DIR });
|
|
831
|
+
if (code === 0) r.success("Tests passed.");
|
|
832
|
+
return code;
|
|
833
|
+
}
|
|
834
|
+
async function lint(ctx) {
|
|
835
|
+
const { r } = ctx;
|
|
836
|
+
r.logo("Running Linters");
|
|
837
|
+
let last = 0;
|
|
838
|
+
for (const svc of getAppServices()) {
|
|
839
|
+
const dir = serviceDir(svc);
|
|
840
|
+
const type = detectType(svc);
|
|
841
|
+
if (type === "go" || fs5.existsSync(path4.join(dir, ".golangci.yml"))) {
|
|
842
|
+
r.step(`Linting Go service: ${svc}`);
|
|
843
|
+
if (commandExists("golangci-lint")) {
|
|
844
|
+
const code = run("golangci-lint", ["run"], { cwd: dir });
|
|
845
|
+
if (code !== 0) last = code;
|
|
846
|
+
} else {
|
|
847
|
+
r.warn("golangci-lint not installed \u2014 skipping. Install: https://golangci-lint.run");
|
|
848
|
+
}
|
|
849
|
+
} else if (type === "node") {
|
|
850
|
+
r.step(`Linting Next.js service: ${svc}`);
|
|
851
|
+
if (commandExists("npm")) {
|
|
852
|
+
const code = run("npm", ["run", "lint"], { cwd: dir });
|
|
853
|
+
if (code !== 0) last = code;
|
|
854
|
+
} else {
|
|
855
|
+
r.warn("npm not installed \u2014 skipping. Install Node.js (includes npm).");
|
|
856
|
+
}
|
|
857
|
+
} else if (type === "python") {
|
|
858
|
+
r.step(`Linting Python service: ${svc}`);
|
|
859
|
+
if (commandExists("uv")) {
|
|
860
|
+
const ruff = run("uv", ["run", "ruff", "check", "."], { cwd: dir });
|
|
861
|
+
const black = run("uv", ["run", "black", "--check", "."], { cwd: dir });
|
|
862
|
+
if (ruff !== 0) last = ruff;
|
|
863
|
+
if (black !== 0) last = black;
|
|
864
|
+
} else {
|
|
865
|
+
r.warn("uv not installed \u2014 skipping ruff/black. Install: https://docs.astral.sh/uv");
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
r.success("Linting complete.");
|
|
870
|
+
return last;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// src/generators/workspace-dev-cli/cli-src/src/commands/doctor.ts
|
|
874
|
+
var fs7 = __toESM(require("fs"));
|
|
875
|
+
var path6 = __toESM(require("path"));
|
|
876
|
+
|
|
877
|
+
// src/generators/workspace-dev-cli/cli-src/src/util/version.ts
|
|
878
|
+
var fs6 = __toESM(require("fs"));
|
|
879
|
+
var path5 = __toESM(require("path"));
|
|
880
|
+
var DEV_CLI_VERSION = "0.10.0";
|
|
881
|
+
function stampedFrameworkVersion() {
|
|
882
|
+
try {
|
|
883
|
+
const state = JSON.parse(
|
|
884
|
+
fs6.readFileSync(path5.join(ROOT, ".groundwork", "config", "state.json"), "utf8")
|
|
885
|
+
);
|
|
886
|
+
return state.groundwork && state.groundwork.version || null;
|
|
887
|
+
} catch {
|
|
888
|
+
return null;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// src/generators/workspace-dev-cli/cli-src/src/commands/doctor.ts
|
|
893
|
+
async function doctor(ctx) {
|
|
894
|
+
const { r } = ctx;
|
|
895
|
+
const json = ctx.json || ctx.args.includes("--json");
|
|
896
|
+
const checks = [];
|
|
897
|
+
checks.push({ name: "docker", ok: commandExists("docker"), hint: "Install Docker Desktop or the docker engine." });
|
|
898
|
+
const compose = capture("docker", ["compose", "version"]);
|
|
899
|
+
checks.push({ name: "docker compose", ok: compose.status === 0, hint: "Install the Docker Compose v2 plugin." });
|
|
900
|
+
let needsNode = false;
|
|
901
|
+
let needsGo = false;
|
|
902
|
+
let needsAir = false;
|
|
903
|
+
let needsPython = false;
|
|
904
|
+
for (const svc of getAppServices()) {
|
|
905
|
+
const dir = serviceDir(svc);
|
|
906
|
+
if (fs7.existsSync(path6.join(dir, "package.json"))) needsNode = true;
|
|
907
|
+
if (fs7.existsSync(path6.join(dir, "go.mod"))) needsGo = true;
|
|
908
|
+
if (fs7.existsSync(path6.join(dir, ".air.toml"))) needsAir = true;
|
|
909
|
+
if (fs7.existsSync(path6.join(dir, "pyproject.toml"))) needsPython = true;
|
|
910
|
+
}
|
|
911
|
+
if (needsNode) checks.push({ name: "npm", ok: commandExists("npm"), hint: "Install Node.js (includes npm)." });
|
|
912
|
+
if (needsGo) checks.push({ name: "go", ok: commandExists("go"), hint: "Install the Go toolchain." });
|
|
913
|
+
if (needsAir) checks.push({ name: "air", ok: commandExists("air"), hint: "go install github.com/air-verse/air@latest" });
|
|
914
|
+
if (needsPython)
|
|
915
|
+
checks.push({
|
|
916
|
+
name: "python",
|
|
917
|
+
ok: commandExists("python3") || commandExists("python"),
|
|
918
|
+
hint: "Install Python 3."
|
|
919
|
+
});
|
|
920
|
+
const stamp = stampedFrameworkVersion();
|
|
921
|
+
if (stamp && DEV_CLI_VERSION !== "unknown") {
|
|
922
|
+
checks.push({
|
|
923
|
+
name: "dev bundle version",
|
|
924
|
+
ok: stamp === DEV_CLI_VERSION,
|
|
925
|
+
hint: `bundle ${DEV_CLI_VERSION} trails framework ${stamp} \u2014 run npx groundwork-method update, then groundwork check for the full staleness report.`
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
const cfg = dockerComposeCapture(["config", "--services"]);
|
|
929
|
+
const composeServices = new Set(
|
|
930
|
+
cfg.status === 0 ? cfg.stdout.split("\n").map((s) => s.trim()).filter(Boolean) : []
|
|
931
|
+
);
|
|
932
|
+
const stackUp = dockerComposeCapture(["ps", "-q"]).stdout.trim().length > 0;
|
|
933
|
+
const hasServices = getAppServices().length > 0;
|
|
934
|
+
const connectivity = [];
|
|
935
|
+
if (hasServices && !stackUp) {
|
|
936
|
+
connectivity.push({ name: "stack", ok: false, hint: "down \u2014 run './dev start' (or './dev start --docker')." });
|
|
937
|
+
}
|
|
938
|
+
if (composeServices.has("db")) {
|
|
939
|
+
const ok = stackUp && dockerComposeCapture(["exec", "-T", "db", "pg_isready", "-U", "postgres"]).status === 0;
|
|
940
|
+
connectivity.push({ name: "postgres (:5432)", ok, hint: ok ? void 0 : "db not accepting connections." });
|
|
941
|
+
}
|
|
942
|
+
if (composeServices.has("redis")) {
|
|
943
|
+
const ping = stackUp ? dockerComposeCapture(["exec", "-T", "redis", "redis-cli", "ping"]) : null;
|
|
944
|
+
const ok = ping !== null && /PONG/i.test(ping.stdout);
|
|
945
|
+
connectivity.push({ name: "redis (:6379)", ok, hint: ok ? void 0 : "no PONG from redis." });
|
|
946
|
+
}
|
|
947
|
+
if (composeServices.has("jaeger")) {
|
|
948
|
+
const code = await httpProbe("http://localhost:16686/api/services");
|
|
949
|
+
const ok = code === 200;
|
|
950
|
+
connectivity.push({ name: "jaeger (:16686)", ok, hint: ok ? void 0 : `query API returned ${code}.` });
|
|
951
|
+
}
|
|
952
|
+
const tooling = [];
|
|
953
|
+
if (needsPython) {
|
|
954
|
+
const ok = commandExists("go");
|
|
955
|
+
tooling.push({
|
|
956
|
+
name: "go (pg-schema-diff)",
|
|
957
|
+
ok,
|
|
958
|
+
hint: ok ? void 0 : "Python './dev migrate' needs the Go toolchain."
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
const depMissing = checks.filter((c) => !c.ok);
|
|
962
|
+
const runtimeMissing = [...connectivity, ...tooling].filter((c) => !c.ok);
|
|
963
|
+
const exitCode = runtimeMissing.length > 0 ? 1 : 0;
|
|
964
|
+
if (json) {
|
|
965
|
+
const allChecks = [...checks, ...connectivity, ...tooling];
|
|
966
|
+
process.stdout.write(
|
|
967
|
+
JSON.stringify({ ok: allChecks.every((c) => c.ok), checks: allChecks }, null, 2) + "\n"
|
|
968
|
+
);
|
|
969
|
+
return exitCode;
|
|
970
|
+
}
|
|
971
|
+
r.logo("Environment Verification (Doctor)");
|
|
972
|
+
r.table(
|
|
973
|
+
"Dependencies",
|
|
974
|
+
checks.map((c) => [c.name, c.ok ? "ok" : "MISSING", c.ok ? "" : c.hint ?? ""])
|
|
975
|
+
);
|
|
976
|
+
r.table(
|
|
977
|
+
"Runtime Connectivity",
|
|
978
|
+
connectivity.map((c) => [c.name, c.ok ? "ok" : "FAIL", c.ok ? "" : c.hint ?? ""])
|
|
979
|
+
);
|
|
980
|
+
if (tooling.length > 0) {
|
|
981
|
+
r.table(
|
|
982
|
+
"Migration Tooling",
|
|
983
|
+
tooling.map((c) => [c.name, c.ok ? "ok" : "MISSING", c.ok ? "" : c.hint ?? ""])
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
const allMissing = [...depMissing, ...runtimeMissing];
|
|
987
|
+
if (allMissing.length === 0) {
|
|
988
|
+
r.success("Your environment is ready!");
|
|
989
|
+
} else {
|
|
990
|
+
r.errorCard(
|
|
991
|
+
`Found ${allMissing.length} issue(s).`,
|
|
992
|
+
allMissing.map((c) => `${c.name}: ${c.hint ?? ""}`).join(" ")
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
return exitCode;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// src/generators/workspace-dev-cli/cli-src/src/commands/bet.ts
|
|
999
|
+
var fs8 = __toESM(require("fs"));
|
|
1000
|
+
var path7 = __toESM(require("path"));
|
|
1001
|
+
|
|
1002
|
+
// src/generators/workspace-dev-cli/cli-src/src/util/prompt.ts
|
|
1003
|
+
var readline = __toESM(require("readline"));
|
|
1004
|
+
function isInteractive() {
|
|
1005
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
1006
|
+
}
|
|
1007
|
+
function selectPrompt(painter, message, choices) {
|
|
1008
|
+
return new Promise((resolve, reject) => {
|
|
1009
|
+
let index = 0;
|
|
1010
|
+
const out = process.stderr;
|
|
1011
|
+
readline.emitKeypressEvents(process.stdin);
|
|
1012
|
+
process.stdin.setRawMode(true);
|
|
1013
|
+
process.stdin.resume();
|
|
1014
|
+
out.write("\x1B[?25l");
|
|
1015
|
+
const render = (first) => {
|
|
1016
|
+
if (!first) out.write(`\x1B[${choices.length + 1}A`);
|
|
1017
|
+
out.write(`\x1B[2K ${painter.bold(message)}
|
|
1018
|
+
`);
|
|
1019
|
+
choices.forEach((c, i) => {
|
|
1020
|
+
const active = i === index;
|
|
1021
|
+
const marker = active ? painter.primary("\u276F") : " ";
|
|
1022
|
+
const label = active ? painter.primary(c.label) : c.label;
|
|
1023
|
+
const hint = c.hint ? painter.dim(` ${c.hint}`) : "";
|
|
1024
|
+
out.write(`\x1B[2K ${marker} ${label}${hint}
|
|
1025
|
+
`);
|
|
1026
|
+
});
|
|
1027
|
+
};
|
|
1028
|
+
const cleanup = () => {
|
|
1029
|
+
process.stdin.setRawMode(false);
|
|
1030
|
+
process.stdin.pause();
|
|
1031
|
+
process.stdin.removeListener("keypress", onKey);
|
|
1032
|
+
out.write("\x1B[?25h");
|
|
1033
|
+
};
|
|
1034
|
+
const onKey = (_str, key) => {
|
|
1035
|
+
if (key.ctrl && key.name === "c") {
|
|
1036
|
+
cleanup();
|
|
1037
|
+
process.exit(130);
|
|
1038
|
+
} else if (key.name === "up" || key.name === "k") {
|
|
1039
|
+
index = (index - 1 + choices.length) % choices.length;
|
|
1040
|
+
render(false);
|
|
1041
|
+
} else if (key.name === "down" || key.name === "j") {
|
|
1042
|
+
index = (index + 1) % choices.length;
|
|
1043
|
+
render(false);
|
|
1044
|
+
} else if (key.name === "return" || key.name === "enter") {
|
|
1045
|
+
cleanup();
|
|
1046
|
+
resolve(choices[index].value);
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
render(true);
|
|
1050
|
+
process.stdin.on("keypress", onKey);
|
|
1051
|
+
process.stdin.on("error", (e) => {
|
|
1052
|
+
cleanup();
|
|
1053
|
+
reject(e);
|
|
1054
|
+
});
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
function textPrompt(painter, message, validate) {
|
|
1058
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
1059
|
+
const ask = () => new Promise((resolve) => {
|
|
1060
|
+
rl.question(` ${painter.bold(message)} `, (answer) => resolve(answer.trim()));
|
|
1061
|
+
});
|
|
1062
|
+
return (async () => {
|
|
1063
|
+
try {
|
|
1064
|
+
for (; ; ) {
|
|
1065
|
+
const value = await ask();
|
|
1066
|
+
const err = validate ? validate(value) : null;
|
|
1067
|
+
if (!err) {
|
|
1068
|
+
rl.close();
|
|
1069
|
+
return value;
|
|
1070
|
+
}
|
|
1071
|
+
process.stderr.write(` ${painter.paint("error", "\u2716")} ${err}
|
|
1072
|
+
`);
|
|
1073
|
+
}
|
|
1074
|
+
} catch (e) {
|
|
1075
|
+
rl.close();
|
|
1076
|
+
throw e;
|
|
1077
|
+
}
|
|
1078
|
+
})();
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// src/generators/workspace-dev-cli/cli-src/src/commands/bet.ts
|
|
1082
|
+
var SLUG_RE = /^([a-z][a-z0-9-]*[a-z0-9]|[a-z0-9])$/;
|
|
1083
|
+
var SLUG_HINT = "Use lowercase kebab-case: letters, digits, and single hyphens, no leading/trailing hyphen.";
|
|
1084
|
+
function validateSlug(slug, label) {
|
|
1085
|
+
if (!SLUG_RE.test(slug)) {
|
|
1086
|
+
throw new CliError(`Invalid ${label}: "${slug}"`, SLUG_HINT);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
var slugValidator = (v) => SLUG_RE.test(v) ? null : SLUG_HINT;
|
|
1090
|
+
function existingBetSlugs() {
|
|
1091
|
+
const dir = path7.join(TESTS_DIR, "bets");
|
|
1092
|
+
if (!fs8.existsSync(dir)) return [];
|
|
1093
|
+
return fs8.readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory() && d.name !== "_archive").map((d) => d.name).sort();
|
|
1094
|
+
}
|
|
1095
|
+
async function resolveBetSlug(ctx, given) {
|
|
1096
|
+
if (given || !isInteractive()) return given;
|
|
1097
|
+
const existing = existingBetSlugs();
|
|
1098
|
+
if (existing.length > 0) {
|
|
1099
|
+
const choices = [
|
|
1100
|
+
...existing.map((s) => ({ label: s, value: s })),
|
|
1101
|
+
{ label: "+ new bet\u2026", value: "\0new" }
|
|
1102
|
+
];
|
|
1103
|
+
const picked = await selectPrompt(ctx.r.painter, "Which bet?", choices);
|
|
1104
|
+
if (picked !== "\0new") return picked;
|
|
1105
|
+
}
|
|
1106
|
+
return textPrompt(ctx.r.painter, "Bet slug:", slugValidator);
|
|
1107
|
+
}
|
|
1108
|
+
function templatePath(name) {
|
|
1109
|
+
return path7.join(ROOT, "scripts", "cli", "templates", name);
|
|
1110
|
+
}
|
|
1111
|
+
function nextIndex(betDir, prefix) {
|
|
1112
|
+
if (!fs8.existsSync(betDir)) return 1;
|
|
1113
|
+
const count = fs8.readdirSync(betDir).filter((f) => f.startsWith(prefix) && f.endsWith(".py")).length;
|
|
1114
|
+
return count + 1;
|
|
1115
|
+
}
|
|
1116
|
+
function substitute(template, tokens) {
|
|
1117
|
+
let out = template;
|
|
1118
|
+
for (const [k, v] of Object.entries(tokens)) {
|
|
1119
|
+
out = out.split(`@@${k}@@`).join(v);
|
|
1120
|
+
}
|
|
1121
|
+
return out;
|
|
1122
|
+
}
|
|
1123
|
+
async function newCmd(ctx) {
|
|
1124
|
+
let noun = ctx.args[0];
|
|
1125
|
+
if (!noun && isInteractive()) {
|
|
1126
|
+
noun = await selectPrompt(ctx.r.painter, "What do you want to scaffold?", [
|
|
1127
|
+
{ label: "bet", value: "bet", hint: "docs + tests directories for a new bet" },
|
|
1128
|
+
{ label: "milestone", value: "milestone", hint: "a red milestone test stub" },
|
|
1129
|
+
{ label: "slice", value: "slice", hint: "a red slice test stub" }
|
|
1130
|
+
]);
|
|
1131
|
+
}
|
|
1132
|
+
switch (noun) {
|
|
1133
|
+
case "bet":
|
|
1134
|
+
return newBet(ctx);
|
|
1135
|
+
case "milestone":
|
|
1136
|
+
return newMilestone(ctx);
|
|
1137
|
+
case "slice":
|
|
1138
|
+
return newSlice(ctx);
|
|
1139
|
+
default:
|
|
1140
|
+
throw new CliError("Usage: ./dev new bet|milestone|slice ...");
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
async function newBet(ctx) {
|
|
1144
|
+
const { r } = ctx;
|
|
1145
|
+
let slug = ctx.args[1];
|
|
1146
|
+
if (!slug && isInteractive()) slug = await textPrompt(r.painter, "Bet slug:", slugValidator);
|
|
1147
|
+
if (!slug) throw new CliError("Usage: ./dev new bet <slug>");
|
|
1148
|
+
validateSlug(slug, "bet slug");
|
|
1149
|
+
r.logo("New Bet");
|
|
1150
|
+
r.step(`Scaffolding bet: ${slug}`);
|
|
1151
|
+
fs8.mkdirSync(path7.join(DOCS_DIR, "bets", slug), { recursive: true });
|
|
1152
|
+
fs8.mkdirSync(path7.join(TESTS_DIR, "bets", slug), { recursive: true });
|
|
1153
|
+
r.success(`Created docs/bets/${slug}/ and tests/bets/${slug}/`);
|
|
1154
|
+
r.info(`Next: ./dev new milestone ${slug} <milestone-slug>`);
|
|
1155
|
+
return 0;
|
|
1156
|
+
}
|
|
1157
|
+
async function newMilestone(ctx) {
|
|
1158
|
+
const { r } = ctx;
|
|
1159
|
+
const betSlug = await resolveBetSlug(ctx, ctx.args[1]);
|
|
1160
|
+
let milestoneSlug = ctx.args[2];
|
|
1161
|
+
if (!milestoneSlug && isInteractive()) milestoneSlug = await textPrompt(r.painter, "Milestone slug:", slugValidator);
|
|
1162
|
+
if (!betSlug || !milestoneSlug) throw new CliError("Usage: ./dev new milestone <bet-slug> <milestone-slug>");
|
|
1163
|
+
validateSlug(betSlug, "bet slug");
|
|
1164
|
+
validateSlug(milestoneSlug, "milestone slug");
|
|
1165
|
+
const betDir = path7.join(TESTS_DIR, "bets", betSlug);
|
|
1166
|
+
if (!fs8.existsSync(betDir)) throw new CliError(`Bet not found: tests/bets/${betSlug}`, `Run: ./dev new bet ${betSlug}`);
|
|
1167
|
+
const n = nextIndex(betDir, "test_milestone_");
|
|
1168
|
+
const template = fs8.readFileSync(templatePath("milestone-test.pytmpl"), "utf8");
|
|
1169
|
+
const content = substitute(template, { BET: betSlug, MILESTONE: milestoneSlug, N: String(n) });
|
|
1170
|
+
const file = path7.join(betDir, `test_milestone_${n}_${milestoneSlug}.py`);
|
|
1171
|
+
fs8.writeFileSync(file, content);
|
|
1172
|
+
r.logo("New Milestone");
|
|
1173
|
+
r.success(`Created tests/bets/${betSlug}/test_milestone_${n}_${milestoneSlug}.py (RED)`);
|
|
1174
|
+
r.info("Fill in the target-state assertions before starting Delivery.");
|
|
1175
|
+
return 0;
|
|
1176
|
+
}
|
|
1177
|
+
async function newSlice(ctx) {
|
|
1178
|
+
const { r } = ctx;
|
|
1179
|
+
const betSlug = await resolveBetSlug(ctx, ctx.args[1]);
|
|
1180
|
+
let milestoneSlug = ctx.args[2];
|
|
1181
|
+
if (!milestoneSlug && isInteractive()) milestoneSlug = await textPrompt(r.painter, "Milestone slug:", slugValidator);
|
|
1182
|
+
let service = ctx.args[3];
|
|
1183
|
+
if (!service && isInteractive()) {
|
|
1184
|
+
const svcs = getAppServices();
|
|
1185
|
+
service = svcs.length ? await selectPrompt(r.painter, "Which service?", svcs.map((s) => ({ label: s, value: s }))) : await textPrompt(r.painter, "Service name:");
|
|
1186
|
+
}
|
|
1187
|
+
let sliceSlug = ctx.args[4];
|
|
1188
|
+
if (!sliceSlug && isInteractive()) sliceSlug = await textPrompt(r.painter, "Slice slug:", slugValidator);
|
|
1189
|
+
if (!betSlug || !milestoneSlug || !service || !sliceSlug)
|
|
1190
|
+
throw new CliError("Usage: ./dev new slice <bet-slug> <milestone-slug> <service> <slice-slug>");
|
|
1191
|
+
validateSlug(betSlug, "bet slug");
|
|
1192
|
+
validateSlug(milestoneSlug, "milestone slug");
|
|
1193
|
+
validateSlug(sliceSlug, "slice slug");
|
|
1194
|
+
const betDir = path7.join(TESTS_DIR, "bets", betSlug);
|
|
1195
|
+
if (!fs8.existsSync(betDir)) throw new CliError(`Bet not found: tests/bets/${betSlug}`, `Run: ./dev new bet ${betSlug}`);
|
|
1196
|
+
const n = nextIndex(betDir, "test_slice_");
|
|
1197
|
+
const template = fs8.readFileSync(templatePath("slice-test.pytmpl"), "utf8");
|
|
1198
|
+
const content = substitute(template, {
|
|
1199
|
+
BET: betSlug,
|
|
1200
|
+
MILESTONE: milestoneSlug,
|
|
1201
|
+
SERVICE: service,
|
|
1202
|
+
SLUG: sliceSlug,
|
|
1203
|
+
N: String(n)
|
|
1204
|
+
});
|
|
1205
|
+
const file = path7.join(betDir, `test_slice_${n}_${service}_${sliceSlug}.py`);
|
|
1206
|
+
fs8.writeFileSync(file, content);
|
|
1207
|
+
r.logo("New Slice");
|
|
1208
|
+
r.success(`Created tests/bets/${betSlug}/test_slice_${n}_${service}_${sliceSlug}.py (RED)`);
|
|
1209
|
+
r.info("Fill in the falsifiable capability assertions before starting Delivery.");
|
|
1210
|
+
return 0;
|
|
1211
|
+
}
|
|
1212
|
+
function archiveMove(rel, src, dest, inGit) {
|
|
1213
|
+
fs8.mkdirSync(path7.dirname(dest), { recursive: true });
|
|
1214
|
+
const destRel = path7.relative(ROOT, dest);
|
|
1215
|
+
if (inGit) {
|
|
1216
|
+
const moved = capture("git", ["-C", ROOT, "mv", rel, destRel]);
|
|
1217
|
+
if (moved.status !== 0) throw new CliError(`git mv failed: ${rel} \u2192 ${destRel}`, moved.stderr.trim());
|
|
1218
|
+
} else {
|
|
1219
|
+
fs8.renameSync(src, dest);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
function ensureArchiveMeta(archiveDir) {
|
|
1223
|
+
const metaPath = path7.join(archiveDir, "meta.json");
|
|
1224
|
+
if (fs8.existsSync(metaPath)) return;
|
|
1225
|
+
fs8.mkdirSync(archiveDir, { recursive: true });
|
|
1226
|
+
fs8.writeFileSync(metaPath, JSON.stringify({ defaultOpen: false, pages: ["..."] }, null, 2) + "\n");
|
|
1227
|
+
}
|
|
1228
|
+
async function archive(ctx) {
|
|
1229
|
+
const { r } = ctx;
|
|
1230
|
+
if (ctx.args[0] !== "bet") throw new CliError("Usage: ./dev archive bet <slug>");
|
|
1231
|
+
const slug = ctx.args[1];
|
|
1232
|
+
if (!slug) throw new CliError("Usage: ./dev archive bet <slug>");
|
|
1233
|
+
validateSlug(slug, "bet slug");
|
|
1234
|
+
const testsSrc = path7.join(TESTS_DIR, "bets", slug);
|
|
1235
|
+
const testsDest = path7.join(TESTS_DIR, "bets", "_archive", slug);
|
|
1236
|
+
const docsSrc = path7.join(DOCS_DIR, "bets", slug);
|
|
1237
|
+
const docsDest = path7.join(DOCS_DIR, "bets", "_archive", slug);
|
|
1238
|
+
if (!fs8.existsSync(testsSrc) && !fs8.existsSync(docsSrc)) {
|
|
1239
|
+
throw new CliError(`Bet not found: ${slug}`, `Expected tests/bets/${slug}/ or docs/bets/${slug}/.`);
|
|
1240
|
+
}
|
|
1241
|
+
if (fs8.existsSync(testsDest)) throw new CliError(`Archive already exists: tests/bets/_archive/${slug}`);
|
|
1242
|
+
if (fs8.existsSync(docsDest)) throw new CliError(`Archive already exists: docs/bets/_archive/${slug}`);
|
|
1243
|
+
r.logo("Archive Bet");
|
|
1244
|
+
const inGit = capture("git", ["-C", ROOT, "rev-parse", "--is-inside-work-tree"]).status === 0;
|
|
1245
|
+
if (fs8.existsSync(testsSrc)) {
|
|
1246
|
+
r.step(`Archiving tests/bets/${slug} \u2192 tests/bets/_archive/${slug}`);
|
|
1247
|
+
archiveMove(`tests/bets/${slug}`, testsSrc, testsDest, inGit);
|
|
1248
|
+
r.success(`Archived suite to tests/bets/_archive/${slug}`);
|
|
1249
|
+
} else {
|
|
1250
|
+
r.warn(`No suite at tests/bets/${slug} \u2014 skipping.`);
|
|
1251
|
+
}
|
|
1252
|
+
if (fs8.existsSync(docsSrc)) {
|
|
1253
|
+
r.step(`Archiving docs/bets/${slug} \u2192 docs/bets/_archive/${slug}`);
|
|
1254
|
+
archiveMove(`docs/bets/${slug}`, docsSrc, docsDest, inGit);
|
|
1255
|
+
ensureArchiveMeta(path7.join(DOCS_DIR, "bets", "_archive"));
|
|
1256
|
+
r.success(`Archived docs to docs/bets/_archive/${slug}`);
|
|
1257
|
+
} else {
|
|
1258
|
+
r.warn(`No docs at docs/bets/${slug} \u2014 skipping.`);
|
|
1259
|
+
}
|
|
1260
|
+
r.info("Permanent best-practice tests remain in place and cover the feature going forward.");
|
|
1261
|
+
return 0;
|
|
1262
|
+
}
|
|
1263
|
+
var MILESTONE_RE = /^test_milestone_(\d+)_(.+)\.[^.]+$/;
|
|
1264
|
+
var SLICE_RE = /^test_slice_(\d+)_(.+)_([a-z0-9][a-z0-9-]*)\.[^.]+$/;
|
|
1265
|
+
function suiteTestFiles(betDir) {
|
|
1266
|
+
if (!fs8.existsSync(betDir)) return [];
|
|
1267
|
+
return fs8.readdirSync(betDir, { withFileTypes: true }).filter((d) => d.isFile() && (MILESTONE_RE.test(d.name) || SLICE_RE.test(d.name))).map((d) => d.name).sort();
|
|
1268
|
+
}
|
|
1269
|
+
function runSuiteVerdicts(slug) {
|
|
1270
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
1271
|
+
if (!commandExists("uv")) return { ran: false, byFile };
|
|
1272
|
+
const res = capture("uv", ["run", "pytest", `bets/${slug}/`, "-v", "--tb=no", "--color=no", "-p", "no:cacheprovider"], {
|
|
1273
|
+
cwd: TESTS_DIR
|
|
1274
|
+
});
|
|
1275
|
+
const tally = /* @__PURE__ */ new Map();
|
|
1276
|
+
const line = /(?:^|\/)(test_(?:milestone|slice)_\d+_[^/:]+\.[A-Za-z0-9]+)::\S+\s+(PASSED|FAILED|ERROR|XFAIL|XPASS|SKIPPED)/g;
|
|
1277
|
+
for (const m of res.stdout.matchAll(line)) {
|
|
1278
|
+
const file = m[1];
|
|
1279
|
+
const outcome = m[2];
|
|
1280
|
+
const t = tally.get(file) ?? { passed: 0, failed: 0 };
|
|
1281
|
+
if (outcome === "PASSED" || outcome === "XFAIL") t.passed += 1;
|
|
1282
|
+
else if (outcome === "FAILED" || outcome === "ERROR" || outcome === "XPASS") t.failed += 1;
|
|
1283
|
+
tally.set(file, t);
|
|
1284
|
+
}
|
|
1285
|
+
for (const [file, t] of tally) {
|
|
1286
|
+
byFile.set(file, t.failed > 0 ? "red" : t.passed > 0 ? "green" : "red");
|
|
1287
|
+
}
|
|
1288
|
+
return { ran: true, byFile };
|
|
1289
|
+
}
|
|
1290
|
+
function deriveBoard(slug) {
|
|
1291
|
+
const betDir = path7.join(TESTS_DIR, "bets", slug);
|
|
1292
|
+
const files = suiteTestFiles(betDir);
|
|
1293
|
+
const { ran, byFile } = files.length > 0 ? runSuiteVerdicts(slug) : { ran: false, byFile: /* @__PURE__ */ new Map() };
|
|
1294
|
+
const rows = files.map((file) => {
|
|
1295
|
+
const verdict = byFile.get(file) ?? (ran ? "red" : "unknown");
|
|
1296
|
+
const mm = MILESTONE_RE.exec(file);
|
|
1297
|
+
if (mm) {
|
|
1298
|
+
return { kind: "milestone", file, n: Number(mm[1]), service: null, slug: mm[2], state: verdict };
|
|
1299
|
+
}
|
|
1300
|
+
const sm = SLICE_RE.exec(file);
|
|
1301
|
+
return { kind: "slice", file, n: Number(sm[1]), service: sm[2], slug: sm[3], state: verdict };
|
|
1302
|
+
});
|
|
1303
|
+
const order = { milestone: 0, slice: 1 };
|
|
1304
|
+
rows.sort((a, b) => order[a.kind] - order[b.kind] || a.n - b.n);
|
|
1305
|
+
return { ran, rows };
|
|
1306
|
+
}
|
|
1307
|
+
async function betCmd(ctx) {
|
|
1308
|
+
const noun = ctx.args.filter((a) => !a.startsWith("-"))[0];
|
|
1309
|
+
switch (noun ?? "status") {
|
|
1310
|
+
case "status":
|
|
1311
|
+
return status2(ctx);
|
|
1312
|
+
default:
|
|
1313
|
+
throw new CliError("Usage: ./dev bet status [<slug>] [--json]");
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
function boardGlyph(p, state) {
|
|
1317
|
+
const u = p.caps.unicode;
|
|
1318
|
+
if (state === "green") return p.paint("success", u ? "\u25CF" : "*");
|
|
1319
|
+
if (state === "red") return p.paint("error", u ? "\u2717" : "x");
|
|
1320
|
+
return p.dim(u ? "\u25CB" : "o");
|
|
1321
|
+
}
|
|
1322
|
+
function stateLabel(state) {
|
|
1323
|
+
return state === "green" ? "passing" : state === "red" ? "failing" : "not run";
|
|
1324
|
+
}
|
|
1325
|
+
async function status2(ctx) {
|
|
1326
|
+
const { r } = ctx;
|
|
1327
|
+
const json = ctx.json || ctx.args.includes("--json");
|
|
1328
|
+
const positional = ctx.args.filter((a) => !a.startsWith("-"));
|
|
1329
|
+
const slug = positional[1];
|
|
1330
|
+
if (!slug) return statusAll(ctx, json);
|
|
1331
|
+
validateSlug(slug, "bet slug");
|
|
1332
|
+
const betDir = path7.join(TESTS_DIR, "bets", slug);
|
|
1333
|
+
const files = suiteTestFiles(betDir);
|
|
1334
|
+
if (files.length === 0) {
|
|
1335
|
+
if (json) {
|
|
1336
|
+
process.stdout.write(JSON.stringify({ bet: slug, materialized: false, milestones: [], slices: [] }, null, 2) + "\n");
|
|
1337
|
+
return 0;
|
|
1338
|
+
}
|
|
1339
|
+
const ro2 = r.asStream(process.stdout);
|
|
1340
|
+
ro2.logo(`Bet Board \u2014 ${slug}`);
|
|
1341
|
+
ro2.info("No board yet \u2014 the bet suite is materialized RED at Delivery start.");
|
|
1342
|
+
ro2.info(`Once Delivery begins, ./dev bet status ${slug} renders red/green from the suite.`);
|
|
1343
|
+
return 0;
|
|
1344
|
+
}
|
|
1345
|
+
const { ran, rows } = deriveBoard(slug);
|
|
1346
|
+
const milestones = rows.filter((x) => x.kind === "milestone");
|
|
1347
|
+
const slices = rows.filter((x) => x.kind === "slice");
|
|
1348
|
+
const green = rows.filter((x) => x.state === "green").length;
|
|
1349
|
+
if (json) {
|
|
1350
|
+
process.stdout.write(
|
|
1351
|
+
JSON.stringify(
|
|
1352
|
+
{
|
|
1353
|
+
bet: slug,
|
|
1354
|
+
materialized: true,
|
|
1355
|
+
ran,
|
|
1356
|
+
milestones: milestones.map((m) => ({ n: m.n, slug: m.slug, file: m.file, state: m.state })),
|
|
1357
|
+
slices: slices.map((s) => ({ n: s.n, service: s.service, slug: s.slug, file: s.file, state: s.state })),
|
|
1358
|
+
summary: { green, total: rows.length }
|
|
1359
|
+
},
|
|
1360
|
+
null,
|
|
1361
|
+
2
|
|
1362
|
+
) + "\n"
|
|
1363
|
+
);
|
|
1364
|
+
return 0;
|
|
1365
|
+
}
|
|
1366
|
+
const ro = r.asStream(process.stdout);
|
|
1367
|
+
ro.logo(`Bet Board \u2014 ${slug}`);
|
|
1368
|
+
if (!ran) {
|
|
1369
|
+
ro.warn("uv not found \u2014 cannot run the suite. Listing the materialized tests without a verdict.");
|
|
1370
|
+
}
|
|
1371
|
+
const mRows = milestones.map(
|
|
1372
|
+
(m) => [`${boardGlyph(ro.painter, m.state)} ${`M${m.n} ${m.slug}`.padEnd(26)}`, "", stateLabel(m.state)]
|
|
1373
|
+
);
|
|
1374
|
+
const sRows = slices.map(
|
|
1375
|
+
(s) => [`${boardGlyph(ro.painter, s.state)} ${`S${s.n} ${s.slug}`.padEnd(26)}`, s.service ?? "", stateLabel(s.state)]
|
|
1376
|
+
);
|
|
1377
|
+
ro.table("Milestones", mRows);
|
|
1378
|
+
ro.table("Slices", sRows);
|
|
1379
|
+
ro.success(`Board: ${green}/${rows.length} green (run ./dev test bet ${slug} for full output).`);
|
|
1380
|
+
return 0;
|
|
1381
|
+
}
|
|
1382
|
+
async function statusAll(ctx, json) {
|
|
1383
|
+
const { r } = ctx;
|
|
1384
|
+
const slugs = existingBetSlugs();
|
|
1385
|
+
const summaries = slugs.map((slug) => {
|
|
1386
|
+
const files = suiteTestFiles(path7.join(TESTS_DIR, "bets", slug));
|
|
1387
|
+
if (files.length === 0) return { bet: slug, materialized: false, green: 0, total: 0 };
|
|
1388
|
+
const { rows } = deriveBoard(slug);
|
|
1389
|
+
return { bet: slug, materialized: true, green: rows.filter((x) => x.state === "green").length, total: rows.length };
|
|
1390
|
+
});
|
|
1391
|
+
if (json) {
|
|
1392
|
+
process.stdout.write(JSON.stringify(summaries, null, 2) + "\n");
|
|
1393
|
+
return 0;
|
|
1394
|
+
}
|
|
1395
|
+
const ro = r.asStream(process.stdout);
|
|
1396
|
+
ro.logo("Bet Board");
|
|
1397
|
+
if (summaries.length === 0) {
|
|
1398
|
+
ro.info("No bets found under tests/bets/.");
|
|
1399
|
+
ro.info("Start one with ./dev new bet <slug>.");
|
|
1400
|
+
return 0;
|
|
1401
|
+
}
|
|
1402
|
+
for (const s of summaries) {
|
|
1403
|
+
const line = s.materialized ? `${s.green}/${s.total} green` : "no suite yet \u2014 materialized at Delivery start";
|
|
1404
|
+
ro.cmd(s.bet, line);
|
|
1405
|
+
}
|
|
1406
|
+
ro.info("Run ./dev bet status <slug> for the full board.");
|
|
1407
|
+
return 0;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
// src/generators/workspace-dev-cli/cli-src/src/commands/surface.ts
|
|
1411
|
+
var fs9 = __toESM(require("fs"));
|
|
1412
|
+
var path8 = __toESM(require("path"));
|
|
1413
|
+
function readSurfacesFile() {
|
|
1414
|
+
if (!fs9.existsSync(GROUNDWORK_SURFACES_FILE)) return null;
|
|
1415
|
+
try {
|
|
1416
|
+
return JSON.parse(fs9.readFileSync(GROUNDWORK_SURFACES_FILE, "utf8"));
|
|
1417
|
+
} catch {
|
|
1418
|
+
throw new CliError(
|
|
1419
|
+
`Could not parse ${path8.relative(ROOT, GROUNDWORK_SURFACES_FILE)}`,
|
|
1420
|
+
"Fix or remove the malformed JSON file \u2014 docs/surfaces.md is its human twin."
|
|
1421
|
+
);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
function isRetired(s) {
|
|
1425
|
+
return s.status === "retired";
|
|
1426
|
+
}
|
|
1427
|
+
function cellGlyph(p, state) {
|
|
1428
|
+
const u = p.caps.unicode;
|
|
1429
|
+
if (state === "delivered") return p.paint("success", u ? "\u25CF" : "*");
|
|
1430
|
+
if (state === "planned") return p.paint("warning", u ? "\u25D0" : "~");
|
|
1431
|
+
if (state === "omitted") return p.dim(u ? "\u25CB" : "o");
|
|
1432
|
+
if (state === "n/a") return p.dim(u ? "\xB7" : ".");
|
|
1433
|
+
return p.paint("error", "!");
|
|
1434
|
+
}
|
|
1435
|
+
function backlogOf(surfaces, capabilities) {
|
|
1436
|
+
const backlog = {};
|
|
1437
|
+
for (const s of surfaces) {
|
|
1438
|
+
if (!isRetired(s)) backlog[s.slug] = 0;
|
|
1439
|
+
}
|
|
1440
|
+
for (const cap of capabilities) {
|
|
1441
|
+
for (const slug of Object.keys(backlog)) {
|
|
1442
|
+
if (cap.cells?.[slug]?.state === "planned") backlog[slug] += 1;
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
return backlog;
|
|
1446
|
+
}
|
|
1447
|
+
function illegalCellsOf(surfaces, capabilities) {
|
|
1448
|
+
const states = /* @__PURE__ */ new Set(["delivered", "planned", "omitted", "n/a"]);
|
|
1449
|
+
const out = [];
|
|
1450
|
+
for (const cap of capabilities) {
|
|
1451
|
+
for (const s of surfaces) {
|
|
1452
|
+
const cell = cap.cells?.[s.slug];
|
|
1453
|
+
if (!cell || !cell.state || !states.has(cell.state)) {
|
|
1454
|
+
out.push(`${cap.key} \xD7 ${s.slug}`);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
return out;
|
|
1459
|
+
}
|
|
1460
|
+
function padPlain(text, width) {
|
|
1461
|
+
return text.padEnd(width);
|
|
1462
|
+
}
|
|
1463
|
+
async function surfaceCmd(ctx) {
|
|
1464
|
+
const noun = ctx.args.filter((a) => !a.startsWith("-"))[0] ?? "status";
|
|
1465
|
+
switch (noun) {
|
|
1466
|
+
case "status":
|
|
1467
|
+
return status3(ctx);
|
|
1468
|
+
default:
|
|
1469
|
+
throw new CliError("Usage: ./dev surface status [--json]");
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
async function status3(ctx) {
|
|
1473
|
+
const { r } = ctx;
|
|
1474
|
+
const json = ctx.json || ctx.args.includes("--json");
|
|
1475
|
+
const file = readSurfacesFile();
|
|
1476
|
+
const ro = r.asStream(process.stdout);
|
|
1477
|
+
if (!file) {
|
|
1478
|
+
if (json) {
|
|
1479
|
+
process.stdout.write(JSON.stringify({ present: false }, null, 2) + "\n");
|
|
1480
|
+
return 0;
|
|
1481
|
+
}
|
|
1482
|
+
ro.info("No surface registry (.groundwork/surfaces.json) \u2014 the groundwork-surface-activation skill bootstraps it.");
|
|
1483
|
+
return 0;
|
|
1484
|
+
}
|
|
1485
|
+
const surfaces = (file.surfaces ?? []).filter((s) => Boolean(s.slug));
|
|
1486
|
+
const capabilities = (file.capabilities ?? []).filter((c) => Boolean(c.key));
|
|
1487
|
+
const backlog = backlogOf(surfaces, capabilities);
|
|
1488
|
+
const illegalCells = illegalCellsOf(surfaces, capabilities);
|
|
1489
|
+
if (json) {
|
|
1490
|
+
process.stdout.write(
|
|
1491
|
+
JSON.stringify({ present: true, core: file.core ?? null, surfaces, capabilities, backlog, illegalCells }, null, 2) + "\n"
|
|
1492
|
+
);
|
|
1493
|
+
return 0;
|
|
1494
|
+
}
|
|
1495
|
+
const p = ro.painter;
|
|
1496
|
+
ro.logo("Surface Board");
|
|
1497
|
+
ro.table(
|
|
1498
|
+
"Surface Registry",
|
|
1499
|
+
surfaces.map((s) => {
|
|
1500
|
+
const slug = isRetired(s) ? p.dim(padPlain(`${s.slug} (retired)`, 28)) : padPlain(s.slug, 28);
|
|
1501
|
+
const shape = `${s.type ?? "?"} \xB7 ${s.platform ?? "?"}`;
|
|
1502
|
+
const detail = `${s.status ?? "?"} \xB7 ${s.scaffold ?? "?"} \xB7 ${s.testMedium ?? "\u2014"}`;
|
|
1503
|
+
return [slug, shape, detail];
|
|
1504
|
+
})
|
|
1505
|
+
);
|
|
1506
|
+
if (surfaces.length === 0) {
|
|
1507
|
+
ro.info("Registry is empty \u2014 a headless core is legal; its contracts stand alone.");
|
|
1508
|
+
}
|
|
1509
|
+
if (capabilities.length === 0) {
|
|
1510
|
+
ro.info("Capability ledger is empty \u2014 bet validation appends capability rows when bets close.");
|
|
1511
|
+
} else {
|
|
1512
|
+
const keyWidth = Math.max(...capabilities.map((c) => c.key.length), "capability".length) + 2;
|
|
1513
|
+
const colWidth = (s) => (isRetired(s) ? s.slug.length + " (retired)".length : s.slug.length) + 2;
|
|
1514
|
+
const header = p.dim(padPlain("capability", keyWidth)) + surfaces.map((s) => isRetired(s) ? p.dim(padPlain(`${s.slug} (retired)`, colWidth(s))) : padPlain(s.slug, colWidth(s))).join("");
|
|
1515
|
+
const rows = [[header, "", ""]];
|
|
1516
|
+
for (const cap of capabilities) {
|
|
1517
|
+
const line = padPlain(cap.key, keyWidth) + surfaces.map((s) => cellGlyph(p, cap.cells?.[s.slug]?.state) + " ".repeat(colWidth(s) - 1)).join("");
|
|
1518
|
+
rows.push([line, "", ""]);
|
|
1519
|
+
}
|
|
1520
|
+
ro.table("Capability Ledger", rows);
|
|
1521
|
+
const u = p.caps.unicode;
|
|
1522
|
+
ro.info(
|
|
1523
|
+
`${cellGlyph(p, "delivered")} delivered ${cellGlyph(p, "planned")} planned ${cellGlyph(p, "omitted")} omitted ${cellGlyph(p, "n/a")} n/a ${p.paint("error", "!")} empty ${u ? "\u2014" : "-"} illegal`
|
|
1524
|
+
);
|
|
1525
|
+
}
|
|
1526
|
+
for (const cell of illegalCells) {
|
|
1527
|
+
ro.error(`Empty ledger cell: ${cell} \u2014 illegal state; bet validation fills every column or the bet does not close.`);
|
|
1528
|
+
}
|
|
1529
|
+
const entries = Object.entries(backlog);
|
|
1530
|
+
if (entries.length > 0 && capabilities.length > 0) {
|
|
1531
|
+
ro.step("Sync backlog (planned cells)");
|
|
1532
|
+
for (const [slug, count] of entries) {
|
|
1533
|
+
ro.cmd(slug, count === 0 ? "in sync \u2014 no planned cells" : `${count} planned cell${count === 1 ? "" : "s"}`);
|
|
1534
|
+
}
|
|
1535
|
+
const total = entries.reduce((acc, [, n]) => acc + n, 0);
|
|
1536
|
+
if (total === 0 && illegalCells.length === 0) {
|
|
1537
|
+
ro.success("All surfaces in sync \u2014 every ledger decision is on record.");
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
return 0;
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
// src/generators/workspace-dev-cli/cli-src/src/commands/completion.ts
|
|
1544
|
+
async function completion(ctx) {
|
|
1545
|
+
const shell = ctx.args[0];
|
|
1546
|
+
const commands = ctx.commands;
|
|
1547
|
+
const verbs = commands.map((c) => c.name);
|
|
1548
|
+
const nounsByVerb = commands.filter((c) => c.nouns?.length).map((c) => ({ verb: c.name, nouns: c.nouns }));
|
|
1549
|
+
const flagsByVerb = commands.filter((c) => c.flags?.length).map((c) => ({
|
|
1550
|
+
verb: c.name,
|
|
1551
|
+
flags: c.flags.map((f) => f.name)
|
|
1552
|
+
}));
|
|
1553
|
+
if (shell === "bash") {
|
|
1554
|
+
process.stdout.write(bashScript(verbs, nounsByVerb, flagsByVerb) + "\n");
|
|
1555
|
+
return 0;
|
|
1556
|
+
}
|
|
1557
|
+
if (shell === "zsh") {
|
|
1558
|
+
process.stdout.write(zshScript(verbs, nounsByVerb, flagsByVerb) + "\n");
|
|
1559
|
+
return 0;
|
|
1560
|
+
}
|
|
1561
|
+
if (shell === "fish") {
|
|
1562
|
+
process.stdout.write(fishScript(verbs, nounsByVerb, flagsByVerb) + "\n");
|
|
1563
|
+
return 0;
|
|
1564
|
+
}
|
|
1565
|
+
throw new UsageError("Usage: ./dev completion bash|zsh|fish");
|
|
1566
|
+
}
|
|
1567
|
+
function bashScript(verbs, nouns, flags) {
|
|
1568
|
+
const nounCases = nouns.map((n) => ` ${n.verb}) COMPREPLY=( $(compgen -W "${n.nouns.join(" ")}" -- "$cur") ); return;;`).join("\n");
|
|
1569
|
+
const flagCases = flags.map((f) => ` ${f.verb}) extra="${f.flags.join(" ")}";;`).join("\n");
|
|
1570
|
+
return `# bash completion for ./dev \u2014 eval "$(./dev completion bash)"
|
|
1571
|
+
_dev_complete() {
|
|
1572
|
+
local cur prev verb extra
|
|
1573
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
1574
|
+
verb="\${COMP_WORDS[1]}"
|
|
1575
|
+
if [ "$COMP_CWORD" -eq 1 ]; then
|
|
1576
|
+
COMPREPLY=( $(compgen -W "${verbs.join(" ")}" -- "$cur") ); return
|
|
1577
|
+
fi
|
|
1578
|
+
case "$verb" in
|
|
1579
|
+
${nounCases}
|
|
1580
|
+
esac
|
|
1581
|
+
extra=""
|
|
1582
|
+
case "$verb" in
|
|
1583
|
+
${flagCases}
|
|
1584
|
+
esac
|
|
1585
|
+
COMPREPLY=( $(compgen -W "$extra" -- "$cur") )
|
|
1586
|
+
}
|
|
1587
|
+
complete -F _dev_complete ./dev dev`;
|
|
1588
|
+
}
|
|
1589
|
+
function zshScript(verbs, nouns, _flags) {
|
|
1590
|
+
const nounCases = nouns.map((n) => ` ${n.verb}) compadd ${n.nouns.join(" ")} ;;`).join("\n");
|
|
1591
|
+
return `#compdef dev ./dev
|
|
1592
|
+
# zsh completion for ./dev \u2014 eval "$(./dev completion zsh)"
|
|
1593
|
+
_dev() {
|
|
1594
|
+
if (( CURRENT == 2 )); then
|
|
1595
|
+
compadd ${verbs.join(" ")}
|
|
1596
|
+
return
|
|
1597
|
+
fi
|
|
1598
|
+
case "\${words[2]}" in
|
|
1599
|
+
${nounCases}
|
|
1600
|
+
esac
|
|
1601
|
+
}
|
|
1602
|
+
_dev "$@"`;
|
|
1603
|
+
}
|
|
1604
|
+
function fishScript(verbs, nouns, flags) {
|
|
1605
|
+
const lines = [
|
|
1606
|
+
"# fish completion for ./dev \u2014 ./dev completion fish | source",
|
|
1607
|
+
`complete -c dev -f`,
|
|
1608
|
+
`complete -c dev -n '__fish_use_subcommand' -a '${verbs.join(" ")}'`
|
|
1609
|
+
];
|
|
1610
|
+
for (const n of nouns) {
|
|
1611
|
+
lines.push(`complete -c dev -n '__fish_seen_subcommand_from ${n.verb}' -a '${n.nouns.join(" ")}'`);
|
|
1612
|
+
}
|
|
1613
|
+
for (const f of flags) {
|
|
1614
|
+
for (const flag of f.flags) {
|
|
1615
|
+
lines.push(`complete -c dev -n '__fish_seen_subcommand_from ${f.verb}' -l '${flag.replace(/^--/, "")}'`);
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
return lines.join("\n");
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
// src/generators/workspace-dev-cli/cli-src/src/registry.ts
|
|
1622
|
+
var COMMANDS = [
|
|
1623
|
+
{
|
|
1624
|
+
name: "start",
|
|
1625
|
+
group: "LIFECYCLE",
|
|
1626
|
+
summary: "Boot infrastructure (Docker) + app services (native)",
|
|
1627
|
+
flags: [{ name: "--docker", desc: "Run all services in Docker" }],
|
|
1628
|
+
handler: start
|
|
1629
|
+
},
|
|
1630
|
+
{
|
|
1631
|
+
name: "stop",
|
|
1632
|
+
group: "LIFECYCLE",
|
|
1633
|
+
summary: "Gracefully tear down all services",
|
|
1634
|
+
handler: stop
|
|
1635
|
+
},
|
|
1636
|
+
{
|
|
1637
|
+
name: "reset",
|
|
1638
|
+
group: "LIFECYCLE",
|
|
1639
|
+
summary: "Stop, wipe volumes, start & migrate (full recycle)",
|
|
1640
|
+
flags: [{ name: "--docker", desc: "Recycle the all-Docker topology" }],
|
|
1641
|
+
handler: reset
|
|
1642
|
+
},
|
|
1643
|
+
{
|
|
1644
|
+
name: "migrate",
|
|
1645
|
+
group: "LIFECYCLE",
|
|
1646
|
+
summary: "Create service databases & apply schemas",
|
|
1647
|
+
handler: migrate
|
|
1648
|
+
},
|
|
1649
|
+
{
|
|
1650
|
+
name: "status",
|
|
1651
|
+
group: "LIFECYCLE",
|
|
1652
|
+
summary: "Show running services (--watch for a live dashboard)",
|
|
1653
|
+
flags: [
|
|
1654
|
+
{ name: "--json", desc: "Emit machine-readable JSON" },
|
|
1655
|
+
{ name: "--watch", desc: "Live-refreshing dashboard (TTY only)" }
|
|
1656
|
+
],
|
|
1657
|
+
handler: status
|
|
1658
|
+
},
|
|
1659
|
+
{
|
|
1660
|
+
name: "logs",
|
|
1661
|
+
group: "LIFECYCLE",
|
|
1662
|
+
summary: "Print recent logs (logs <service> to filter; --follow to stream)",
|
|
1663
|
+
flags: [{ name: "--follow", desc: "Stream logs (TTY only)" }],
|
|
1664
|
+
handler: logs
|
|
1665
|
+
},
|
|
1666
|
+
{
|
|
1667
|
+
name: "health",
|
|
1668
|
+
group: "LIFECYCLE",
|
|
1669
|
+
summary: "Poll every app service + Jaeger health endpoint",
|
|
1670
|
+
flags: [{ name: "--json", desc: "Emit machine-readable JSON" }],
|
|
1671
|
+
handler: health
|
|
1672
|
+
},
|
|
1673
|
+
{
|
|
1674
|
+
name: "clean",
|
|
1675
|
+
group: "LIFECYCLE",
|
|
1676
|
+
summary: "Tear down & wipe state (--hard wipes volumes)",
|
|
1677
|
+
flags: [{ name: "--hard", desc: "Also wipe Docker volumes" }],
|
|
1678
|
+
handler: clean
|
|
1679
|
+
},
|
|
1680
|
+
{
|
|
1681
|
+
name: "doctor",
|
|
1682
|
+
group: "QUALITY",
|
|
1683
|
+
summary: "Verify the local environment",
|
|
1684
|
+
flags: [{ name: "--json", desc: "Emit machine-readable JSON" }],
|
|
1685
|
+
handler: doctor
|
|
1686
|
+
},
|
|
1687
|
+
{
|
|
1688
|
+
name: "test",
|
|
1689
|
+
group: "QUALITY",
|
|
1690
|
+
summary: "Run tests (integration | bet <slug>)",
|
|
1691
|
+
nouns: ["integration", "bet"],
|
|
1692
|
+
flags: [
|
|
1693
|
+
{ name: "--integration", desc: "Boot the stack for a bet suite" },
|
|
1694
|
+
{ name: "--keep", desc: "Leave the stack running after tests" }
|
|
1695
|
+
],
|
|
1696
|
+
handler: test
|
|
1697
|
+
},
|
|
1698
|
+
{
|
|
1699
|
+
name: "lint",
|
|
1700
|
+
group: "QUALITY",
|
|
1701
|
+
summary: "Run static analysis across services",
|
|
1702
|
+
handler: lint
|
|
1703
|
+
},
|
|
1704
|
+
{
|
|
1705
|
+
name: "new",
|
|
1706
|
+
group: "BET WORKFLOW",
|
|
1707
|
+
summary: "Scaffold a bet / milestone / slice (red test stubs)",
|
|
1708
|
+
nouns: ["bet", "milestone", "slice"],
|
|
1709
|
+
handler: newCmd
|
|
1710
|
+
},
|
|
1711
|
+
{
|
|
1712
|
+
name: "archive",
|
|
1713
|
+
group: "BET WORKFLOW",
|
|
1714
|
+
summary: "Archive a delivered bet's progress suite",
|
|
1715
|
+
nouns: ["bet"],
|
|
1716
|
+
handler: archive
|
|
1717
|
+
},
|
|
1718
|
+
{
|
|
1719
|
+
name: "bet",
|
|
1720
|
+
group: "BET WORKFLOW",
|
|
1721
|
+
summary: "Bet progress board (status [<slug>])",
|
|
1722
|
+
nouns: ["status"],
|
|
1723
|
+
flags: [{ name: "--json", desc: "Emit machine-readable JSON (status)" }],
|
|
1724
|
+
handler: betCmd
|
|
1725
|
+
},
|
|
1726
|
+
{
|
|
1727
|
+
name: "surface",
|
|
1728
|
+
group: "BET WORKFLOW",
|
|
1729
|
+
summary: "Surface registry & capability ledger (status)",
|
|
1730
|
+
nouns: ["status"],
|
|
1731
|
+
flags: [{ name: "--json", desc: "Emit machine-readable JSON (status)" }],
|
|
1732
|
+
handler: surfaceCmd
|
|
1733
|
+
},
|
|
1734
|
+
{
|
|
1735
|
+
name: "completion",
|
|
1736
|
+
group: "META",
|
|
1737
|
+
summary: "Print a shell completion script (bash|zsh|fish)",
|
|
1738
|
+
nouns: ["bash", "zsh", "fish"],
|
|
1739
|
+
handler: completion
|
|
1740
|
+
}
|
|
1741
|
+
];
|
|
1742
|
+
function findCommand(list, name) {
|
|
1743
|
+
return list.find((c) => c.name === name);
|
|
1744
|
+
}
|
|
1745
|
+
function shellQuote(arg) {
|
|
1746
|
+
return `'${arg.replace(/'/g, `'\\''`)}'`;
|
|
1747
|
+
}
|
|
1748
|
+
function projectCommandDef(pc) {
|
|
1749
|
+
return {
|
|
1750
|
+
name: pc.name,
|
|
1751
|
+
group: pc.group || "PROJECT",
|
|
1752
|
+
summary: pc.summary,
|
|
1753
|
+
handler: async (ctx) => {
|
|
1754
|
+
const cwd = pc.cwd ? path9.join(ROOT, pc.cwd) : ROOT;
|
|
1755
|
+
const env = pc.env ? { ...process.env, ...pc.env } : process.env;
|
|
1756
|
+
const extra = ctx.args.map(shellQuote).join(" ");
|
|
1757
|
+
const command = extra ? `${pc.run} ${extra}` : pc.run;
|
|
1758
|
+
return run("bash", ["-c", command], { cwd, env });
|
|
1759
|
+
}
|
|
1760
|
+
};
|
|
1761
|
+
}
|
|
1762
|
+
function buildRegistry(project) {
|
|
1763
|
+
const projDefs = project.map(projectCommandDef);
|
|
1764
|
+
const shadowed = new Set(projDefs.map((d) => d.name));
|
|
1765
|
+
const core = COMMANDS.filter((c) => !shadowed.has(c.name));
|
|
1766
|
+
return [...core, ...projDefs];
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
// src/generators/workspace-dev-cli/cli-src/src/theme/color.ts
|
|
1770
|
+
var RESET = "\x1B[0m";
|
|
1771
|
+
function envFlag(name) {
|
|
1772
|
+
const v = process.env[name];
|
|
1773
|
+
return v !== void 0 && v !== "" && v !== "0" && v.toLowerCase() !== "false";
|
|
1774
|
+
}
|
|
1775
|
+
function detectCaps(stream = process.stdout) {
|
|
1776
|
+
const isTTY = Boolean(stream.isTTY);
|
|
1777
|
+
const term = process.env.TERM ?? "";
|
|
1778
|
+
const unicode = term !== "dumb" && !envFlag("ASCII_ONLY") && /utf-?8/i.test(process.env.LC_ALL || process.env.LC_CTYPE || process.env.LANG || "UTF-8");
|
|
1779
|
+
let depth;
|
|
1780
|
+
if (envFlag("NO_COLOR")) {
|
|
1781
|
+
depth = "none";
|
|
1782
|
+
} else if (!isTTY && !envFlag("FORCE_COLOR")) {
|
|
1783
|
+
depth = "none";
|
|
1784
|
+
} else {
|
|
1785
|
+
const colorterm = (process.env.COLORTERM ?? "").toLowerCase();
|
|
1786
|
+
if (colorterm === "truecolor" || colorterm === "24bit") {
|
|
1787
|
+
depth = "truecolor";
|
|
1788
|
+
} else if (/256/.test(term)) {
|
|
1789
|
+
depth = "ansi256";
|
|
1790
|
+
} else if (term === "dumb" || term === "") {
|
|
1791
|
+
depth = "none";
|
|
1792
|
+
} else {
|
|
1793
|
+
depth = "ansi16";
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
return { depth, unicode, isTTY };
|
|
1797
|
+
}
|
|
1798
|
+
function hexToRgb(hex) {
|
|
1799
|
+
const m = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex.trim());
|
|
1800
|
+
if (!m) return [255, 255, 255];
|
|
1801
|
+
return [parseInt(m[1], 16), parseInt(m[2], 16), parseInt(m[3], 16)];
|
|
1802
|
+
}
|
|
1803
|
+
function treatmentCode(t) {
|
|
1804
|
+
switch (t) {
|
|
1805
|
+
case "bold":
|
|
1806
|
+
case "bold+upper":
|
|
1807
|
+
return "\x1B[1m";
|
|
1808
|
+
case "dim":
|
|
1809
|
+
return "\x1B[2m";
|
|
1810
|
+
case "underline":
|
|
1811
|
+
return "\x1B[4m";
|
|
1812
|
+
default:
|
|
1813
|
+
return "";
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
var Painter = class {
|
|
1817
|
+
constructor(tokens, caps) {
|
|
1818
|
+
this.tokens = tokens;
|
|
1819
|
+
this.caps = caps;
|
|
1820
|
+
}
|
|
1821
|
+
roleColor(role) {
|
|
1822
|
+
return this.tokens.terminal?.colorRoles?.[role];
|
|
1823
|
+
}
|
|
1824
|
+
/** Paint text in a semantic role, degrading by capability. */
|
|
1825
|
+
paint(role, text) {
|
|
1826
|
+
const r = this.roleColor(role);
|
|
1827
|
+
if (!r) return text;
|
|
1828
|
+
if (r.noColor === "bold+upper") text = text.toUpperCase();
|
|
1829
|
+
if (this.caps.depth === "none") {
|
|
1830
|
+
const code2 = treatmentCode(r.noColor);
|
|
1831
|
+
return code2 ? `${code2}${text}${RESET}` : text;
|
|
1832
|
+
}
|
|
1833
|
+
if (this.caps.depth === "truecolor" && r.truecolor) {
|
|
1834
|
+
const [rr, gg, bb] = hexToRgb(r.truecolor);
|
|
1835
|
+
return `\x1B[38;2;${rr};${gg};${bb}m${text}${RESET}`;
|
|
1836
|
+
}
|
|
1837
|
+
if ((this.caps.depth === "ansi256" || this.caps.depth === "truecolor") && r.ansi256 !== null) {
|
|
1838
|
+
return `\x1B[38;5;${r.ansi256}m${text}${RESET}`;
|
|
1839
|
+
}
|
|
1840
|
+
const code = treatmentCode(r.noColor);
|
|
1841
|
+
return code ? `${code}${text}${RESET}` : text;
|
|
1842
|
+
}
|
|
1843
|
+
/** Paint with the brand primary accent (truecolor only; degrades to bold). */
|
|
1844
|
+
primary(text) {
|
|
1845
|
+
if (this.caps.depth === "truecolor") {
|
|
1846
|
+
const [r, g, b] = hexToRgb(this.tokens.identity.primary);
|
|
1847
|
+
return `\x1B[38;2;${r};${g};${b}m${text}${RESET}`;
|
|
1848
|
+
}
|
|
1849
|
+
if (this.caps.depth === "none") return text;
|
|
1850
|
+
return `\x1B[1m${text}${RESET}`;
|
|
1851
|
+
}
|
|
1852
|
+
bold(text) {
|
|
1853
|
+
return this.caps.depth === "none" ? text : `\x1B[1m${text}${RESET}`;
|
|
1854
|
+
}
|
|
1855
|
+
dim(text) {
|
|
1856
|
+
return this.caps.depth === "none" ? text : `\x1B[2m${text}${RESET}`;
|
|
1857
|
+
}
|
|
1858
|
+
};
|
|
1859
|
+
|
|
1860
|
+
// src/generators/workspace-dev-cli/cli-src/src/theme/tokens.ts
|
|
1861
|
+
var DEFAULT_TOKENS = {
|
|
1862
|
+
identity: {
|
|
1863
|
+
appName: "Workspace",
|
|
1864
|
+
wordmark: "\u25E2\u25E4",
|
|
1865
|
+
primary: "#5fafff",
|
|
1866
|
+
accent: "#d7afff",
|
|
1867
|
+
voice: "clear, modern"
|
|
1868
|
+
},
|
|
1869
|
+
terminal: {
|
|
1870
|
+
colorRoles: {
|
|
1871
|
+
success: { truecolor: "#5faf87", ansi256: 72, noColor: "bold" },
|
|
1872
|
+
error: { truecolor: "#d75f5f", ansi256: 167, noColor: "bold" },
|
|
1873
|
+
warning: { truecolor: "#d7af5f", ansi256: 179, noColor: "bold" },
|
|
1874
|
+
info: { truecolor: "#5fafff", ansi256: 75, noColor: "dim" },
|
|
1875
|
+
muted: { truecolor: "#8a8a8a", ansi256: 245, noColor: "dim" },
|
|
1876
|
+
accent: { truecolor: "#d7afff", ansi256: 183, noColor: "underline" },
|
|
1877
|
+
header: { truecolor: null, ansi256: null, noColor: "bold+upper" },
|
|
1878
|
+
key: { truecolor: "#5fafff", ansi256: 75, noColor: "plain" },
|
|
1879
|
+
value: { truecolor: "#d0d0d0", ansi256: 252, noColor: "plain" }
|
|
1880
|
+
},
|
|
1881
|
+
symbols: {
|
|
1882
|
+
success: { unicode: "\u2714", ascii: "OK" },
|
|
1883
|
+
error: { unicode: "\u2716", ascii: "x" },
|
|
1884
|
+
warning: { unicode: "\u26A0", ascii: "!" },
|
|
1885
|
+
info: { unicode: "\u25CF", ascii: "*" },
|
|
1886
|
+
step: { unicode: "\u25B6", ascii: ">" },
|
|
1887
|
+
substep: { unicode: "\u21B3", ascii: "-" },
|
|
1888
|
+
active: { unicode: "\u276F", ascii: ">" }
|
|
1889
|
+
},
|
|
1890
|
+
splash: { style: "wordmark-line", tagline: "" },
|
|
1891
|
+
typography: {
|
|
1892
|
+
header: "bold + UPPERCASE",
|
|
1893
|
+
title: "bold + primary",
|
|
1894
|
+
body: "plain",
|
|
1895
|
+
muted: "dim"
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
};
|
|
1899
|
+
function mergeTokens(partial) {
|
|
1900
|
+
const p = partial ?? {};
|
|
1901
|
+
const identity = { ...DEFAULT_TOKENS.identity, ...p.identity ?? {} };
|
|
1902
|
+
const terminal = p.terminal ? {
|
|
1903
|
+
colorRoles: { ...DEFAULT_TOKENS.terminal.colorRoles, ...p.terminal.colorRoles },
|
|
1904
|
+
symbols: { ...DEFAULT_TOKENS.terminal.symbols, ...p.terminal.symbols },
|
|
1905
|
+
splash: { ...DEFAULT_TOKENS.terminal.splash, ...p.terminal.splash },
|
|
1906
|
+
typography: { ...DEFAULT_TOKENS.terminal.typography, ...p.terminal.typography }
|
|
1907
|
+
} : DEFAULT_TOKENS.terminal;
|
|
1908
|
+
return { identity, terminal };
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
// src/generators/workspace-dev-cli/cli-src/src/theme/render.ts
|
|
1912
|
+
var PAD = " ";
|
|
1913
|
+
var Renderer = class _Renderer {
|
|
1914
|
+
constructor(tokens, stream = process.stderr) {
|
|
1915
|
+
this.spinnerTimer = null;
|
|
1916
|
+
this.spinnerText = "";
|
|
1917
|
+
this.tokens = tokens;
|
|
1918
|
+
this.out = stream;
|
|
1919
|
+
this.painter = new Painter(tokens, detectCaps(stream));
|
|
1920
|
+
}
|
|
1921
|
+
/** A twin renderer bound to a different stream (e.g. stdout for command *results*,
|
|
1922
|
+
* while progress and spinners stay on stderr). */
|
|
1923
|
+
asStream(stream) {
|
|
1924
|
+
return new _Renderer(this.tokens, stream);
|
|
1925
|
+
}
|
|
1926
|
+
sym(name) {
|
|
1927
|
+
const t = this.tokens.terminal?.symbols?.[name];
|
|
1928
|
+
if (!t) return "";
|
|
1929
|
+
return this.painter.caps.unicode ? t.unicode : t.ascii;
|
|
1930
|
+
}
|
|
1931
|
+
write(line) {
|
|
1932
|
+
this.out.write(line + "\n");
|
|
1933
|
+
}
|
|
1934
|
+
logo(subtitle) {
|
|
1935
|
+
const { wordmark, appName } = this.tokens.identity;
|
|
1936
|
+
const mark = this.painter.primary(`${wordmark} ${appName}`.trim());
|
|
1937
|
+
this.write("");
|
|
1938
|
+
this.write(subtitle ? `${PAD}${this.painter.bold(mark)} ${this.painter.dim("\u2014 " + subtitle)}` : `${PAD}${this.painter.bold(mark)}`);
|
|
1939
|
+
this.write("");
|
|
1940
|
+
}
|
|
1941
|
+
step(text) {
|
|
1942
|
+
this.write(`
|
|
1943
|
+
${PAD}${this.painter.primary(this.sym("step"))} ${this.painter.bold(text)}`);
|
|
1944
|
+
}
|
|
1945
|
+
substep(text) {
|
|
1946
|
+
this.write(`${PAD}${PAD}${this.painter.dim(`${this.sym("substep")} ${text}`)}`);
|
|
1947
|
+
}
|
|
1948
|
+
info(text) {
|
|
1949
|
+
this.write(`${PAD}${this.painter.dim(this.sym("info"))} ${text}`);
|
|
1950
|
+
}
|
|
1951
|
+
success(text) {
|
|
1952
|
+
this.write(`${PAD}${this.painter.paint("success", this.sym("success"))} ${text}`);
|
|
1953
|
+
}
|
|
1954
|
+
error(text) {
|
|
1955
|
+
this.write(`${PAD}${this.painter.paint("error", this.sym("error"))} ${text}`);
|
|
1956
|
+
}
|
|
1957
|
+
warn(text) {
|
|
1958
|
+
this.write(`${PAD}${this.painter.paint("warning", this.sym("warning"))} ${text}`);
|
|
1959
|
+
}
|
|
1960
|
+
category(text) {
|
|
1961
|
+
this.write(`
|
|
1962
|
+
${PAD}${this.painter.dim("\u25A0")} ${this.painter.paint("header", text)}`);
|
|
1963
|
+
}
|
|
1964
|
+
cmd(name, desc) {
|
|
1965
|
+
this.write(` ${this.painter.paint("accent", name.padEnd(15))} ${desc}`);
|
|
1966
|
+
}
|
|
1967
|
+
/** A boxed error card with an optional action line. */
|
|
1968
|
+
errorCard(msg, action) {
|
|
1969
|
+
const bar = this.painter.paint("error", "\u2502");
|
|
1970
|
+
this.write("");
|
|
1971
|
+
this.write(`${PAD}${this.painter.paint("error", "\u256D" + "\u2500".repeat(58) + "\u256E")}`);
|
|
1972
|
+
this.write(`${PAD}${bar} ${this.painter.paint("error", this.sym("error"))} ${this.painter.bold("ERROR:")} ${msg}`);
|
|
1973
|
+
if (action) {
|
|
1974
|
+
this.write(`${PAD}${bar}`);
|
|
1975
|
+
this.write(`${PAD}${bar} ${this.painter.dim("Action required:")}`);
|
|
1976
|
+
this.write(`${PAD}${bar} ${this.painter.paint("accent", this.sym("active"))} ${action}`);
|
|
1977
|
+
}
|
|
1978
|
+
this.write(`${PAD}${this.painter.paint("error", "\u2570" + "\u2500".repeat(58) + "\u256F")}`);
|
|
1979
|
+
this.write("");
|
|
1980
|
+
}
|
|
1981
|
+
/** A simple three-column table for status output. */
|
|
1982
|
+
table(title, rows) {
|
|
1983
|
+
this.write(`${PAD}${this.painter.dim("\u256D\u2500")} ${this.painter.bold(title)}`);
|
|
1984
|
+
if (rows.length === 0) {
|
|
1985
|
+
this.write(`${PAD}${this.painter.dim("\u2502")} ${this.painter.dim("(none)")}`);
|
|
1986
|
+
}
|
|
1987
|
+
for (const [a, b, c] of rows) {
|
|
1988
|
+
this.write(`${PAD}${this.painter.dim("\u2502")} ${a.padEnd(28)} ${b.padEnd(16)} ${this.painter.dim(c)}`);
|
|
1989
|
+
}
|
|
1990
|
+
this.write(`${PAD}${this.painter.dim("\u2570" + "\u2500".repeat(40))}`);
|
|
1991
|
+
}
|
|
1992
|
+
// --- Spinner (TTY only; degrades to a static line) -------------------------
|
|
1993
|
+
startSpinner(text) {
|
|
1994
|
+
this.spinnerText = text;
|
|
1995
|
+
if (!this.painter.caps.isTTY) {
|
|
1996
|
+
this.write(`${PAD}${this.painter.dim(this.sym("info"))} ${text}...`);
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
2000
|
+
const asciiFrames = ["|", "/", "-", "\\"];
|
|
2001
|
+
const set = this.painter.caps.unicode ? frames : asciiFrames;
|
|
2002
|
+
let i = 0;
|
|
2003
|
+
this.out.write("\x1B[?25l");
|
|
2004
|
+
this.spinnerTimer = setInterval(() => {
|
|
2005
|
+
const frame = this.painter.primary(set[i % set.length]);
|
|
2006
|
+
this.out.write(`\r${PAD}${frame} ${this.spinnerText}`);
|
|
2007
|
+
i += 1;
|
|
2008
|
+
}, 90);
|
|
2009
|
+
}
|
|
2010
|
+
stopSpinner(successMsg, elapsed) {
|
|
2011
|
+
if (this.spinnerTimer) {
|
|
2012
|
+
clearInterval(this.spinnerTimer);
|
|
2013
|
+
this.spinnerTimer = null;
|
|
2014
|
+
this.out.write("\r\x1B[K");
|
|
2015
|
+
this.out.write("\x1B[?25h");
|
|
2016
|
+
}
|
|
2017
|
+
const time = elapsed ? ` ${this.painter.dim(`(${elapsed})`)}` : "";
|
|
2018
|
+
this.success(`${successMsg}${time}`);
|
|
2019
|
+
}
|
|
2020
|
+
failSpinner(failMsg) {
|
|
2021
|
+
if (this.spinnerTimer) {
|
|
2022
|
+
clearInterval(this.spinnerTimer);
|
|
2023
|
+
this.spinnerTimer = null;
|
|
2024
|
+
this.out.write("\r\x1B[K");
|
|
2025
|
+
this.out.write("\x1B[?25h");
|
|
2026
|
+
}
|
|
2027
|
+
this.error(failMsg);
|
|
2028
|
+
}
|
|
2029
|
+
};
|
|
2030
|
+
function makeRenderer(partialTokens, stream) {
|
|
2031
|
+
return new Renderer(mergeTokens(partialTokens), stream);
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
// src/generators/workspace-dev-cli/cli-src/src/util/extensions.ts
|
|
2035
|
+
var fs10 = __toESM(require("fs"));
|
|
2036
|
+
var path10 = __toESM(require("path"));
|
|
2037
|
+
var NAME_RX = /^[a-z][a-z0-9:-]*$/;
|
|
2038
|
+
function coerce(item) {
|
|
2039
|
+
if (!item || typeof item !== "object") return null;
|
|
2040
|
+
const c = item;
|
|
2041
|
+
if (typeof c.name !== "string" || !NAME_RX.test(c.name)) return null;
|
|
2042
|
+
if (typeof c.run !== "string" || !c.run.trim()) return null;
|
|
2043
|
+
return {
|
|
2044
|
+
name: c.name,
|
|
2045
|
+
summary: typeof c.summary === "string" && c.summary.trim() ? c.summary : "(project command)",
|
|
2046
|
+
group: typeof c.group === "string" && c.group.trim() ? c.group.trim().toUpperCase() : "PROJECT",
|
|
2047
|
+
run: c.run,
|
|
2048
|
+
cwd: typeof c.cwd === "string" ? c.cwd : void 0,
|
|
2049
|
+
env: c.env && typeof c.env === "object" && !Array.isArray(c.env) ? c.env : void 0
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
2052
|
+
function loadProjectCommands() {
|
|
2053
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2054
|
+
try {
|
|
2055
|
+
if (fs10.existsSync(CONFIG_PATH)) {
|
|
2056
|
+
const raw = JSON.parse(fs10.readFileSync(CONFIG_PATH, "utf8"));
|
|
2057
|
+
if (Array.isArray(raw.commands)) {
|
|
2058
|
+
for (const item of raw.commands) {
|
|
2059
|
+
const c = coerce(item);
|
|
2060
|
+
if (c) byName.set(c.name, c);
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
} catch {
|
|
2065
|
+
}
|
|
2066
|
+
try {
|
|
2067
|
+
const dir = path10.join(DEV_DIR, "commands");
|
|
2068
|
+
if (fs10.existsSync(dir)) {
|
|
2069
|
+
for (const f of fs10.readdirSync(dir).sort()) {
|
|
2070
|
+
if (!f.endsWith(".json")) continue;
|
|
2071
|
+
try {
|
|
2072
|
+
const c = coerce(JSON.parse(fs10.readFileSync(path10.join(dir, f), "utf8")));
|
|
2073
|
+
if (c) byName.set(c.name, c);
|
|
2074
|
+
} catch {
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
} catch {
|
|
2079
|
+
}
|
|
2080
|
+
return [...byName.values()];
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
// src/generators/workspace-dev-cli/cli-src/src/index.ts
|
|
2084
|
+
function loadConfig() {
|
|
2085
|
+
try {
|
|
2086
|
+
if (fs11.existsSync(CONFIG_PATH)) {
|
|
2087
|
+
const raw = JSON.parse(fs11.readFileSync(CONFIG_PATH, "utf8"));
|
|
2088
|
+
return { config: raw, tokens: { identity: raw.identity, terminal: raw.terminal } };
|
|
2089
|
+
}
|
|
2090
|
+
} catch {
|
|
2091
|
+
}
|
|
2092
|
+
return { config: {}, tokens: {} };
|
|
2093
|
+
}
|
|
2094
|
+
var CORE_GROUPS = ["LIFECYCLE", "QUALITY", "BET WORKFLOW", "META"];
|
|
2095
|
+
function showHelp(r, commands) {
|
|
2096
|
+
r.logo("Local Development CLI");
|
|
2097
|
+
const present = [...new Set(commands.map((c) => c.group))];
|
|
2098
|
+
const order = [
|
|
2099
|
+
...CORE_GROUPS.filter((g) => present.includes(g)),
|
|
2100
|
+
...present.filter((g) => !CORE_GROUPS.includes(g)).sort()
|
|
2101
|
+
];
|
|
2102
|
+
for (const g of order) {
|
|
2103
|
+
const cmds = commands.filter((c) => c.group === g);
|
|
2104
|
+
if (cmds.length === 0) continue;
|
|
2105
|
+
r.category(g);
|
|
2106
|
+
for (const c of cmds) r.cmd(c.name, c.summary);
|
|
2107
|
+
}
|
|
2108
|
+
process.stderr.write("\n");
|
|
2109
|
+
}
|
|
2110
|
+
async function main() {
|
|
2111
|
+
const argv = process.argv.slice(2);
|
|
2112
|
+
if (argv.includes("--version")) {
|
|
2113
|
+
process.stdout.write(`${DEV_CLI_VERSION}
|
|
2114
|
+
`);
|
|
2115
|
+
return 0;
|
|
2116
|
+
}
|
|
2117
|
+
let json = false;
|
|
2118
|
+
let help = false;
|
|
2119
|
+
const rest = [];
|
|
2120
|
+
for (const a of argv) {
|
|
2121
|
+
if (a === "--json") json = true;
|
|
2122
|
+
else if (a === "-h" || a === "--help") help = true;
|
|
2123
|
+
else if (a === "-v" || a === "--verbose") {
|
|
2124
|
+
} else rest.push(a);
|
|
2125
|
+
}
|
|
2126
|
+
const { config, tokens } = loadConfig();
|
|
2127
|
+
const r = makeRenderer(tokens);
|
|
2128
|
+
const registry = buildRegistry(loadProjectCommands());
|
|
2129
|
+
const command = rest[0];
|
|
2130
|
+
const args = rest.slice(1);
|
|
2131
|
+
if (!command || command === "help" || help) {
|
|
2132
|
+
showHelp(r, registry);
|
|
2133
|
+
return 0;
|
|
2134
|
+
}
|
|
2135
|
+
const def = findCommand(registry, command);
|
|
2136
|
+
if (!def) {
|
|
2137
|
+
r.error(`Unknown command: ${command}`);
|
|
2138
|
+
showHelp(r, registry);
|
|
2139
|
+
return 2;
|
|
2140
|
+
}
|
|
2141
|
+
const ctx = {
|
|
2142
|
+
r,
|
|
2143
|
+
json,
|
|
2144
|
+
args,
|
|
2145
|
+
projectPrefix: config.projectPrefix || "workspace",
|
|
2146
|
+
runners: parseRunners(config.runners),
|
|
2147
|
+
commands: registry
|
|
2148
|
+
};
|
|
2149
|
+
return def.handler(ctx);
|
|
2150
|
+
}
|
|
2151
|
+
process.on("SIGINT", () => {
|
|
2152
|
+
process.stderr.write("\n");
|
|
2153
|
+
process.stderr.write("\x1B[?25h");
|
|
2154
|
+
process.exit(130);
|
|
2155
|
+
});
|
|
2156
|
+
main().then((code) => process.exit(code)).catch((err) => {
|
|
2157
|
+
const r = makeRenderer({});
|
|
2158
|
+
if (err instanceof CliError) {
|
|
2159
|
+
r.errorCard(err.message, err.action);
|
|
2160
|
+
process.exit(1);
|
|
2161
|
+
}
|
|
2162
|
+
if (err instanceof UsageError) {
|
|
2163
|
+
r.error(err.message);
|
|
2164
|
+
process.exit(2);
|
|
2165
|
+
}
|
|
2166
|
+
r.errorCard(err?.message ?? String(err));
|
|
2167
|
+
process.exit(1);
|
|
2168
|
+
});
|