devchain-cli 0.9.2 → 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/dist/cli.js +985 -194
- package/dist/drizzle/0044_supreme_joshua_kane.sql +57 -0
- package/dist/drizzle/0045_provider_auto_compact_threshold.sql +11 -0
- package/dist/drizzle/0046_worktrees_owner_project_id.sql +3 -0
- package/dist/drizzle/meta/0044_snapshot.json +4620 -0
- package/dist/drizzle/meta/_journal.json +22 -1
- package/dist/node_modules/@devchain/shared/schemas/export-schema.d.ts +18 -0
- package/dist/node_modules/@devchain/shared/schemas/export-schema.d.ts.map +1 -1
- package/dist/node_modules/@devchain/shared/schemas/export-schema.js +6 -0
- package/dist/node_modules/@devchain/shared/schemas/export-schema.js.map +1 -1
- package/dist/node_modules/@devchain/shared/tsconfig.tsbuildinfo +1 -1
- package/dist/server/app.main.module.d.ts +4 -0
- package/dist/server/app.main.module.js +102 -0
- package/dist/server/app.main.module.js.map +1 -0
- package/dist/server/app.module.d.ts +1 -4
- package/dist/server/app.module.js +2 -78
- package/dist/server/app.module.js.map +1 -1
- package/dist/server/app.normal.module.d.ts +4 -0
- package/dist/server/app.normal.module.js +90 -0
- package/dist/server/app.normal.module.js.map +1 -0
- package/dist/server/common/config/env.config.d.ts +81 -1
- package/dist/server/common/config/env.config.js +52 -2
- package/dist/server/common/config/env.config.js.map +1 -1
- package/dist/server/common/templates-directory.d.ts +8 -0
- package/dist/server/common/templates-directory.js +56 -0
- package/dist/server/common/templates-directory.js.map +1 -0
- package/dist/server/main.js +58 -7
- package/dist/server/main.js.map +1 -1
- package/dist/server/modules/chat/dtos/chat.dto.d.ts +2 -2
- package/dist/server/modules/core/controllers/health.controller.d.ts +4 -0
- package/dist/server/modules/core/controllers/health.controller.js +22 -1
- package/dist/server/modules/core/controllers/health.controller.js.map +1 -1
- package/dist/server/modules/core/controllers/runtime.controller.d.ts +18 -0
- package/dist/server/modules/core/controllers/runtime.controller.js +130 -0
- package/dist/server/modules/core/controllers/runtime.controller.js.map +1 -0
- package/dist/server/modules/core/core-common.module.d.ts +2 -0
- package/dist/server/modules/core/core-common.module.js +24 -0
- package/dist/server/modules/core/core-common.module.js.map +1 -0
- package/dist/server/modules/core/core-main-health.module.d.ts +2 -0
- package/dist/server/modules/core/core-main-health.module.js +32 -0
- package/dist/server/modules/core/core-main-health.module.js.map +1 -0
- package/dist/server/modules/core/core-normal-health.module.d.ts +2 -0
- package/dist/server/modules/core/core-normal-health.module.js +29 -0
- package/dist/server/modules/core/core-normal-health.module.js.map +1 -0
- package/dist/server/modules/core/core-normal.module.d.ts +2 -0
- package/dist/server/modules/core/core-normal.module.js +28 -0
- package/dist/server/modules/core/core-normal.module.js.map +1 -0
- package/dist/server/modules/core/core.module.js +4 -11
- package/dist/server/modules/core/core.module.js.map +1 -1
- package/dist/server/modules/core/services/health.service.d.ts +13 -0
- package/dist/server/modules/core/services/health.service.js +39 -0
- package/dist/server/modules/core/services/health.service.js.map +1 -0
- package/dist/server/modules/core/services/main-readiness-checker.service.d.ts +14 -0
- package/dist/server/modules/core/services/main-readiness-checker.service.js +82 -0
- package/dist/server/modules/core/services/main-readiness-checker.service.js.map +1 -0
- package/dist/server/modules/core/services/normal-readiness-checker.service.d.ts +13 -0
- package/dist/server/modules/core/services/normal-readiness-checker.service.js +67 -0
- package/dist/server/modules/core/services/normal-readiness-checker.service.js.map +1 -0
- package/dist/server/modules/core/services/preflight.service.d.ts +1 -0
- package/dist/server/modules/core/services/preflight.service.js +18 -1
- package/dist/server/modules/core/services/preflight.service.js.map +1 -1
- package/dist/server/modules/core/services/provider-mcp-ensure.service.js +8 -0
- package/dist/server/modules/core/services/provider-mcp-ensure.service.js.map +1 -1
- package/dist/server/modules/epics/epics.module.js +2 -2
- package/dist/server/modules/epics/epics.module.js.map +1 -1
- package/dist/server/modules/events/catalog/claude.hooks.session.started.d.ts +39 -0
- package/dist/server/modules/events/catalog/claude.hooks.session.started.js +20 -0
- package/dist/server/modules/events/catalog/claude.hooks.session.started.js.map +1 -0
- package/dist/server/modules/events/catalog/epic.created.d.ts +2 -2
- package/dist/server/modules/events/catalog/index.d.ts +38 -4
- package/dist/server/modules/events/catalog/index.js +2 -0
- package/dist/server/modules/events/catalog/index.js.map +1 -1
- package/dist/server/modules/events/catalog/terminal.watcher.triggered.d.ts +2 -2
- package/dist/server/modules/events/controllers/event-log.controller.d.ts +1 -1
- package/dist/server/modules/events/controllers/event-log.controller.js +11 -9
- package/dist/server/modules/events/controllers/event-log.controller.js.map +1 -1
- package/dist/server/modules/events/dtos/event-log.dto.d.ts +1 -0
- package/dist/server/modules/events/events-domain.module.d.ts +2 -0
- package/dist/server/modules/events/events-domain.module.js +42 -0
- package/dist/server/modules/events/events-domain.module.js.map +1 -0
- package/dist/server/modules/events/events-infra.module.d.ts +2 -0
- package/dist/server/modules/events/events-infra.module.js +26 -0
- package/dist/server/modules/events/events-infra.module.js.map +1 -0
- package/dist/server/modules/events/events.module.js +4 -27
- package/dist/server/modules/events/events.module.js.map +1 -1
- package/dist/server/modules/events/index.d.ts +2 -0
- package/dist/server/modules/events/index.js +2 -0
- package/dist/server/modules/events/index.js.map +1 -1
- package/dist/server/modules/events/services/event-log.service.d.ts +8 -1
- package/dist/server/modules/events/services/event-log.service.js +41 -0
- package/dist/server/modules/events/services/event-log.service.js.map +1 -1
- package/dist/server/modules/events/subscribers/index.js +2 -0
- package/dist/server/modules/events/subscribers/index.js.map +1 -1
- package/dist/server/modules/events/subscribers/worktree-broadcaster.subscriber.d.ts +8 -0
- package/dist/server/modules/events/subscribers/worktree-broadcaster.subscriber.js +48 -0
- package/dist/server/modules/events/subscribers/worktree-broadcaster.subscriber.js.map +1 -0
- package/dist/server/modules/git/dtos/git.dto.d.ts +1 -1
- package/dist/server/modules/guests/guests.module.js +2 -2
- package/dist/server/modules/guests/guests.module.js.map +1 -1
- package/dist/server/modules/hooks/controllers/hooks.controller.d.ts +7 -0
- package/dist/server/modules/hooks/controllers/hooks.controller.js +49 -0
- package/dist/server/modules/hooks/controllers/hooks.controller.js.map +1 -0
- package/dist/server/modules/hooks/dtos/hook-event.dto.d.ts +41 -0
- package/dist/server/modules/hooks/dtos/hook-event.dto.js +19 -0
- package/dist/server/modules/hooks/dtos/hook-event.dto.js.map +1 -0
- package/dist/server/modules/hooks/hooks.module.d.ts +2 -0
- package/dist/server/modules/hooks/hooks.module.js +27 -0
- package/dist/server/modules/hooks/hooks.module.js.map +1 -0
- package/dist/server/modules/hooks/services/hooks-config.service.d.ts +5 -0
- package/dist/server/modules/hooks/services/hooks-config.service.js +215 -0
- package/dist/server/modules/hooks/services/hooks-config.service.js.map +1 -0
- package/dist/server/modules/hooks/services/hooks.service.d.ts +11 -0
- package/dist/server/modules/hooks/services/hooks.service.js +83 -0
- package/dist/server/modules/hooks/services/hooks.service.js.map +1 -0
- package/dist/server/modules/mcp/dtos/mcp.dto.d.ts +14 -14
- package/dist/server/modules/mcp/mcp.module.js +2 -2
- package/dist/server/modules/mcp/mcp.module.js.map +1 -1
- package/dist/server/modules/orchestrator/docker/docker.module.d.ts +2 -0
- package/dist/server/modules/orchestrator/docker/docker.module.js +22 -0
- package/dist/server/modules/orchestrator/docker/docker.module.js.map +1 -0
- package/dist/server/modules/orchestrator/docker/index.d.ts +3 -0
- package/dist/server/modules/orchestrator/docker/index.js +20 -0
- package/dist/server/modules/orchestrator/docker/index.js.map +1 -0
- package/dist/server/modules/orchestrator/docker/services/docker.service.d.ts +85 -0
- package/dist/server/modules/orchestrator/docker/services/docker.service.js +745 -0
- package/dist/server/modules/orchestrator/docker/services/docker.service.js.map +1 -0
- package/dist/server/modules/orchestrator/docker/services/seed-preparation.service.d.ts +11 -0
- package/dist/server/modules/orchestrator/docker/services/seed-preparation.service.js +181 -0
- package/dist/server/modules/orchestrator/docker/services/seed-preparation.service.js.map +1 -0
- package/dist/server/modules/orchestrator/git/controllers/git.controller.d.ts +8 -0
- package/dist/server/modules/orchestrator/git/controllers/git.controller.js +38 -0
- package/dist/server/modules/orchestrator/git/controllers/git.controller.js.map +1 -0
- package/dist/server/modules/orchestrator/git/git.module.d.ts +2 -0
- package/dist/server/modules/orchestrator/git/git.module.js +23 -0
- package/dist/server/modules/orchestrator/git/git.module.js.map +1 -0
- package/dist/server/modules/orchestrator/git/index.d.ts +3 -0
- package/dist/server/modules/orchestrator/git/index.js +20 -0
- package/dist/server/modules/orchestrator/git/index.js.map +1 -0
- package/dist/server/modules/orchestrator/git/services/git-worktree.service.d.ts +83 -0
- package/dist/server/modules/orchestrator/git/services/git-worktree.service.js +474 -0
- package/dist/server/modules/orchestrator/git/services/git-worktree.service.js.map +1 -0
- package/dist/server/modules/orchestrator/index.d.ts +6 -0
- package/dist/server/modules/orchestrator/index.js +23 -0
- package/dist/server/modules/orchestrator/index.js.map +1 -0
- package/dist/server/modules/orchestrator/orchestrator-storage/db/index.d.ts +1 -0
- package/dist/server/modules/orchestrator/orchestrator-storage/db/index.js +18 -0
- package/dist/server/modules/orchestrator/orchestrator-storage/db/index.js.map +1 -0
- package/dist/server/modules/orchestrator/orchestrator-storage/db/orchestrator.provider.d.ts +5 -0
- package/dist/server/modules/orchestrator/orchestrator-storage/db/orchestrator.provider.js +10 -0
- package/dist/server/modules/orchestrator/orchestrator-storage/db/orchestrator.provider.js.map +1 -0
- package/dist/server/modules/orchestrator/orchestrator-storage/index.d.ts +2 -0
- package/dist/server/modules/orchestrator/orchestrator-storage/index.js +19 -0
- package/dist/server/modules/orchestrator/orchestrator-storage/index.js.map +1 -0
- package/dist/server/modules/orchestrator/orchestrator-storage/orchestrator-storage.module.d.ts +2 -0
- package/dist/server/modules/orchestrator/orchestrator-storage/orchestrator-storage.module.js +23 -0
- package/dist/server/modules/orchestrator/orchestrator-storage/orchestrator-storage.module.js.map +1 -0
- package/dist/server/modules/orchestrator/proxy/index.d.ts +2 -0
- package/dist/server/modules/orchestrator/proxy/index.js +19 -0
- package/dist/server/modules/orchestrator/proxy/index.js.map +1 -0
- package/dist/server/modules/orchestrator/proxy/orchestrator-proxy.module.d.ts +2 -0
- package/dist/server/modules/orchestrator/proxy/orchestrator-proxy.module.js +22 -0
- package/dist/server/modules/orchestrator/proxy/orchestrator-proxy.module.js.map +1 -0
- package/dist/server/modules/orchestrator/proxy/services/orchestrator-proxy.service.d.ts +18 -0
- package/dist/server/modules/orchestrator/proxy/services/orchestrator-proxy.service.js +192 -0
- package/dist/server/modules/orchestrator/proxy/services/orchestrator-proxy.service.js.map +1 -0
- package/dist/server/modules/orchestrator/sync/controllers/overview.controller.d.ts +9 -0
- package/dist/server/modules/orchestrator/sync/controllers/overview.controller.js +66 -0
- package/dist/server/modules/orchestrator/sync/controllers/overview.controller.js.map +1 -0
- package/dist/server/modules/orchestrator/sync/dtos/overview.dto.d.ts +52 -0
- package/dist/server/modules/orchestrator/sync/dtos/overview.dto.js +3 -0
- package/dist/server/modules/orchestrator/sync/dtos/overview.dto.js.map +1 -0
- package/dist/server/modules/orchestrator/sync/dtos/task-merge.dto.d.ts +5 -0
- package/dist/server/modules/orchestrator/sync/dtos/task-merge.dto.js +3 -0
- package/dist/server/modules/orchestrator/sync/dtos/task-merge.dto.js.map +1 -0
- package/dist/server/modules/orchestrator/sync/events/task-merge.events.d.ts +4 -0
- package/dist/server/modules/orchestrator/sync/events/task-merge.events.js +5 -0
- package/dist/server/modules/orchestrator/sync/events/task-merge.events.js.map +1 -0
- package/dist/server/modules/orchestrator/sync/index.d.ts +7 -0
- package/dist/server/modules/orchestrator/sync/index.js +24 -0
- package/dist/server/modules/orchestrator/sync/index.js.map +1 -0
- package/dist/server/modules/orchestrator/sync/services/lazy-fetch.service.d.ts +31 -0
- package/dist/server/modules/orchestrator/sync/services/lazy-fetch.service.js +410 -0
- package/dist/server/modules/orchestrator/sync/services/lazy-fetch.service.js.map +1 -0
- package/dist/server/modules/orchestrator/sync/services/task-merge.service.d.ts +44 -0
- package/dist/server/modules/orchestrator/sync/services/task-merge.service.js +730 -0
- package/dist/server/modules/orchestrator/sync/services/task-merge.service.js.map +1 -0
- package/dist/server/modules/orchestrator/sync/sync.module.d.ts +2 -0
- package/dist/server/modules/orchestrator/sync/sync.module.js +36 -0
- package/dist/server/modules/orchestrator/sync/sync.module.js.map +1 -0
- package/dist/server/modules/orchestrator/ui/app/lib/worktrees.d.ts +118 -0
- package/dist/server/modules/orchestrator/ui/app/lib/worktrees.js +297 -0
- package/dist/server/modules/orchestrator/ui/app/lib/worktrees.js.map +1 -0
- package/dist/server/modules/orchestrator/ui/app/orchestrator-app.d.ts +17 -0
- package/dist/server/modules/orchestrator/ui/app/orchestrator-app.js +752 -0
- package/dist/server/modules/orchestrator/ui/app/orchestrator-app.js.map +1 -0
- package/dist/server/modules/orchestrator/worktrees/controllers/templates.controller.d.ts +15 -0
- package/dist/server/modules/orchestrator/worktrees/controllers/templates.controller.js +85 -0
- package/dist/server/modules/orchestrator/worktrees/controllers/templates.controller.js.map +1 -0
- package/dist/server/modules/orchestrator/worktrees/controllers/worktrees.controller.d.ts +29 -0
- package/dist/server/modules/orchestrator/worktrees/controllers/worktrees.controller.js +272 -0
- package/dist/server/modules/orchestrator/worktrees/controllers/worktrees.controller.js.map +1 -0
- package/dist/server/modules/orchestrator/worktrees/dtos/worktree.dto.d.ts +109 -0
- package/dist/server/modules/orchestrator/worktrees/dtos/worktree.dto.js +57 -0
- package/dist/server/modules/orchestrator/worktrees/dtos/worktree.dto.js.map +1 -0
- package/dist/server/modules/orchestrator/worktrees/events/worktree.events.d.ts +4 -0
- package/dist/server/modules/orchestrator/worktrees/events/worktree.events.js +5 -0
- package/dist/server/modules/orchestrator/worktrees/events/worktree.events.js.map +1 -0
- package/dist/server/modules/orchestrator/worktrees/index.d.ts +7 -0
- package/dist/server/modules/orchestrator/worktrees/index.js +24 -0
- package/dist/server/modules/orchestrator/worktrees/index.js.map +1 -0
- package/dist/server/modules/orchestrator/worktrees/local-worktrees.store.d.ts +18 -0
- package/dist/server/modules/orchestrator/worktrees/local-worktrees.store.js +198 -0
- package/dist/server/modules/orchestrator/worktrees/local-worktrees.store.js.map +1 -0
- package/dist/server/modules/orchestrator/worktrees/services/worktrees.service.d.ts +87 -0
- package/dist/server/modules/orchestrator/worktrees/services/worktrees.service.js +1380 -0
- package/dist/server/modules/orchestrator/worktrees/services/worktrees.service.js.map +1 -0
- package/dist/server/modules/orchestrator/worktrees/worktree-validation.d.ts +4 -0
- package/dist/server/modules/orchestrator/worktrees/worktree-validation.js +43 -0
- package/dist/server/modules/orchestrator/worktrees/worktree-validation.js.map +1 -0
- package/dist/server/modules/orchestrator/worktrees/worktrees.module.d.ts +2 -0
- package/dist/server/modules/orchestrator/worktrees/worktrees.module.js +44 -0
- package/dist/server/modules/orchestrator/worktrees/worktrees.module.js.map +1 -0
- package/dist/server/modules/orchestrator/worktrees/worktrees.store.d.ts +38 -0
- package/dist/server/modules/orchestrator/worktrees/worktrees.store.js +5 -0
- package/dist/server/modules/orchestrator/worktrees/worktrees.store.js.map +1 -0
- package/dist/server/modules/profiles/dto.d.ts +4 -4
- package/dist/server/modules/projects/controllers/projects.controller.d.ts +13 -3
- package/dist/server/modules/projects/controllers/projects.controller.js +55 -7
- package/dist/server/modules/projects/controllers/projects.controller.js.map +1 -1
- package/dist/server/modules/projects/projects.module.js +3 -2
- package/dist/server/modules/projects/projects.module.js.map +1 -1
- package/dist/server/modules/projects/services/main-project-bootstrap.service.d.ts +11 -0
- package/dist/server/modules/projects/services/main-project-bootstrap.service.js +76 -0
- package/dist/server/modules/projects/services/main-project-bootstrap.service.js.map +1 -0
- package/dist/server/modules/projects/services/projects.service.d.ts +8 -0
- package/dist/server/modules/projects/services/projects.service.js +115 -37
- package/dist/server/modules/projects/services/projects.service.js.map +1 -1
- package/dist/server/modules/providers/adapters/gemini.adapter.d.ts +1 -1
- package/dist/server/modules/providers/adapters/gemini.adapter.js +6 -4
- package/dist/server/modules/providers/adapters/gemini.adapter.js.map +1 -1
- package/dist/server/modules/providers/controllers/providers.controller.d.ts +3 -0
- package/dist/server/modules/providers/controllers/providers.controller.js +28 -0
- package/dist/server/modules/providers/controllers/providers.controller.js.map +1 -1
- package/dist/server/modules/providers/providers.module.js +2 -2
- package/dist/server/modules/providers/providers.module.js.map +1 -1
- package/dist/server/modules/registry/services/unified-template.service.js +6 -18
- package/dist/server/modules/registry/services/unified-template.service.js.map +1 -1
- package/dist/server/modules/reviews/dtos/review.dto.d.ts +4 -4
- package/dist/server/modules/reviews/reviews.module.js +2 -2
- package/dist/server/modules/reviews/reviews.module.js.map +1 -1
- package/dist/server/modules/seeders/seeders/0005_seed_renew_instructions_subscriber.d.ts +3 -0
- package/dist/server/modules/seeders/seeders/0005_seed_renew_instructions_subscriber.js +89 -0
- package/dist/server/modules/seeders/seeders/0005_seed_renew_instructions_subscriber.js.map +1 -0
- package/dist/server/modules/seeders/seeders/0006_seed_rename_template_slugs.d.ts +3 -0
- package/dist/server/modules/seeders/seeders/0006_seed_rename_template_slugs.js +60 -0
- package/dist/server/modules/seeders/seeders/0006_seed_rename_template_slugs.js.map +1 -0
- package/dist/server/modules/seeders/services/data-seeder.service.js +4 -0
- package/dist/server/modules/seeders/services/data-seeder.service.js.map +1 -1
- package/dist/server/modules/sessions/services/sessions.service.d.ts +3 -1
- package/dist/server/modules/sessions/services/sessions.service.js +79 -22
- package/dist/server/modules/sessions/services/sessions.service.js.map +1 -1
- package/dist/server/modules/sessions/sessions.module.js +6 -4
- package/dist/server/modules/sessions/sessions.module.js.map +1 -1
- package/dist/server/modules/sessions/utils/claude-config.d.ts +6 -2
- package/dist/server/modules/sessions/utils/claude-config.js +19 -6
- package/dist/server/modules/sessions/utils/claude-config.js.map +1 -1
- package/dist/server/modules/settings/dtos/settings.dto.d.ts +4 -4
- package/dist/server/modules/skills/dtos/community-sources.dto.d.ts +2 -2
- package/dist/server/modules/skills/dtos/local-sources.dto.d.ts +2 -2
- package/dist/server/modules/skills/dtos/skill.dto.d.ts +7 -7
- package/dist/server/modules/storage/db/schema.d.ts +811 -0
- package/dist/server/modules/storage/db/schema.js +63 -1
- package/dist/server/modules/storage/db/schema.js.map +1 -1
- package/dist/server/modules/storage/db/sqlite-json.d.ts +2 -0
- package/dist/server/modules/storage/db/sqlite-json.js +8 -0
- package/dist/server/modules/storage/db/sqlite-json.js.map +1 -0
- package/dist/server/modules/storage/interfaces/storage.interface.d.ts +4 -1
- package/dist/server/modules/storage/interfaces/storage.interface.js.map +1 -1
- package/dist/server/modules/storage/local/local-storage.service.d.ts +1 -1
- package/dist/server/modules/storage/local/local-storage.service.js +19 -2
- package/dist/server/modules/storage/local/local-storage.service.js.map +1 -1
- package/dist/server/modules/storage/models/domain.models.d.ts +2 -0
- package/dist/server/modules/subscribers/dtos/subscriber.dto.d.ts +16 -16
- package/dist/server/modules/subscribers/events/event-fields-catalog.js +18 -0
- package/dist/server/modules/subscribers/events/event-fields-catalog.js.map +1 -1
- package/dist/server/modules/subscribers/subscribers.module.js +2 -2
- package/dist/server/modules/subscribers/subscribers.module.js.map +1 -1
- package/dist/server/modules/terminal/gateways/terminal.gateway.js +7 -2
- package/dist/server/modules/terminal/gateways/terminal.gateway.js.map +1 -1
- package/dist/server/modules/terminal/services/tmux.service.d.ts +9 -0
- package/dist/server/modules/terminal/services/tmux.service.js +55 -5
- package/dist/server/modules/terminal/services/tmux.service.js.map +1 -1
- package/dist/server/modules/terminal/terminal.module.js +2 -2
- package/dist/server/modules/terminal/terminal.module.js.map +1 -1
- package/dist/server/modules/watchers/watchers.module.js +2 -2
- package/dist/server/modules/watchers/watchers.module.js.map +1 -1
- package/dist/server/templates/3-agents-dev.json +662 -0
- package/dist/server/templates/{dev-loop.json → 5-agents-dev.json} +174 -100
- package/dist/server/test-setup.js +7 -0
- package/dist/server/test-setup.js.map +1 -1
- package/dist/server/tsconfig.tsbuildinfo +1 -1
- package/dist/server/ui/assets/ReviewDetailPage-CZZQtaY7.js +1 -0
- package/dist/server/ui/assets/{ReviewsPage-MKT-vv59.js → ReviewsPage-C209GLQG.js} +1 -1
- package/dist/server/ui/assets/index-DvRuLfpZ.css +32 -0
- package/dist/server/ui/assets/index-Th1FDtKR.js +977 -0
- package/dist/server/ui/assets/{useReviewSubscription-Dc58i6Bk.js → useReviewSubscription-Dcabsa78.js} +1 -1
- package/dist/server/ui/index.html +2 -2
- package/dist/templates/3-agents-dev.json +662 -0
- package/dist/templates/{dev-loop.json → 5-agents-dev.json} +174 -100
- package/package.json +15 -1
- package/dist/server/ui/assets/ReviewDetailPage-BvSckWKj.js +0 -6
- package/dist/server/ui/assets/index-BtUq-Qxb.css +0 -32
- package/dist/server/ui/assets/index-kTb634Zp.js +0 -945
package/dist/cli.js
CHANGED
|
@@ -10,9 +10,9 @@ const { Command } = require('commander');
|
|
|
10
10
|
const getPort = require('get-port');
|
|
11
11
|
const open = require('open');
|
|
12
12
|
const { join, dirname, basename } = require('path');
|
|
13
|
-
const { existsSync, writeFileSync, readFileSync, unlinkSync, mkdirSync, openSync } = require('fs');
|
|
13
|
+
const { existsSync, writeFileSync, readFileSync, unlinkSync, mkdirSync, openSync, realpathSync } = require('fs');
|
|
14
14
|
const { homedir, platform } = require('os');
|
|
15
|
-
const { spawn, execSync } = require('child_process');
|
|
15
|
+
const { spawn, execSync, execFileSync } = require('child_process');
|
|
16
16
|
const { InteractiveCLI } = require('./lib/interactive-cli');
|
|
17
17
|
const readline = require('readline');
|
|
18
18
|
|
|
@@ -96,6 +96,116 @@ function getChangelogBetweenVersions(changelog, fromVersion, toVersion) {
|
|
|
96
96
|
return changes;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
function normalizeCliArgv(argv) {
|
|
100
|
+
const rawArgs = argv.slice(2);
|
|
101
|
+
const hasContainerFlag = rawArgs.includes('--container');
|
|
102
|
+
if (!hasContainerFlag) {
|
|
103
|
+
return argv;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const knownCommands = new Set(['start', 'stop', 'help']);
|
|
107
|
+
const hasKnownCommand = rawArgs.some((arg) => knownCommands.has(arg));
|
|
108
|
+
if (hasKnownCommand) {
|
|
109
|
+
return argv;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const passthrough = rawArgs.filter((arg) => arg !== '--container');
|
|
113
|
+
return [argv[0], argv[1], 'start', '--container', ...passthrough];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const WORKTREE_RUNTIME_TYPES = new Set(['container', 'process']);
|
|
117
|
+
|
|
118
|
+
function normalizeWorktreeRuntimeType(rawRuntimeType) {
|
|
119
|
+
if (typeof rawRuntimeType !== 'string') {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const normalized = rawRuntimeType.trim().toLowerCase();
|
|
124
|
+
if (!normalized) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!WORKTREE_RUNTIME_TYPES.has(normalized)) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`Invalid --worktree-runtime value "${rawRuntimeType}". Expected one of: container, process.`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return normalized;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function isWorktreeRuntimeModeEnabled(worktreeRuntimeType) {
|
|
138
|
+
return worktreeRuntimeType === 'container' || worktreeRuntimeType === 'process';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Detect which global package manager owns the devchain install.
|
|
143
|
+
*
|
|
144
|
+
* @param {string} packageName - The npm package name (e.g. 'devchain-cli')
|
|
145
|
+
* @param {object} [deps] - Dependency-injected functions for testability
|
|
146
|
+
* @returns {{ name: 'npm'|'pnpm', installCmd: string[], sudoInstallCmd: string[]|null, manualCmd: string }|null}
|
|
147
|
+
*/
|
|
148
|
+
function detectGlobalPackageManager(packageName, {
|
|
149
|
+
realpathSyncFn = realpathSync,
|
|
150
|
+
execFileSyncFn = execFileSync,
|
|
151
|
+
argvPath = process.argv[1],
|
|
152
|
+
} = {}) {
|
|
153
|
+
try {
|
|
154
|
+
let scriptRealPath;
|
|
155
|
+
try {
|
|
156
|
+
scriptRealPath = realpathSyncFn(argvPath);
|
|
157
|
+
} catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const pms = [
|
|
162
|
+
{ name: 'pnpm', rootArgs: ['root', '-g'], installVerb: 'add' },
|
|
163
|
+
{ name: 'npm', rootArgs: ['root', '-g'], installVerb: 'install' },
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
const available = [];
|
|
167
|
+
for (const pm of pms) {
|
|
168
|
+
try {
|
|
169
|
+
execFileSyncFn(pm.name, ['--version'], { stdio: 'ignore' });
|
|
170
|
+
available.push(pm);
|
|
171
|
+
} catch {
|
|
172
|
+
// PM not on PATH
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (available.length === 0) return null;
|
|
177
|
+
|
|
178
|
+
const owners = [];
|
|
179
|
+
for (const pm of available) {
|
|
180
|
+
try {
|
|
181
|
+
const globalRoot = execFileSyncFn(pm.name, pm.rootArgs, {
|
|
182
|
+
encoding: 'utf8',
|
|
183
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
184
|
+
}).trim();
|
|
185
|
+
if (globalRoot && scriptRealPath.startsWith(globalRoot)) {
|
|
186
|
+
owners.push(pm);
|
|
187
|
+
}
|
|
188
|
+
} catch {
|
|
189
|
+
// root -g failed
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Ambiguous: both match or neither match
|
|
194
|
+
if (owners.length !== 1) return null;
|
|
195
|
+
|
|
196
|
+
const pm = owners[0];
|
|
197
|
+
const installCmd = [pm.name, pm.installVerb, '-g', `${packageName}@latest`];
|
|
198
|
+
const sudoInstallCmd = platform() !== 'win32'
|
|
199
|
+
? ['sudo', ...installCmd]
|
|
200
|
+
: null;
|
|
201
|
+
const manualCmd = `${pm.name} ${pm.installVerb} -g ${packageName}`;
|
|
202
|
+
|
|
203
|
+
return { name: pm.name, installCmd, sudoInstallCmd, manualCmd };
|
|
204
|
+
} catch {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
99
209
|
async function checkForUpdates(cli, askYesNoFn) {
|
|
100
210
|
try {
|
|
101
211
|
const pkg = require('../package.json');
|
|
@@ -130,26 +240,35 @@ async function checkForUpdates(cli, askYesNoFn) {
|
|
|
130
240
|
const shouldUpdate = await askYesNoFn('Would you like to update now?', true);
|
|
131
241
|
|
|
132
242
|
if (shouldUpdate) {
|
|
133
|
-
|
|
243
|
+
const pm = detectGlobalPackageManager(packageName);
|
|
244
|
+
|
|
245
|
+
if (!pm) {
|
|
246
|
+
// Cannot determine owning PM — show manual instructions
|
|
247
|
+
cli.warn('Could not detect the package manager used to install devchain.');
|
|
248
|
+
cli.info('Please update manually:');
|
|
249
|
+
cli.info(' npm install -g ' + packageName);
|
|
250
|
+
cli.info(' pnpm add -g ' + packageName);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
cli.info(`Updating devchain via ${pm.name}...`);
|
|
134
255
|
try {
|
|
135
|
-
|
|
136
|
-
// Use npm install -g instead of update -g (update can remove packages)
|
|
137
|
-
execSync(`npm install -g ${packageName}@latest`, { stdio: 'inherit' });
|
|
256
|
+
execFileSync(pm.installCmd[0], pm.installCmd.slice(1), { stdio: 'inherit' });
|
|
138
257
|
cli.success('Update complete! Please restart devchain.');
|
|
139
258
|
process.exit(0);
|
|
140
259
|
} catch (e) {
|
|
141
|
-
// On Linux/Mac, might need sudo for system
|
|
142
|
-
if (
|
|
260
|
+
// On Linux/Mac, might need sudo for system installs
|
|
261
|
+
if (pm.sudoInstallCmd) {
|
|
143
262
|
cli.info('Retrying with sudo...');
|
|
144
263
|
try {
|
|
145
|
-
|
|
264
|
+
execFileSync(pm.sudoInstallCmd[0], pm.sudoInstallCmd.slice(1), { stdio: 'inherit' });
|
|
146
265
|
cli.success('Update complete! Please restart devchain.');
|
|
147
266
|
process.exit(0);
|
|
148
267
|
} catch (e2) {
|
|
149
|
-
cli.error('Update failed. You can manually run: sudo
|
|
268
|
+
cli.error('Update failed. You can manually run: sudo ' + pm.manualCmd);
|
|
150
269
|
}
|
|
151
270
|
} else {
|
|
152
|
-
cli.error('Update failed. You can manually run:
|
|
271
|
+
cli.error('Update failed. You can manually run: ' + pm.manualCmd);
|
|
153
272
|
}
|
|
154
273
|
}
|
|
155
274
|
}
|
|
@@ -181,6 +300,424 @@ function detectInstalledProviders() {
|
|
|
181
300
|
return detected; // Map<name, absolutePath>
|
|
182
301
|
}
|
|
183
302
|
|
|
303
|
+
const ORCHESTRATOR_WORKTREE_IMAGE_REPO = 'ghcr.io/twitech-lab/devchain';
|
|
304
|
+
const WORKTREE_IMAGE_BUILD_TIMEOUT_MS = 30 * 60 * 1000;
|
|
305
|
+
|
|
306
|
+
function sleep(ms) {
|
|
307
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function ensureDockerAvailable(execSyncFn = execSync) {
|
|
311
|
+
try {
|
|
312
|
+
execSyncFn('docker info --format "{{.ID}}"', {
|
|
313
|
+
stdio: 'pipe',
|
|
314
|
+
timeout: 10000,
|
|
315
|
+
});
|
|
316
|
+
} catch (_) {
|
|
317
|
+
throw new Error('Container mode requires Docker. Please install Docker and try again.');
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function isDockerAvailable(execSyncFn = execSync) {
|
|
322
|
+
try {
|
|
323
|
+
ensureDockerAvailable(execSyncFn);
|
|
324
|
+
return true;
|
|
325
|
+
} catch (_) {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function deriveRepoRootFromGit(execSyncFn = execSync) {
|
|
331
|
+
try {
|
|
332
|
+
const gitTopLevel = execSyncFn('git rev-parse --show-toplevel', {
|
|
333
|
+
encoding: 'utf8',
|
|
334
|
+
stdio: 'pipe',
|
|
335
|
+
timeout: 5000,
|
|
336
|
+
}).trim();
|
|
337
|
+
|
|
338
|
+
if (!gitTopLevel) {
|
|
339
|
+
throw new Error('empty-git-top-level');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
process.env.REPO_ROOT = gitTopLevel;
|
|
343
|
+
return gitTopLevel;
|
|
344
|
+
} catch (_) {
|
|
345
|
+
throw new Error('Container mode must be run from within a git repository.');
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function isInsideGitRepo(execSyncFn = execSync) {
|
|
350
|
+
const previousRepoRoot = process.env.REPO_ROOT;
|
|
351
|
+
try {
|
|
352
|
+
deriveRepoRootFromGit(execSyncFn);
|
|
353
|
+
return true;
|
|
354
|
+
} catch (_) {
|
|
355
|
+
return false;
|
|
356
|
+
} finally {
|
|
357
|
+
if (typeof previousRepoRoot === 'string') {
|
|
358
|
+
process.env.REPO_ROOT = previousRepoRoot;
|
|
359
|
+
} else {
|
|
360
|
+
delete process.env.REPO_ROOT;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function ensureProjectGitignoreIncludesDevchain(repoRoot) {
|
|
366
|
+
const normalizedRepoRoot = typeof repoRoot === 'string' ? repoRoot.trim() : '';
|
|
367
|
+
if (!normalizedRepoRoot || !existsSync(normalizedRepoRoot)) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const gitignorePath = join(normalizedRepoRoot, '.gitignore');
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
const existingContent = existsSync(gitignorePath) ? readFileSync(gitignorePath, 'utf8') : '';
|
|
375
|
+
const alreadyIgnored = existingContent.split(/\r?\n/).some((line) => {
|
|
376
|
+
const trimmed = line.trim();
|
|
377
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
return (
|
|
381
|
+
trimmed === '.devchain/' ||
|
|
382
|
+
trimmed === '/.devchain/' ||
|
|
383
|
+
trimmed === '.devchain' ||
|
|
384
|
+
trimmed === '/.devchain'
|
|
385
|
+
);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
if (alreadyIgnored) {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const delimiter = existingContent.length > 0 && !existingContent.endsWith('\n') ? '\n' : '';
|
|
393
|
+
writeFileSync(gitignorePath, `${existingContent}${delimiter}.devchain/\n`, 'utf8');
|
|
394
|
+
} catch (error) {
|
|
395
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
396
|
+
console.warn(`Warning: unable to update project .gitignore with .devchain/ (${message})`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function resolveRepoRootForDockerBuild(execSyncFn = execSync) {
|
|
401
|
+
const repoRootFromEnv = typeof process.env.REPO_ROOT === 'string' ? process.env.REPO_ROOT.trim() : '';
|
|
402
|
+
if (repoRootFromEnv) {
|
|
403
|
+
return repoRootFromEnv;
|
|
404
|
+
}
|
|
405
|
+
try {
|
|
406
|
+
return deriveRepoRootFromGit(execSyncFn);
|
|
407
|
+
} catch (_) {
|
|
408
|
+
return join(__dirname, '..');
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function shouldSkipHostPreflights(enableOrchestration) {
|
|
413
|
+
return Boolean(enableOrchestration);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function resolveWorktreeImageFromPackageVersion() {
|
|
417
|
+
const pkg = require('../package.json');
|
|
418
|
+
const version = typeof pkg?.version === 'string' ? pkg.version.trim() : '';
|
|
419
|
+
if (!version) {
|
|
420
|
+
throw new Error('Unable to resolve CLI package version for worktree image provisioning.');
|
|
421
|
+
}
|
|
422
|
+
return `${ORCHESTRATOR_WORKTREE_IMAGE_REPO}:${version}`;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function hasWorktreeImageLocally(imageRef, execSyncFn = execSync) {
|
|
426
|
+
try {
|
|
427
|
+
execSyncFn(`docker image inspect ${imageRef}`, {
|
|
428
|
+
stdio: 'pipe',
|
|
429
|
+
timeout: 15000,
|
|
430
|
+
});
|
|
431
|
+
return true;
|
|
432
|
+
} catch (_) {
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function buildWorktreeImage({
|
|
438
|
+
imageRef = resolveWorktreeImageFromPackageVersion(),
|
|
439
|
+
execSyncFn = execSync,
|
|
440
|
+
} = {}) {
|
|
441
|
+
const repoRoot = resolveRepoRootForDockerBuild(execSyncFn);
|
|
442
|
+
const dockerfilePath = join(repoRoot, 'apps', 'local-app', 'Dockerfile');
|
|
443
|
+
console.log(`Building worktree image: ${imageRef}`);
|
|
444
|
+
try {
|
|
445
|
+
execSyncFn(
|
|
446
|
+
`docker build -f "${dockerfilePath}" -t ${imageRef} "${repoRoot}"`,
|
|
447
|
+
{
|
|
448
|
+
stdio: 'inherit',
|
|
449
|
+
timeout: WORKTREE_IMAGE_BUILD_TIMEOUT_MS,
|
|
450
|
+
},
|
|
451
|
+
);
|
|
452
|
+
console.log(`Built worktree image: ${imageRef}`);
|
|
453
|
+
return imageRef;
|
|
454
|
+
} catch (error) {
|
|
455
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
456
|
+
throw new Error(
|
|
457
|
+
[
|
|
458
|
+
`Failed to build worktree image: ${imageRef}`,
|
|
459
|
+
`Reason: ${message}`,
|
|
460
|
+
`Try manually: docker build -f "${dockerfilePath}" -t ${imageRef} "${repoRoot}"`,
|
|
461
|
+
].join('\n'),
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function resolveDevchainApiBaseUrlForRestart({
|
|
467
|
+
readPidFileFn = readPidFile,
|
|
468
|
+
isProcessRunningFn = isProcessRunning,
|
|
469
|
+
} = {}) {
|
|
470
|
+
const pidData = readPidFileFn();
|
|
471
|
+
if (!pidData || !Number.isFinite(Number(pidData.pid)) || !Number.isFinite(Number(pidData.port))) {
|
|
472
|
+
throw new Error(
|
|
473
|
+
'Image was rebuilt, but Devchain is not running. Start container mode first, then retry with --restart.',
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const pid = Number(pidData.pid);
|
|
478
|
+
const port = Number(pidData.port);
|
|
479
|
+
if (!isProcessRunningFn(pid)) {
|
|
480
|
+
throw new Error(
|
|
481
|
+
'Image was rebuilt, but Devchain is not running. Start container mode first, then retry with --restart.',
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return `http://127.0.0.1:${port}`;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function normalizeWorktreeListPayload(payload) {
|
|
489
|
+
if (Array.isArray(payload)) {
|
|
490
|
+
return payload;
|
|
491
|
+
}
|
|
492
|
+
if (payload && typeof payload === 'object' && Array.isArray(payload.items)) {
|
|
493
|
+
return payload.items;
|
|
494
|
+
}
|
|
495
|
+
throw new Error('Unexpected response shape from /api/worktrees.');
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async function restartRunningWorktrees({
|
|
499
|
+
baseUrl,
|
|
500
|
+
fetchFn = fetch,
|
|
501
|
+
} = {}) {
|
|
502
|
+
if (!baseUrl || typeof baseUrl !== 'string') {
|
|
503
|
+
throw new Error('A baseUrl is required to restart running worktrees.');
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const listRes = await fetchFn(`${baseUrl}/api/worktrees`);
|
|
507
|
+
if (!listRes.ok) {
|
|
508
|
+
throw new Error(`Failed to list worktrees for restart (HTTP ${listRes.status}).`);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const worktrees = normalizeWorktreeListPayload(await listRes.json());
|
|
512
|
+
const runningWorktrees = worktrees.filter(
|
|
513
|
+
(worktree) => String(worktree?.status || '').toLowerCase() === 'running',
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
if (runningWorktrees.length === 0) {
|
|
517
|
+
console.log('No running worktrees found. Build completed without restarts.');
|
|
518
|
+
return 0;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
console.log(`Restarting ${runningWorktrees.length} running worktree(s)...`);
|
|
522
|
+
for (const worktree of runningWorktrees) {
|
|
523
|
+
const worktreeId = typeof worktree?.id === 'string' ? worktree.id.trim() : '';
|
|
524
|
+
const worktreeName =
|
|
525
|
+
typeof worktree?.name === 'string' && worktree.name.trim()
|
|
526
|
+
? worktree.name.trim()
|
|
527
|
+
: worktreeId || 'unknown';
|
|
528
|
+
|
|
529
|
+
if (!worktreeId) {
|
|
530
|
+
throw new Error('Cannot restart worktree without an id from /api/worktrees response.');
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
console.log(`Stopping worktree "${worktreeName}"...`);
|
|
534
|
+
const stopRes = await fetchFn(`${baseUrl}/api/worktrees/${encodeURIComponent(worktreeId)}/stop`, {
|
|
535
|
+
method: 'POST',
|
|
536
|
+
});
|
|
537
|
+
if (!stopRes.ok) {
|
|
538
|
+
throw new Error(`Failed stopping worktree "${worktreeName}" (HTTP ${stopRes.status}).`);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
console.log(`Starting worktree "${worktreeName}"...`);
|
|
542
|
+
const startRes = await fetchFn(`${baseUrl}/api/worktrees/${encodeURIComponent(worktreeId)}/start`, {
|
|
543
|
+
method: 'POST',
|
|
544
|
+
});
|
|
545
|
+
if (!startRes.ok) {
|
|
546
|
+
throw new Error(`Failed starting worktree "${worktreeName}" (HTTP ${startRes.status}).`);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
console.log('Worktree restart complete.');
|
|
551
|
+
return runningWorktrees.length;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
async function ensureWorktreeImage({
|
|
555
|
+
execSyncFn = execSync,
|
|
556
|
+
onMissing = 'pull',
|
|
557
|
+
} = {}) {
|
|
558
|
+
const existingImageOverride =
|
|
559
|
+
typeof process.env.ORCHESTRATOR_CONTAINER_IMAGE === 'string'
|
|
560
|
+
? process.env.ORCHESTRATOR_CONTAINER_IMAGE.trim()
|
|
561
|
+
: '';
|
|
562
|
+
if (existingImageOverride) {
|
|
563
|
+
process.env.ORCHESTRATOR_CONTAINER_IMAGE = existingImageOverride;
|
|
564
|
+
console.log(`Using worktree image override: ${existingImageOverride}`);
|
|
565
|
+
return existingImageOverride;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const imageRef = resolveWorktreeImageFromPackageVersion();
|
|
569
|
+
|
|
570
|
+
if (hasWorktreeImageLocally(imageRef, execSyncFn)) {
|
|
571
|
+
console.log(`Using local worktree image: ${imageRef}`);
|
|
572
|
+
} else if (onMissing === 'build') {
|
|
573
|
+
console.log(`Local worktree image missing: ${imageRef}`);
|
|
574
|
+
buildWorktreeImage({ imageRef, execSyncFn });
|
|
575
|
+
} else if (onMissing === 'pull') {
|
|
576
|
+
console.log(`Pulling worktree image: ${imageRef}`);
|
|
577
|
+
try {
|
|
578
|
+
execSyncFn(`docker pull ${imageRef}`, {
|
|
579
|
+
stdio: 'inherit',
|
|
580
|
+
timeout: 300000,
|
|
581
|
+
});
|
|
582
|
+
console.log(`Pulled worktree image: ${imageRef}`);
|
|
583
|
+
} catch (error) {
|
|
584
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
585
|
+
throw new Error(
|
|
586
|
+
[
|
|
587
|
+
`Failed to pull worktree image: ${imageRef}`,
|
|
588
|
+
`Reason: ${message}`,
|
|
589
|
+
`Try manually: docker pull ${imageRef}`,
|
|
590
|
+
`Or build locally: docker build -t ${imageRef} -f apps/local-app/Dockerfile .`,
|
|
591
|
+
].join('\n'),
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
} else {
|
|
595
|
+
throw new Error(`Invalid ensureWorktreeImage onMissing strategy: ${onMissing}`);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
process.env.ORCHESTRATOR_CONTAINER_IMAGE = imageRef;
|
|
599
|
+
return imageRef;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function ensureWorktreeImageRefFromPackageVersion() {
|
|
603
|
+
const existingImageOverride =
|
|
604
|
+
typeof process.env.ORCHESTRATOR_CONTAINER_IMAGE === 'string'
|
|
605
|
+
? process.env.ORCHESTRATOR_CONTAINER_IMAGE.trim()
|
|
606
|
+
: '';
|
|
607
|
+
if (existingImageOverride) {
|
|
608
|
+
process.env.ORCHESTRATOR_CONTAINER_IMAGE = existingImageOverride;
|
|
609
|
+
return existingImageOverride;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const imageRef = resolveWorktreeImageFromPackageVersion();
|
|
613
|
+
process.env.ORCHESTRATOR_CONTAINER_IMAGE = imageRef;
|
|
614
|
+
return imageRef;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
async function bootstrapContainerMode({
|
|
618
|
+
execSyncFn = execSync,
|
|
619
|
+
} = {}) {
|
|
620
|
+
ensureDockerAvailable(execSyncFn);
|
|
621
|
+
const repoRoot = deriveRepoRootFromGit(execSyncFn);
|
|
622
|
+
ensureProjectGitignoreIncludesDevchain(repoRoot);
|
|
623
|
+
ensureWorktreeImageRefFromPackageVersion();
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function formatOrchestrationDetectionFailureReason({
|
|
627
|
+
skippedByEnvNormal = false,
|
|
628
|
+
dockerAvailable = false,
|
|
629
|
+
insideGitRepo = false,
|
|
630
|
+
} = {}) {
|
|
631
|
+
if (skippedByEnvNormal) {
|
|
632
|
+
return 'DEVCHAIN_MODE=normal override is active';
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const missing = [];
|
|
636
|
+
if (!dockerAvailable) {
|
|
637
|
+
missing.push('Docker is unavailable');
|
|
638
|
+
}
|
|
639
|
+
if (!insideGitRepo) {
|
|
640
|
+
missing.push('current directory is not inside a git repository');
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (missing.length === 0) {
|
|
644
|
+
return 'orchestration prerequisites are not met';
|
|
645
|
+
}
|
|
646
|
+
if (missing.length === 1) {
|
|
647
|
+
return missing[0];
|
|
648
|
+
}
|
|
649
|
+
return `${missing.slice(0, -1).join(', ')} and ${missing[missing.length - 1]}`;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
async function resolveStartupOrchestration({
|
|
653
|
+
forceContainer = false,
|
|
654
|
+
env = process.env,
|
|
655
|
+
execSyncFn = execSync,
|
|
656
|
+
bootstrapContainerModeFn = bootstrapContainerMode,
|
|
657
|
+
warnFn = (message) => console.warn(message),
|
|
658
|
+
} = {}) {
|
|
659
|
+
const modeOverride = typeof env.DEVCHAIN_MODE === 'string' ? env.DEVCHAIN_MODE.trim() : '';
|
|
660
|
+
const skippedByEnvNormal = modeOverride.toLowerCase() === 'normal';
|
|
661
|
+
if (skippedByEnvNormal) {
|
|
662
|
+
if (forceContainer) {
|
|
663
|
+
throw new Error(
|
|
664
|
+
`--container requires orchestration, but ${formatOrchestrationDetectionFailureReason({ skippedByEnvNormal })}.`,
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
return {
|
|
668
|
+
enableOrchestration: false,
|
|
669
|
+
skippedByEnvNormal,
|
|
670
|
+
dockerAvailable: false,
|
|
671
|
+
insideGitRepo: false,
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const dockerAvailable = isDockerAvailable(execSyncFn);
|
|
676
|
+
const insideGitRepo = isInsideGitRepo(execSyncFn);
|
|
677
|
+
|
|
678
|
+
if (!dockerAvailable || !insideGitRepo) {
|
|
679
|
+
const reason = formatOrchestrationDetectionFailureReason({
|
|
680
|
+
dockerAvailable,
|
|
681
|
+
insideGitRepo,
|
|
682
|
+
});
|
|
683
|
+
if (forceContainer) {
|
|
684
|
+
throw new Error(`--container requires orchestration, but ${reason}.`);
|
|
685
|
+
}
|
|
686
|
+
return {
|
|
687
|
+
enableOrchestration: false,
|
|
688
|
+
skippedByEnvNormal: false,
|
|
689
|
+
dockerAvailable,
|
|
690
|
+
insideGitRepo,
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
try {
|
|
695
|
+
await bootstrapContainerModeFn({
|
|
696
|
+
execSyncFn,
|
|
697
|
+
});
|
|
698
|
+
env.DEVCHAIN_MODE = 'main';
|
|
699
|
+
return {
|
|
700
|
+
enableOrchestration: true,
|
|
701
|
+
skippedByEnvNormal: false,
|
|
702
|
+
dockerAvailable: true,
|
|
703
|
+
insideGitRepo: true,
|
|
704
|
+
};
|
|
705
|
+
} catch (error) {
|
|
706
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
707
|
+
if (forceContainer) {
|
|
708
|
+
throw new Error(`--container failed to initialize orchestration: ${message}`);
|
|
709
|
+
}
|
|
710
|
+
warnFn(`Container auto-detection found prerequisites but bootstrap failed: ${message}`);
|
|
711
|
+
return {
|
|
712
|
+
enableOrchestration: false,
|
|
713
|
+
skippedByEnvNormal: false,
|
|
714
|
+
dockerAvailable: true,
|
|
715
|
+
insideGitRepo: true,
|
|
716
|
+
bootstrapError: message,
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
184
721
|
async function ensureProvidersInDb(baseUrl, detected, log) {
|
|
185
722
|
try {
|
|
186
723
|
const res = await fetch(`${baseUrl}/api/providers`);
|
|
@@ -542,13 +1079,215 @@ function getTmuxErrorMessage(osType) {
|
|
|
542
1079
|
}
|
|
543
1080
|
}
|
|
544
1081
|
|
|
1082
|
+
async function runHostPreflightChecks(
|
|
1083
|
+
{
|
|
1084
|
+
enableOrchestration,
|
|
1085
|
+
opts,
|
|
1086
|
+
cli,
|
|
1087
|
+
log,
|
|
1088
|
+
isDetachedChild,
|
|
1089
|
+
},
|
|
1090
|
+
{
|
|
1091
|
+
execSyncFn = execSync,
|
|
1092
|
+
isTmuxInstalledFn = isTmuxInstalled,
|
|
1093
|
+
getOSTypeFn = getOSType,
|
|
1094
|
+
detectInstalledProvidersFn = detectInstalledProviders,
|
|
1095
|
+
ensureClaudeBypassPermissionsFn = ensureClaudeBypassPermissions,
|
|
1096
|
+
platformFn = platform,
|
|
1097
|
+
} = {},
|
|
1098
|
+
) {
|
|
1099
|
+
const skipHostPreflights = shouldSkipHostPreflights(enableOrchestration);
|
|
1100
|
+
|
|
1101
|
+
// Tmux preflight check
|
|
1102
|
+
const skipTmuxCheck = process.env.DEVCHAIN_SKIP_TMUX_CHECK === '1';
|
|
1103
|
+
if (skipHostPreflights) {
|
|
1104
|
+
if (opts.foreground) {
|
|
1105
|
+
log('info', 'Skipping tmux check in container mode', { skipReason: 'container_mode' });
|
|
1106
|
+
} else {
|
|
1107
|
+
cli.info('Skipping tmux check in container mode');
|
|
1108
|
+
}
|
|
1109
|
+
} else if (skipTmuxCheck) {
|
|
1110
|
+
if (opts.foreground) {
|
|
1111
|
+
log('info', 'Skipping tmux check (DEVCHAIN_SKIP_TMUX_CHECK=1)', { skipReason: 'env_var' });
|
|
1112
|
+
} else {
|
|
1113
|
+
cli.info('Skipping tmux check (DEVCHAIN_SKIP_TMUX_CHECK=1)');
|
|
1114
|
+
}
|
|
1115
|
+
} else {
|
|
1116
|
+
const osType = getOSTypeFn();
|
|
1117
|
+
|
|
1118
|
+
if (osType === 'windows') {
|
|
1119
|
+
if (opts.foreground) {
|
|
1120
|
+
log('info', 'Skipping tmux check on Windows', { skipReason: 'windows', platform: 'win32' });
|
|
1121
|
+
} else {
|
|
1122
|
+
cli.info('Skipping tmux check on Windows');
|
|
1123
|
+
}
|
|
1124
|
+
} else {
|
|
1125
|
+
if (!opts.foreground) {
|
|
1126
|
+
cli.step('Checking tmux');
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
if (!isTmuxInstalledFn()) {
|
|
1130
|
+
if (opts.foreground) {
|
|
1131
|
+
log('error', 'tmux not found; aborting startup', { platform: osType, checked: 'which tmux' });
|
|
1132
|
+
} else {
|
|
1133
|
+
cli.stepDone('✗ not found');
|
|
1134
|
+
cli.blank();
|
|
1135
|
+
}
|
|
1136
|
+
console.error('\n' + getTmuxErrorMessage(osType));
|
|
1137
|
+
process.exit(1);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
try {
|
|
1141
|
+
const tmuxPath = execSyncFn('which tmux', { encoding: 'utf8' }).trim();
|
|
1142
|
+
if (opts.foreground) {
|
|
1143
|
+
log('info', 'tmux found', { tmuxPath });
|
|
1144
|
+
} else {
|
|
1145
|
+
cli.stepDone('✓ found');
|
|
1146
|
+
}
|
|
1147
|
+
} catch {
|
|
1148
|
+
if (opts.foreground) {
|
|
1149
|
+
log('info', 'tmux check passed');
|
|
1150
|
+
} else {
|
|
1151
|
+
cli.stepDone('✓');
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// Provider detection (Linux/macOS only)
|
|
1158
|
+
const skipProviderCheck = process.env.DEVCHAIN_SKIP_PROVIDER_CHECK === '1';
|
|
1159
|
+
const plat = platformFn();
|
|
1160
|
+
if (skipHostPreflights) {
|
|
1161
|
+
if (opts.foreground) {
|
|
1162
|
+
log('info', 'Skipping provider check in container mode', { skipReason: 'container_mode' });
|
|
1163
|
+
} else {
|
|
1164
|
+
cli.info('Skipping provider check in container mode');
|
|
1165
|
+
}
|
|
1166
|
+
} else if (skipProviderCheck) {
|
|
1167
|
+
if (opts.foreground) {
|
|
1168
|
+
log('info', 'Skipping provider check (DEVCHAIN_SKIP_PROVIDER_CHECK=1)', {
|
|
1169
|
+
skipReason: 'env_var',
|
|
1170
|
+
});
|
|
1171
|
+
} else {
|
|
1172
|
+
cli.info('Skipping provider check (DEVCHAIN_SKIP_PROVIDER_CHECK=1)');
|
|
1173
|
+
}
|
|
1174
|
+
} else if (plat === 'win32') {
|
|
1175
|
+
if (opts.foreground) {
|
|
1176
|
+
log('info', 'Skipping provider check on Windows', { skipReason: 'windows' });
|
|
1177
|
+
} else {
|
|
1178
|
+
cli.info('Skipping provider check on Windows');
|
|
1179
|
+
}
|
|
1180
|
+
} else {
|
|
1181
|
+
if (!opts.foreground) {
|
|
1182
|
+
cli.step('Detecting providers');
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
const providersDetected = detectInstalledProvidersFn();
|
|
1186
|
+
if (providersDetected.size === 0) {
|
|
1187
|
+
const guide = [
|
|
1188
|
+
'No provider binaries detected on PATH. Install at least one provider and retry.',
|
|
1189
|
+
'Checked: "which codex", "which claude", and "which gemini"',
|
|
1190
|
+
'Examples:',
|
|
1191
|
+
' - Install Codex: npm i -g @openai/codex (example) or follow provider docs',
|
|
1192
|
+
' - Install Claude: npm i -g @anthropic-ai/claude-code (example) or follow provider docs',
|
|
1193
|
+
' - Install Gemini: npm i -g @google/gemini-cli (example) or follow provider docs',
|
|
1194
|
+
'Advanced: bypass with DEVCHAIN_SKIP_PROVIDER_CHECK=1',
|
|
1195
|
+
].join('\n');
|
|
1196
|
+
if (opts.foreground) {
|
|
1197
|
+
log('error', 'No providers found; aborting startup', {
|
|
1198
|
+
checked: ['which codex', 'which claude', 'which gemini'],
|
|
1199
|
+
});
|
|
1200
|
+
} else {
|
|
1201
|
+
cli.stepDone('✗ none found');
|
|
1202
|
+
cli.blank();
|
|
1203
|
+
}
|
|
1204
|
+
console.error('\n' + guide + '\n');
|
|
1205
|
+
process.exit(1);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
opts.__providersDetected = providersDetected;
|
|
1209
|
+
const providerNames = Array.from(providersDetected.keys());
|
|
1210
|
+
|
|
1211
|
+
if (opts.foreground) {
|
|
1212
|
+
log('info', 'Detected providers', {
|
|
1213
|
+
providers: Array.from(providersDetected.entries()).map(([name, p]) => ({ name, path: p })),
|
|
1214
|
+
});
|
|
1215
|
+
} else {
|
|
1216
|
+
cli.stepDone(`✓ ${providerNames.join(', ')}`);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// Prompt for Claude bypass permissions (parent only - requires stdin)
|
|
1221
|
+
// This runs BEFORE detach since it needs user interaction
|
|
1222
|
+
if (!isDetachedChild && opts.__providersDetected && opts.__providersDetected.has('claude')) {
|
|
1223
|
+
await ensureClaudeBypassPermissionsFn(cli);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
function getDevUiConfig(containerMode) {
|
|
1228
|
+
if (containerMode) {
|
|
1229
|
+
return {
|
|
1230
|
+
script: 'dev:ui',
|
|
1231
|
+
startMessage: 'Starting UI (dev mode)...',
|
|
1232
|
+
logLabel: 'UI dev server',
|
|
1233
|
+
url: 'http://127.0.0.1:5175',
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
return {
|
|
1238
|
+
script: 'dev:ui',
|
|
1239
|
+
startMessage: 'Starting UI (dev mode)...',
|
|
1240
|
+
logLabel: 'UI dev server',
|
|
1241
|
+
url: 'http://127.0.0.1:5175',
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
function applyContainerModeDefaults(containerMode, opts = {}, env = process.env) {
|
|
1246
|
+
if (!containerMode) {
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
const hasExplicitPortEnv = typeof env.PORT === 'string' && env.PORT.trim() !== '';
|
|
1251
|
+
if (!opts.port && !hasExplicitPortEnv) {
|
|
1252
|
+
env.PORT = '3000';
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
function getPreferredDevApiPort(optsPort, containerMode, env = process.env) {
|
|
1257
|
+
if (optsPort) {
|
|
1258
|
+
return Number(optsPort);
|
|
1259
|
+
}
|
|
1260
|
+
if (containerMode) {
|
|
1261
|
+
return Number(env.PORT || 3000);
|
|
1262
|
+
}
|
|
1263
|
+
return 3000;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
function getDevModeSpawnConfig({ containerMode, port, env = process.env }) {
|
|
1267
|
+
const ui = getDevUiConfig(containerMode);
|
|
1268
|
+
return {
|
|
1269
|
+
ui,
|
|
1270
|
+
nest: {
|
|
1271
|
+
command: 'pnpm',
|
|
1272
|
+
args: ['--filter', 'local-app', 'dev:api'],
|
|
1273
|
+
env: { ...env, PORT: String(port) },
|
|
1274
|
+
},
|
|
1275
|
+
vite: {
|
|
1276
|
+
command: 'pnpm',
|
|
1277
|
+
args: ['--filter', 'local-app', ui.script],
|
|
1278
|
+
env: { ...env, VITE_API_PORT: String(port) },
|
|
1279
|
+
},
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
|
|
545
1283
|
async function main(argv) {
|
|
546
1284
|
const program = new Command();
|
|
547
1285
|
const pkg = require('../package.json');
|
|
548
1286
|
program
|
|
549
1287
|
.name('devchain')
|
|
550
1288
|
.description('Devchain — Local-first AI agent orchestration')
|
|
551
|
-
.version(pkg.version)
|
|
1289
|
+
.version(pkg.version)
|
|
1290
|
+
.option('--container', 'Shorthand for "start --container"');
|
|
552
1291
|
|
|
553
1292
|
const startCommand = program
|
|
554
1293
|
.command('start [args...]')
|
|
@@ -559,6 +1298,14 @@ async function main(argv) {
|
|
|
559
1298
|
.option('--no-open', 'Do not open a browser window')
|
|
560
1299
|
.option('--db <path>', 'Path to database directory or file (overrides DB_PATH/DB_FILENAME)')
|
|
561
1300
|
.option('--project <path>', 'Initial project root path; creates project if missing')
|
|
1301
|
+
.option(
|
|
1302
|
+
'--container',
|
|
1303
|
+
'Force orchestration startup (errors if Docker or git repository prerequisites are missing)'
|
|
1304
|
+
)
|
|
1305
|
+
.option(
|
|
1306
|
+
'--worktree-runtime <type>',
|
|
1307
|
+
'[internal] Worktree runtime context (container|process); bypasses singleton and interactive startup flows',
|
|
1308
|
+
)
|
|
562
1309
|
.option(
|
|
563
1310
|
'--log-level <level>',
|
|
564
1311
|
'Set log verbosity: error (errors only), warn, info, debug, or trace. ' +
|
|
@@ -567,7 +1314,24 @@ async function main(argv) {
|
|
|
567
1314
|
)
|
|
568
1315
|
.option('--dev', 'Development mode with hot reload (spawns nest --watch + vite)')
|
|
569
1316
|
.option('--internal-detached-child', '[internal] Marker for detached child process')
|
|
570
|
-
.action(async (
|
|
1317
|
+
.action(async (rawArgs, opts) => {
|
|
1318
|
+
const args = Array.isArray(rawArgs) ? [...rawArgs] : [];
|
|
1319
|
+
const usesContainerSubcommand = args[0] === 'container';
|
|
1320
|
+
if (usesContainerSubcommand) {
|
|
1321
|
+
args.shift();
|
|
1322
|
+
}
|
|
1323
|
+
const forceContainer = Boolean(
|
|
1324
|
+
opts.container || program.opts().container || usesContainerSubcommand,
|
|
1325
|
+
);
|
|
1326
|
+
let worktreeRuntimeType = null;
|
|
1327
|
+
try {
|
|
1328
|
+
worktreeRuntimeType = normalizeWorktreeRuntimeType(opts.worktreeRuntime);
|
|
1329
|
+
} catch (error) {
|
|
1330
|
+
console.error(error instanceof Error ? error.message : 'Invalid --worktree-runtime value.');
|
|
1331
|
+
process.exit(1);
|
|
1332
|
+
}
|
|
1333
|
+
const worktreeRuntimeMode = isWorktreeRuntimeModeEnabled(worktreeRuntimeType);
|
|
1334
|
+
|
|
571
1335
|
// Check if "help" was passed as an argument
|
|
572
1336
|
if (args && args.length > 0 && args[0] === 'help') {
|
|
573
1337
|
startCommand.help();
|
|
@@ -575,7 +1339,7 @@ async function main(argv) {
|
|
|
575
1339
|
}
|
|
576
1340
|
|
|
577
1341
|
// Check if already running (skip for detached child - parent already checked)
|
|
578
|
-
if (!opts.internalDetachedChild) {
|
|
1342
|
+
if (!opts.internalDetachedChild && !worktreeRuntimeMode) {
|
|
579
1343
|
const existingPid = readPidFile();
|
|
580
1344
|
if (existingPid && isProcessRunning(existingPid.pid)) {
|
|
581
1345
|
console.error(`Devchain is already running (PID ${existingPid.pid}, port ${existingPid.port})`);
|
|
@@ -591,16 +1355,19 @@ async function main(argv) {
|
|
|
591
1355
|
|
|
592
1356
|
// Normalize defaults for negatable options (Commander may leave undefined)
|
|
593
1357
|
if (typeof opts.open === 'undefined') {
|
|
594
|
-
opts.open = true;
|
|
1358
|
+
opts.open = worktreeRuntimeMode ? false : forceContainer ? false : true;
|
|
1359
|
+
}
|
|
1360
|
+
if (worktreeRuntimeMode) {
|
|
1361
|
+
opts.open = false;
|
|
595
1362
|
}
|
|
596
1363
|
// Detached mode by default, unless foreground is explicitly requested
|
|
597
1364
|
// Don't detach again if we're already the detached child process
|
|
598
|
-
const isDetachedChild = opts.internalDetachedChild;
|
|
599
|
-
const shouldDetach = opts.foreground !== true && !isDetachedChild;
|
|
1365
|
+
const isDetachedChild = Boolean(opts.internalDetachedChild);
|
|
1366
|
+
const shouldDetach = opts.foreground !== true && !isDetachedChild && !worktreeRuntimeMode;
|
|
600
1367
|
|
|
601
1368
|
// Initialize interactive CLI (user-friendly output unless in foreground mode)
|
|
602
1369
|
const cli = new InteractiveCLI({
|
|
603
|
-
interactive: !opts.foreground && !isDetachedChild,
|
|
1370
|
+
interactive: !opts.foreground && !isDetachedChild && !worktreeRuntimeMode,
|
|
604
1371
|
colors: true,
|
|
605
1372
|
spinners: true
|
|
606
1373
|
});
|
|
@@ -610,141 +1377,70 @@ async function main(argv) {
|
|
|
610
1377
|
console.log(JSON.stringify(entry));
|
|
611
1378
|
};
|
|
612
1379
|
|
|
1380
|
+
let enableOrchestration = false;
|
|
1381
|
+
if (worktreeRuntimeMode) {
|
|
1382
|
+
enableOrchestration = worktreeRuntimeType === 'container';
|
|
1383
|
+
} else {
|
|
1384
|
+
try {
|
|
1385
|
+
const orchestrationResolution = await resolveStartupOrchestration({
|
|
1386
|
+
forceContainer,
|
|
1387
|
+
env: process.env,
|
|
1388
|
+
warnFn: (message) => {
|
|
1389
|
+
if (opts.foreground) {
|
|
1390
|
+
log('warn', message);
|
|
1391
|
+
} else {
|
|
1392
|
+
cli.warn(message);
|
|
1393
|
+
}
|
|
1394
|
+
},
|
|
1395
|
+
});
|
|
1396
|
+
enableOrchestration = orchestrationResolution.enableOrchestration;
|
|
1397
|
+
} catch (error) {
|
|
1398
|
+
console.error(
|
|
1399
|
+
error instanceof Error
|
|
1400
|
+
? error.message
|
|
1401
|
+
: 'Failed to resolve orchestration startup prerequisites.',
|
|
1402
|
+
);
|
|
1403
|
+
process.exit(1);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
applyContainerModeDefaults(enableOrchestration, opts, process.env);
|
|
1408
|
+
|
|
613
1409
|
// Check for updates (parent process only, skip in dev mode)
|
|
614
|
-
if (!isDetachedChild && !opts.dev) {
|
|
1410
|
+
if (!isDetachedChild && !opts.dev && !worktreeRuntimeMode) {
|
|
615
1411
|
await checkForUpdates(cli, askYesNo);
|
|
616
1412
|
}
|
|
617
1413
|
|
|
618
1414
|
// Show startup banner (interactive mode only)
|
|
619
|
-
if (!opts.foreground) {
|
|
1415
|
+
if (!opts.foreground && !worktreeRuntimeMode) {
|
|
620
1416
|
cli.blank();
|
|
621
1417
|
cli.info('Starting Devchain...');
|
|
622
1418
|
cli.blank();
|
|
623
1419
|
}
|
|
624
1420
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
}
|
|
633
|
-
} else {
|
|
634
|
-
const osType = getOSType();
|
|
635
|
-
|
|
636
|
-
if (osType === 'windows') {
|
|
637
|
-
if (opts.foreground) {
|
|
638
|
-
log('info', 'Skipping tmux check on Windows', { skipReason: 'windows', platform: 'win32' });
|
|
639
|
-
} else {
|
|
640
|
-
cli.info('Skipping tmux check on Windows');
|
|
641
|
-
}
|
|
642
|
-
} else {
|
|
643
|
-
// Interactive: show step
|
|
644
|
-
if (!opts.foreground) {
|
|
645
|
-
cli.step('Checking tmux');
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
// Check if tmux is installed
|
|
649
|
-
if (!isTmuxInstalled()) {
|
|
650
|
-
if (opts.foreground) {
|
|
651
|
-
log('error', 'tmux not found; aborting startup', { platform: osType, checked: 'which tmux' });
|
|
652
|
-
} else {
|
|
653
|
-
cli.stepDone('✗ not found');
|
|
654
|
-
cli.blank();
|
|
655
|
-
}
|
|
656
|
-
console.error('\n' + getTmuxErrorMessage(osType));
|
|
657
|
-
process.exit(1);
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
// tmux found, log success
|
|
661
|
-
try {
|
|
662
|
-
const tmuxPath = execSync('which tmux', { encoding: 'utf8' }).trim();
|
|
663
|
-
if (opts.foreground) {
|
|
664
|
-
log('info', 'tmux found', { tmuxPath });
|
|
665
|
-
} else {
|
|
666
|
-
cli.stepDone('✓ found');
|
|
667
|
-
}
|
|
668
|
-
} catch {
|
|
669
|
-
// Shouldn't happen since we just checked, but handle gracefully
|
|
670
|
-
if (opts.foreground) {
|
|
671
|
-
log('info', 'tmux check passed');
|
|
672
|
-
} else {
|
|
673
|
-
cli.stepDone('✓');
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
}
|
|
1421
|
+
if (!worktreeRuntimeMode) {
|
|
1422
|
+
await runHostPreflightChecks({
|
|
1423
|
+
enableOrchestration,
|
|
1424
|
+
opts,
|
|
1425
|
+
cli,
|
|
1426
|
+
log,
|
|
1427
|
+
isDetachedChild,
|
|
1428
|
+
});
|
|
677
1429
|
}
|
|
678
1430
|
|
|
679
|
-
|
|
680
|
-
const skipProviderCheck = process.env.DEVCHAIN_SKIP_PROVIDER_CHECK === '1';
|
|
681
|
-
const plat = platform();
|
|
682
|
-
if (skipProviderCheck) {
|
|
683
|
-
if (opts.foreground) {
|
|
684
|
-
log('info', 'Skipping provider check (DEVCHAIN_SKIP_PROVIDER_CHECK=1)', {
|
|
685
|
-
skipReason: 'env_var',
|
|
686
|
-
});
|
|
687
|
-
} else {
|
|
688
|
-
cli.info('Skipping provider check (DEVCHAIN_SKIP_PROVIDER_CHECK=1)');
|
|
689
|
-
}
|
|
690
|
-
} else if (plat === 'win32') {
|
|
691
|
-
if (opts.foreground) {
|
|
692
|
-
log('info', 'Skipping provider check on Windows', { skipReason: 'windows' });
|
|
693
|
-
} else {
|
|
694
|
-
cli.info('Skipping provider check on Windows');
|
|
695
|
-
}
|
|
696
|
-
} else {
|
|
697
|
-
// Interactive: show step
|
|
698
|
-
if (!opts.foreground) {
|
|
699
|
-
cli.step('Detecting providers');
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
const providersDetected = detectInstalledProviders();
|
|
703
|
-
if (providersDetected.size === 0) {
|
|
704
|
-
// Provide guidance and exit prior to server startup
|
|
705
|
-
const guide = [
|
|
706
|
-
'No provider binaries detected on PATH. Install at least one provider and retry.',
|
|
707
|
-
'Checked: "which codex", "which claude", and "which gemini"',
|
|
708
|
-
'Examples:',
|
|
709
|
-
' - Install Codex: npm i -g @openai/codex (example) or follow provider docs',
|
|
710
|
-
' - Install Claude: npm i -g @anthropic-ai/claude-code (example) or follow provider docs',
|
|
711
|
-
' - Install Gemini: npm i -g @google/gemini-cli (example) or follow provider docs',
|
|
712
|
-
'Advanced: bypass with DEVCHAIN_SKIP_PROVIDER_CHECK=1',
|
|
713
|
-
].join('\n');
|
|
714
|
-
if (opts.foreground) {
|
|
715
|
-
log('error', 'No providers found; aborting startup', {
|
|
716
|
-
checked: ['which codex', 'which claude', 'which gemini'],
|
|
717
|
-
});
|
|
718
|
-
} else {
|
|
719
|
-
cli.stepDone('✗ none found');
|
|
720
|
-
cli.blank();
|
|
721
|
-
}
|
|
722
|
-
console.error('\n' + guide + '\n');
|
|
723
|
-
process.exit(1);
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
// stash on opts for later ensure step after server ready
|
|
727
|
-
opts.__providersDetected = providersDetected;
|
|
728
|
-
const providerNames = Array.from(providersDetected.keys());
|
|
729
|
-
|
|
730
|
-
if (opts.foreground) {
|
|
731
|
-
log('info', 'Detected providers', {
|
|
732
|
-
providers: Array.from(providersDetected.entries()).map(([name, p]) => ({ name, path: p })),
|
|
733
|
-
});
|
|
734
|
-
} else {
|
|
735
|
-
cli.stepDone(`✓ ${providerNames.join(', ')}`);
|
|
736
|
-
}
|
|
737
|
-
}
|
|
1431
|
+
const preferPort = getPreferredDevApiPort(opts.port, enableOrchestration, process.env);
|
|
738
1432
|
|
|
739
|
-
//
|
|
740
|
-
//
|
|
741
|
-
|
|
742
|
-
|
|
1433
|
+
// In worktree runtime mode, bind strictly to the requested port.
|
|
1434
|
+
// getPort() silently picks a different port when the requested one is unavailable,
|
|
1435
|
+
// which causes the parent orchestrator to talk to the wrong instance.
|
|
1436
|
+
// Let NestJS fail fast with EADDRINUSE instead of silently rebinding.
|
|
1437
|
+
let port;
|
|
1438
|
+
if (worktreeRuntimeMode && preferPort) {
|
|
1439
|
+
port = preferPort;
|
|
1440
|
+
} else {
|
|
1441
|
+
port = await getPort({ port: preferPort });
|
|
743
1442
|
}
|
|
744
1443
|
|
|
745
|
-
const preferPort = opts.port ? Number(opts.port) : 3000;
|
|
746
|
-
const port = await getPort({ port: preferPort });
|
|
747
|
-
|
|
748
1444
|
// === DETACH POINT ===
|
|
749
1445
|
// All interactive prompts are done. Now spawn the detached child if needed.
|
|
750
1446
|
if (shouldDetach) {
|
|
@@ -831,9 +1527,15 @@ async function main(argv) {
|
|
|
831
1527
|
};
|
|
832
1528
|
|
|
833
1529
|
// Spawn NestJS in watch mode (detached to create process group)
|
|
834
|
-
const
|
|
1530
|
+
const devSpawnConfig = getDevModeSpawnConfig({
|
|
1531
|
+
containerMode: enableOrchestration,
|
|
1532
|
+
port,
|
|
1533
|
+
env: process.env,
|
|
1534
|
+
});
|
|
1535
|
+
|
|
1536
|
+
const nestProcess = spawn(devSpawnConfig.nest.command, devSpawnConfig.nest.args, {
|
|
835
1537
|
stdio: 'inherit',
|
|
836
|
-
env:
|
|
1538
|
+
env: devSpawnConfig.nest.env,
|
|
837
1539
|
shell: true,
|
|
838
1540
|
detached: platform() !== 'win32', // Create process group on Unix
|
|
839
1541
|
});
|
|
@@ -853,7 +1555,12 @@ async function main(argv) {
|
|
|
853
1555
|
cli.info(`API docs: ${baseUrl}/api/docs`);
|
|
854
1556
|
|
|
855
1557
|
// Ensure provider rows exist
|
|
856
|
-
if (
|
|
1558
|
+
if (
|
|
1559
|
+
!worktreeRuntimeMode
|
|
1560
|
+
&& !enableOrchestration
|
|
1561
|
+
&& opts.__providersDetected
|
|
1562
|
+
&& opts.__providersDetected.size > 0
|
|
1563
|
+
) {
|
|
857
1564
|
await ensureProvidersInDb(baseUrl, opts.__providersDetected, log);
|
|
858
1565
|
}
|
|
859
1566
|
|
|
@@ -863,36 +1570,44 @@ async function main(argv) {
|
|
|
863
1570
|
: process.cwd();
|
|
864
1571
|
|
|
865
1572
|
// Validate MCP for all providers
|
|
866
|
-
|
|
1573
|
+
if (!worktreeRuntimeMode && !enableOrchestration) {
|
|
1574
|
+
await validateMcpForProviders(baseUrl, cli, opts, log, startupPath);
|
|
1575
|
+
}
|
|
867
1576
|
|
|
868
1577
|
// Note: Claude bypass prompt already handled before server start
|
|
869
1578
|
|
|
1579
|
+
const devUiConfig = devSpawnConfig.ui;
|
|
1580
|
+
|
|
870
1581
|
cli.blank();
|
|
871
|
-
cli.info(
|
|
1582
|
+
cli.info(devUiConfig.startMessage);
|
|
872
1583
|
|
|
873
1584
|
// Spawn Vite for UI hot reload (pass API port, detached to create process group)
|
|
874
|
-
const viteProcess = spawn(
|
|
1585
|
+
const viteProcess = spawn(devSpawnConfig.vite.command, devSpawnConfig.vite.args, {
|
|
875
1586
|
stdio: 'inherit',
|
|
876
|
-
env:
|
|
1587
|
+
env: devSpawnConfig.vite.env,
|
|
877
1588
|
shell: true,
|
|
878
1589
|
detached: platform() !== 'win32', // Create process group on Unix
|
|
879
1590
|
});
|
|
880
1591
|
|
|
881
1592
|
cli.blank();
|
|
882
1593
|
cli.success('Development servers running');
|
|
883
|
-
cli.info(
|
|
1594
|
+
cli.info(`${devUiConfig.logLabel}: ${devUiConfig.url}`);
|
|
884
1595
|
cli.info(`API: ${baseUrl}`);
|
|
885
1596
|
cli.blank();
|
|
886
1597
|
|
|
887
|
-
// Write PID file
|
|
888
|
-
|
|
1598
|
+
// Write PID file for top-level runtime only
|
|
1599
|
+
if (!worktreeRuntimeMode) {
|
|
1600
|
+
writePidFile(port);
|
|
1601
|
+
}
|
|
889
1602
|
|
|
890
1603
|
// Handle cleanup on exit - kill entire process groups
|
|
891
1604
|
const cleanup = () => {
|
|
892
1605
|
console.log('\nShutting down development servers...');
|
|
893
1606
|
killProcessGroup(nestProcess);
|
|
894
1607
|
killProcessGroup(viteProcess);
|
|
895
|
-
|
|
1608
|
+
if (!worktreeRuntimeMode) {
|
|
1609
|
+
removePidFile();
|
|
1610
|
+
}
|
|
896
1611
|
process.exit(0);
|
|
897
1612
|
};
|
|
898
1613
|
|
|
@@ -960,7 +1675,12 @@ async function main(argv) {
|
|
|
960
1675
|
}
|
|
961
1676
|
|
|
962
1677
|
// Ensure provider rows exist (idempotent) before opening UI
|
|
963
|
-
if (
|
|
1678
|
+
if (
|
|
1679
|
+
!worktreeRuntimeMode
|
|
1680
|
+
&& !enableOrchestration
|
|
1681
|
+
&& opts.__providersDetected
|
|
1682
|
+
&& opts.__providersDetected.size > 0
|
|
1683
|
+
) {
|
|
964
1684
|
await ensureProvidersInDb(baseUrl, opts.__providersDetected, log);
|
|
965
1685
|
}
|
|
966
1686
|
|
|
@@ -970,7 +1690,9 @@ async function main(argv) {
|
|
|
970
1690
|
: process.cwd();
|
|
971
1691
|
|
|
972
1692
|
// Validate MCP for all providers (with project context)
|
|
973
|
-
|
|
1693
|
+
if (!worktreeRuntimeMode && !enableOrchestration) {
|
|
1694
|
+
await validateMcpForProviders(baseUrl, cli, opts, log, startupPath);
|
|
1695
|
+
}
|
|
974
1696
|
|
|
975
1697
|
// Note: Claude bypass prompt already handled before server start (in parent process for detach mode)
|
|
976
1698
|
|
|
@@ -1020,13 +1742,15 @@ async function main(argv) {
|
|
|
1020
1742
|
cli.blank();
|
|
1021
1743
|
}
|
|
1022
1744
|
|
|
1023
|
-
|
|
1024
|
-
|
|
1745
|
+
if (!worktreeRuntimeMode) {
|
|
1746
|
+
// Write PID file for stop command
|
|
1747
|
+
writePidFile(port);
|
|
1025
1748
|
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1749
|
+
// Clean up PID file on exit (main.ts handles SIGINT/SIGTERM and graceful shutdown)
|
|
1750
|
+
process.on('exit', () => {
|
|
1751
|
+
removePidFile();
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1030
1754
|
|
|
1031
1755
|
if (opts.open) {
|
|
1032
1756
|
if (!opts.foreground) {
|
|
@@ -1056,66 +1780,133 @@ async function main(argv) {
|
|
|
1056
1780
|
}
|
|
1057
1781
|
});
|
|
1058
1782
|
|
|
1783
|
+
program
|
|
1784
|
+
.command('dev:image')
|
|
1785
|
+
.description('Rebuild worktree image for Docker-enabled development')
|
|
1786
|
+
.option('--restart', 'After rebuild, restart running worktrees via orchestrator API')
|
|
1787
|
+
.action(async (opts) => {
|
|
1788
|
+
try {
|
|
1789
|
+
ensureDockerAvailable();
|
|
1790
|
+
const imageRef = buildWorktreeImage();
|
|
1791
|
+
process.env.ORCHESTRATOR_CONTAINER_IMAGE = imageRef;
|
|
1792
|
+
|
|
1793
|
+
if (!opts.restart) {
|
|
1794
|
+
console.log('Build complete. Running worktrees were not restarted.');
|
|
1795
|
+
process.exit(0);
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
const baseUrl = resolveDevchainApiBaseUrlForRestart();
|
|
1799
|
+
const ready = await waitForHealth(`${baseUrl}/health`, {
|
|
1800
|
+
timeoutMs: 5000,
|
|
1801
|
+
intervalMs: 250,
|
|
1802
|
+
});
|
|
1803
|
+
if (!ready) {
|
|
1804
|
+
throw new Error(
|
|
1805
|
+
`Image was rebuilt, but orchestrator is not reachable at ${baseUrl}. Start it and retry --restart.`,
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
await restartRunningWorktrees({ baseUrl });
|
|
1810
|
+
process.exit(0);
|
|
1811
|
+
} catch (error) {
|
|
1812
|
+
console.error(
|
|
1813
|
+
error instanceof Error
|
|
1814
|
+
? error.message
|
|
1815
|
+
: 'Failed to rebuild worktree image.',
|
|
1816
|
+
);
|
|
1817
|
+
process.exit(1);
|
|
1818
|
+
}
|
|
1819
|
+
});
|
|
1820
|
+
|
|
1059
1821
|
program
|
|
1060
1822
|
.command('stop')
|
|
1061
1823
|
.description('Stop the running Devchain instance')
|
|
1062
|
-
.action(() => {
|
|
1824
|
+
.action(async () => {
|
|
1063
1825
|
const pidData = readPidFile();
|
|
1064
1826
|
|
|
1065
1827
|
if (!pidData) {
|
|
1066
1828
|
console.log('No running Devchain instance found.');
|
|
1067
1829
|
process.exit(1);
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
const { pid, port } = pidData;
|
|
1830
|
+
} else {
|
|
1831
|
+
const { pid, port } = pidData;
|
|
1071
1832
|
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1833
|
+
if (!isProcessRunning(pid)) {
|
|
1834
|
+
console.log(`Devchain process (PID ${pid}) is not running. Cleaning up stale PID file.`);
|
|
1835
|
+
removePidFile();
|
|
1836
|
+
process.exit(1);
|
|
1837
|
+
} else {
|
|
1838
|
+
console.log(`Stopping Devchain (PID ${pid}, port ${port})...`);
|
|
1077
1839
|
|
|
1078
|
-
|
|
1840
|
+
try {
|
|
1841
|
+
process.kill(pid, 'SIGTERM');
|
|
1842
|
+
} catch (err) {
|
|
1843
|
+
console.error('Failed to stop Devchain:', err.message);
|
|
1844
|
+
process.exit(1);
|
|
1845
|
+
}
|
|
1079
1846
|
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
if (!isProcessRunning(pid)) {
|
|
1088
|
-
clearInterval(checkInterval);
|
|
1089
|
-
removePidFile();
|
|
1090
|
-
console.log('Devchain stopped successfully.');
|
|
1091
|
-
process.exit(0);
|
|
1847
|
+
let stopped = false;
|
|
1848
|
+
for (let attempts = 0; attempts <= 20; attempts += 1) {
|
|
1849
|
+
if (!isProcessRunning(pid)) {
|
|
1850
|
+
stopped = true;
|
|
1851
|
+
break;
|
|
1852
|
+
}
|
|
1853
|
+
await sleep(100);
|
|
1092
1854
|
}
|
|
1093
1855
|
|
|
1094
|
-
if (
|
|
1095
|
-
// After 2 seconds, force kill
|
|
1096
|
-
clearInterval(checkInterval);
|
|
1856
|
+
if (!stopped) {
|
|
1097
1857
|
console.log('Graceful shutdown timed out, forcing...');
|
|
1098
1858
|
try {
|
|
1099
1859
|
process.kill(pid, 'SIGKILL');
|
|
1100
|
-
removePidFile();
|
|
1101
|
-
console.log('Devchain stopped (forced).');
|
|
1102
1860
|
} catch (err) {
|
|
1103
1861
|
console.error('Failed to stop Devchain:', err.message);
|
|
1104
1862
|
process.exit(1);
|
|
1105
1863
|
}
|
|
1106
|
-
process.exit(0);
|
|
1107
1864
|
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1865
|
+
|
|
1866
|
+
removePidFile();
|
|
1867
|
+
console.log(stopped ? 'Devchain stopped successfully.' : 'Devchain stopped (forced).');
|
|
1868
|
+
}
|
|
1112
1869
|
}
|
|
1870
|
+
|
|
1871
|
+
process.exit(0);
|
|
1113
1872
|
});
|
|
1114
1873
|
|
|
1115
|
-
await program.parseAsync(argv);
|
|
1874
|
+
await program.parseAsync(normalizeCliArgv(argv));
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
if (require.main === module) {
|
|
1878
|
+
main(process.argv).catch((err) => {
|
|
1879
|
+
console.error('Fatal error:', err);
|
|
1880
|
+
process.exit(1);
|
|
1881
|
+
});
|
|
1116
1882
|
}
|
|
1117
1883
|
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1884
|
+
module.exports = {
|
|
1885
|
+
main,
|
|
1886
|
+
normalizeCliArgv,
|
|
1887
|
+
__test__: {
|
|
1888
|
+
ensureDockerAvailable,
|
|
1889
|
+
isDockerAvailable,
|
|
1890
|
+
deriveRepoRootFromGit,
|
|
1891
|
+
isInsideGitRepo,
|
|
1892
|
+
ensureProjectGitignoreIncludesDevchain,
|
|
1893
|
+
shouldSkipHostPreflights,
|
|
1894
|
+
runHostPreflightChecks,
|
|
1895
|
+
getDevUiConfig,
|
|
1896
|
+
applyContainerModeDefaults,
|
|
1897
|
+
getPreferredDevApiPort,
|
|
1898
|
+
getDevModeSpawnConfig,
|
|
1899
|
+
hasWorktreeImageLocally,
|
|
1900
|
+
buildWorktreeImage,
|
|
1901
|
+
resolveDevchainApiBaseUrlForRestart,
|
|
1902
|
+
restartRunningWorktrees,
|
|
1903
|
+
ensureWorktreeImage,
|
|
1904
|
+
bootstrapContainerMode,
|
|
1905
|
+
ensureWorktreeImageRefFromPackageVersion,
|
|
1906
|
+
formatOrchestrationDetectionFailureReason,
|
|
1907
|
+
resolveStartupOrchestration,
|
|
1908
|
+
normalizeWorktreeRuntimeType,
|
|
1909
|
+
isWorktreeRuntimeModeEnabled,
|
|
1910
|
+
detectGlobalPackageManager,
|
|
1911
|
+
},
|
|
1912
|
+
};
|