elanous 0.2.8 → 0.2.10
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/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/2DsiDfDCks44g1cPUMhiW/_buildManifest.js +1 -0
- package/apps/pwa/out/_next/static/chunks/24-e6d28380efab7faf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3615-740dd270764ffa24.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3734-1015fe991eb9b7e7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5012-a7fe6f3c8b6fe45a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5783-ef40ebfc91d55230.js +1 -0
- package/apps/pwa/out/_next/static/chunks/62-ff227c3edb8b248b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7633-1b3709b0854c9412.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7951-8b03dbb10b24cd06.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8067-87ce253ec2f23697.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{5080-1613752891fcadb4.js → 8195-6fde3d7a39859206.js} +5 -5
- package/apps/pwa/out/_next/static/chunks/8894-b5fb36bab837cd29.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9454-84d0d5d77446bd92.js +1 -0
- package/apps/pwa/out/_next/static/chunks/988-5f92e7ab7ccea3dd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9989-5582582f06429840.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-d2253d7e58ef1890.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-5441d4a9ab35e518.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-d9b12d680922d980.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-d4b6df905f185055.js → page-54b32bd27726588d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-e268d6d145827a22.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-186fc6952f027acc.js → page-f8116559c7424da7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-1085399fd0f32112.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-1df686a14f65895a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-cbbe03c0da7b081a.js → page-54e1f966ab60a4dc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-7ed95f296f5ee8e9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/page-11edac668d77e5c3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-59e2b9a22cd5a6d5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/page-2976df21ad74ab48.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-34d211a67f16d510.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-8ff652d5d950639b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-c0c0d19d51342d3e.js → page-b31610deda76337b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-2e002ad8faa43291.js → page-6cb0e1bbefe5fb3d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-a286a152cf584576.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/not-found-d9237581e7003bcb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-83274e761643fdd2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-ceee6cfb1d4e6d68.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-9c9f8bb3e4a64091.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c17217603ea3426a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-efb6177cbf1e8a51.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-d307523703bc4df3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-462f4e4bab45ce7d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-8990e96f33af3837.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-7464c1987dd76e6d.js → page-e168fa1030af0548.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-1233e05a4068bc9c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-13a1840b1fac337a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-64a29fc80b11ab55.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-68e787339a16b3dc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/{page-d8ab4d1489223459.js → page-4fddc90b23feb215.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-886ba8260dc79c79.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-0f26507a1722e40b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-109d4ccd42259c9d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-f5c2a4e510726774.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-b2e0039416f42522.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-26102fd7350f171e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-708948bc8a802794.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-5e5611620f78fb0b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-139ba5c367bbf60a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-app-202b4e81f33895ea.js +1 -0
- package/apps/pwa/out/_next/static/css/43feca5fd387dcbd.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +30 -29
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +30 -29
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +30 -29
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +27 -26
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +27 -26
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +27 -26
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +30 -29
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +30 -29
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +30 -29
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +30 -29
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +30 -29
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +30 -29
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +27 -26
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +30 -29
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +30 -29
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +27 -26
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +27 -26
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +30 -29
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +30 -29
- package/apps/pwa/out/ops/checklist/index.html +1 -0
- package/apps/pwa/out/ops/checklist/index.txt +30 -0
- package/apps/pwa/out/ops/release/index.html +1 -0
- package/apps/pwa/out/ops/release/index.txt +30 -0
- package/apps/pwa/out/ops/seats/index.html +1 -0
- package/apps/pwa/out/ops/seats/index.txt +30 -0
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +28 -27
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +30 -29
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +27 -26
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +30 -29
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +30 -29
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +30 -29
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +30 -29
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +30 -29
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +27 -26
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +30 -29
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +30 -29
- package/apps/pwa/out/today/index.html +1 -0
- package/apps/pwa/out/today/index.txt +33 -0
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +30 -29
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +30 -29
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +30 -29
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +30 -29
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +30 -29
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +30 -29
- package/graphs/field/field-feed.yaml +2 -0
- package/graphs/implement-loop.yaml +1 -1
- package/graphs/release/recipes.yaml +3 -0
- package/graphs/release/release-loop.yaml +3 -1
- package/graphs/test-diet/nightly-audit.yaml +18 -0
- package/graphs/test-diet/recipes.yaml +3 -0
- package/package.json +1 -1
- package/plugins/card-followup/graphs/run-step.ts +48 -13
- package/plugins/doc-draft/graphs/doc-draft.yaml +3 -3
- package/plugins/doc-draft/graphs/run-step.ts +14 -7
- package/scripts/ci-pwa-build-gate.ts +160 -0
- package/scripts/coord-channel-watch.sh +42 -8
- package/scripts/coord-digest.ts +199 -0
- package/scripts/coord-post.sh +60 -3
- package/scripts/install.ps1 +68 -9
- package/scripts/install.sh +39 -12
- package/scripts/lib/pwa-scenarios/scenarios.ts +20 -0
- package/scripts/lib/tui-regress-checks.ts +146 -0
- package/scripts/memory-lifecycle-cycle.ts +2 -2
- package/scripts/ops-now-probe.ts +327 -0
- package/scripts/publish-internal-dist.ts +11 -1
- package/scripts/release-loop/cutoff.ts +31 -2
- package/scripts/release-loop/export-check-node.ts +85 -0
- package/scripts/release-loop/gate-node.ts +18 -4
- package/scripts/release-loop/node-verdict.ts +8 -1
- package/scripts/release-loop/ops-upgrade-node.ts +72 -16
- package/scripts/release-loop/release-readiness.ts +9 -0
- package/scripts/release-loop/version-node.ts +58 -10
- package/scripts/repo-watch-cycle.ts +1 -1
- package/scripts/retro-cycle.ts +2 -2
- package/scripts/schedule-health-report.ts +1 -1
- package/scripts/storyboard/storyboard.ts +5 -161
- package/scripts/test-diet/lib.ts +31 -1
- package/scripts/test-diet/node.ts +134 -17
- package/scripts/test-timeout-audit.ts +114 -0
- package/scripts/tree-sync-alert.ts +1 -1
- package/scripts/tui-regress.ts +71 -0
- package/scripts/waitlist-daily-digest.ts +1 -1
- package/skills/explainer-video/SKILL.md +4 -0
- package/skills/explainer-video/engine/reel.mjs +18 -13
- package/skills/explainer-video/engine/reel.sh +24 -5
- package/skills/explainer-video/engine/stitch.mjs +145 -0
- package/skills/pitch-deck/SKILL.md +36 -0
- package/skills/pitch-deck/template.html +80 -0
- package/src/acp/codex-approval-adapter.ts +9 -3
- package/src/acp/core-turn-bridge.ts +117 -18
- package/src/acp/fact-question.ts +11 -0
- package/src/acp/server.ts +48 -7
- package/src/acp/tool-approval.ts +126 -0
- package/src/acp/turn-runner.ts +36 -0
- package/src/agent/monad-agent-turn.ts +8 -0
- package/src/ask-user-question/pending-questions.ts +25 -10
- package/src/ask-user-question/types.ts +9 -0
- package/src/auth/setup-link-tokens.fixture.ts +19 -0
- package/src/auth/setup-link-tokens.ts +164 -0
- package/src/autopilot/build/integrity-gate.ts +12 -0
- package/src/boot/daemon-prompt-turn.ts +3 -0
- package/src/boot/daemon-tools/index.ts +8 -0
- package/src/boot/daemon-tools/types.ts +2 -0
- package/src/boot/daemon-tools/web-search.ts +1 -0
- package/src/budget/unified-usage.ts +75 -0
- package/src/channel-bot-token.ts +37 -10
- package/src/chat/ctrl-c-in-input.ts +14 -0
- package/src/chat/index.ts +61 -9
- package/src/chat/pickers/state.ts +6 -2
- package/src/cli/agent-cli.ts +7 -0
- package/src/cli/decisions-cli.ts +25 -3
- package/src/cli/doctor-bundle-upload.ts +68 -0
- package/src/cli/doctor-cli.ts +38 -4
- package/src/cli/op-proxy-sync.ts +83 -0
- package/src/cli/ops-cli.ts +20 -0
- package/src/cli/pod-cli.ts +49 -0
- package/src/cli/pr-cli.ts +37 -4
- package/src/cli/pwa-stop.ts +163 -13
- package/src/cli/pwa-test.ts +3 -0
- package/src/cli/release-cli.ts +98 -15
- package/src/cli/self-update.ts +14 -0
- package/src/cli/storyboard-cli.ts +37 -0
- package/src/cli/test-state-dir-flag.ts +11 -4
- package/src/connectors/linear.ts +146 -0
- package/src/context-bus/coord-events.ts +115 -0
- package/src/dashboard/dashboard-boot-fixture.ts +23 -0
- package/src/dashboard/dashboard-status-lines.ts +45 -0
- package/src/dashboard/first-screen-band.ts +35 -0
- package/src/dashboard/index.ts +128 -34
- package/src/dashboard/provider-slash-runtime.ts +28 -11
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +17 -0
- package/src/dashboard/slash-runtime/help-from-registry.ts +43 -28
- package/src/dashboard/turn-stream-formatter.ts +1 -1
- package/src/decisions/decision-cards.ts +50 -2
- package/src/decisions/decision-ledger.ts +75 -4
- package/src/decisions/decision-linear-projection.ts +157 -0
- package/src/decisions/web-push-decision-cards.ts +50 -0
- package/src/directive/interpretation-card.ts +36 -0
- package/src/discord.ts +2 -1
- package/src/domains/coo-admin-tool.ts +62 -0
- package/src/domains/core-tools.ts +11 -3
- package/src/domains/elanous-card.ts +29 -0
- package/src/domains/ops-facts-tool.ts +76 -0
- package/src/domains/outbound-alert.ts +41 -13
- package/src/domains/release-tool.ts +168 -0
- package/src/domains/telegram-kind-route.ts +19 -1
- package/src/exec-requests/planner.ts +98 -28
- package/src/exec-requests/runner.ts +107 -10
- package/src/exec-requests/store.ts +8 -2
- package/src/field/field-media.ts +10 -7
- package/src/field/field-reel.ts +14 -4
- package/src/git-fs/worktree.ts +45 -6
- package/src/graph-runner/graph-approval-card.ts +113 -0
- package/src/graph-runner/graph-cli.ts +129 -4
- package/src/graph-runner/graph-notify.ts +25 -3
- package/src/graph-runner/runner.ts +142 -16
- package/src/harness/browser-verify.ts +25 -2
- package/src/harness/harness-cli-command.ts +231 -35
- package/src/harness/harness-incidents-writer.fixture.ts +24 -0
- package/src/harness/harness-incidents.ts +129 -0
- package/src/harness/harness-stop.ts +53 -3
- package/src/hitl/clarification-policy.ts +3 -1
- package/src/index.ts +172 -41
- package/src/intake-plane/author-goals.ts +37 -4
- package/src/intake-plane/check.ts +3 -0
- package/src/intake-plane/discord-seat-work.ts +47 -2
- package/src/intake-plane/seat-doc-route.ts +170 -6
- package/src/intake-plane/telegram-seat-work.ts +44 -2
- package/src/intake-plane/telegram-work.ts +1 -0
- package/src/maturity/cli-maturity.ts +34 -0
- package/src/maturity/feature-maturity.ts +100 -1
- package/src/maturity/tui-slash-maturity.ts +31 -0
- package/src/memory.ts +32 -4
- package/src/nexus/api/exec-requests.ts +33 -3
- package/src/nexus/api/http-server.ts +73 -1
- package/src/nexus/api/meta-api.ts +38 -0
- package/src/nexus/api/operator.ts +61 -0
- package/src/nexus/api/ops-api.ts +148 -0
- package/src/nexus/api/ops-seats-sources.ts +95 -0
- package/src/nexus/api/ops-seats.ts +124 -0
- package/src/nexus/api/public-routes.ts +14 -1
- package/src/nexus/api/seat-requests.ts +88 -8
- package/src/nexus/api/sessions-store.ts +63 -0
- package/src/nexus/api/setup-claim.ts +21 -0
- package/src/nexus/api/setup-codex-login.ts +160 -0
- package/src/nexus/api/usage-api.ts +102 -0
- package/src/nexus/config/secrets/file-backend.ts +56 -18
- package/src/nexus/index.ts +45 -4
- package/src/onboarding/first-run-doctor.ts +54 -0
- package/src/onboarding/first-run.ts +89 -0
- package/src/onboarding/lan-address.ts +45 -0
- package/src/onboarding/web-first.ts +119 -24
- package/src/onboarding.ts +4 -0
- package/src/ops-now/ops-now-refresh.ts +100 -0
- package/src/project/project-store.ts +115 -0
- package/src/release-loop/checklist.ts +76 -61
- package/src/release-loop/feature-store.ts +460 -0
- package/src/release-loop/manifest.ts +59 -5
- package/src/release-loop/release-schedule.ts +53 -0
- package/src/repl/index.ts +7 -0
- package/src/seat-dispatch/ceo-commands.ts +98 -0
- package/src/seat-dispatch/ceo-intent.ts +38 -0
- package/src/seat-loop/seat-loop.ts +231 -0
- package/src/seat-loop/seat-report.ts +64 -0
- package/src/self-dev/decompose.ts +66 -11
- package/src/self-dev/run-store.ts +10 -1
- package/src/self-implement/execution-clarification.ts +115 -0
- package/src/self-implement/gate-cli.ts +6 -3
- package/src/self-implement/orchestrator.ts +37 -3
- package/src/self-implement/pipeline-shape.ts +1 -1
- package/src/self-implement/run-status-mapping.ts +5 -3
- package/src/session/chat.ts +2 -0
- package/src/session/index.ts +9 -0
- package/src/shell-env-bootstrap.ts +17 -0
- package/src/skills/tools/usage-slash.ts +8 -7
- package/src/steward/directive.ts +51 -2
- package/src/steward/launch.ts +250 -0
- package/src/steward/outcome-notify.ts +115 -0
- package/src/steward/steward-cards.ts +8 -2
- package/src/steward/triage-plan.ts +32 -4
- package/src/steward/triage.ts +93 -9
- package/src/steward/working-backwards.ts +64 -0
- package/src/storyboard/storyboard.ts +141 -0
- package/src/task-cards/card-store.ts +2 -2
- package/src/task-orchestrator/surfaces/pod-lease.ts +260 -0
- package/src/telegram-card-followup.ts +233 -0
- package/src/telegram-commands.ts +16 -0
- package/src/telegram-report.ts +15 -9
- package/src/telegram-run.ts +90 -13
- package/src/telegram.ts +29 -8
- package/src/thinking-line.ts +28 -20
- package/src/tool-runtime/types.ts +4 -0
- package/src/tui/chat/execution-badge.ts +1 -1
- package/src/tui-client/dashboard-session.ts +2 -0
- package/src/user-config.ts +118 -11
- package/src/ux-sim/turn-states.ts +1 -1
- package/src/version/packed-revision.json +1 -1
- package/src/web-push/notify-exec-request.ts +32 -0
- package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4836-3084c58c1725a40b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6090-85a3f05f954e5697.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6284-69db415193d5f8bd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7032-f4ef698518fba61c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8067-d976b441a104ab74.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8519-5632377cce994f86.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8894-7317998f6cb97d8c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9454-a3e0953307216c58.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9740-8f5ff96c522b5f55.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-9aa47b0bc6d570ff.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f4d2ecbbae6088d6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-2053e49930c82748.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-a70ba27c33003dd0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-18166f885d831d3e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-1c9d6cfe68b121c2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-c108ff1fdb665c97.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-147a328753d983e2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-799395a9df7d95aa.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-6a8158b71d1fe9d7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-9567560270857531.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-e7ed3d010153d818.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-b672e02e4bec8deb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-1ba4a5d857276334.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-9db2226f38c542fb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-70ee65f5f7a151dc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-0bb8e43203efb23b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-092e1bcf27d751cc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-eb52580c4b75b905.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-e573437b0a1c728f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-294300625e49e4d3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-085847b088675978.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-9fe9d38c36999eb4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-6141f67f531d8b6a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-295a02b1026b8255.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-1eb9a0e1e054209d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-82efa177a30f3741.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d0be51cec4f62606.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-2e367075ffdbc062.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-647ed2b66c1d49f1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ba058c56e38f2792.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-6870f30ea99f8346.js +0 -1
- package/apps/pwa/out/_next/static/css/5ef72a618a8994f3.css +0 -1
- package/apps/pwa/out/_next/static/lywc8m_HDzH9MKSDQhyEj/_buildManifest.js +0 -1
- package/src/dashboard/turn-typeahead-echo.ts +0 -23
- /package/apps/pwa/out/_next/static/{lywc8m_HDzH9MKSDQhyEj → 2DsiDfDCks44g1cPUMhiW}/_ssgManifest.js +0 -0
package/src/cli/pr-cli.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { runGate as runChangedTypecheckGate } from '../../scripts/ci-typecheck-c
|
|
|
13
13
|
import { runIsolationHardcodeGate } from '../../scripts/ci-isolation-hardcode-gate.js';
|
|
14
14
|
import { androidFilesIn, runAndroidUnitTestGate } from '../../scripts/ci-android-unit-tests.js';
|
|
15
15
|
import { iosFilesIn, runIosUnitTestGate } from '../../scripts/ci-ios-unit-tests.js';
|
|
16
|
+
import { runPwaBuildGate } from '../../scripts/ci-pwa-build-gate.js';
|
|
16
17
|
import { runMockModuleRestoreGate } from '../../scripts/ci-mock-module-restore-gate.js';
|
|
17
18
|
import { runModelHardcodeGate } from '../../scripts/ci-model-hardcode-gate.js';
|
|
18
19
|
import { runDaemonPortGate } from '../../scripts/ci-daemon-port-gate.js';
|
|
@@ -51,6 +52,7 @@ import {
|
|
|
51
52
|
unionPaths,
|
|
52
53
|
} from './pr-granularity.js';
|
|
53
54
|
import { branchLineageSlug, findSiblingPrs } from './pr-lineage.js';
|
|
55
|
+
import { parseReleaseNoteSection } from '../release-loop/release-note.js';
|
|
54
56
|
|
|
55
57
|
export { branchLineageSlug, findSiblingPrs };
|
|
56
58
|
|
|
@@ -114,7 +116,10 @@ export interface PrLandDeps {
|
|
|
114
116
|
runAndroidGate?: (out: { log: (message: string) => void; error: (message: string) => void }, changedFiles: readonly string[]) => boolean;
|
|
115
117
|
/** iOS 시험 게이트 심(시험 주입용). */
|
|
116
118
|
runIosGate?: (out: { log: (message: string) => void; error: (message: string) => void }, changedFiles: readonly string[]) => boolean;
|
|
119
|
+
runPwaGate?: (out: { log: (message: string) => void; error: (message: string) => void }, changedFiles: readonly string[]) => boolean;
|
|
117
120
|
isInteractive?: () => boolean;
|
|
121
|
+
/** Clock for the recent-landings window (test seam · default Date.now). */
|
|
122
|
+
now?: () => number;
|
|
118
123
|
requestConfirmation?: (opts: ConfirmOpts) => Promise<ConfirmResult>;
|
|
119
124
|
overlapConfirmChannels?: ConfirmChannel[];
|
|
120
125
|
overlapConfirmTimeoutMs?: number;
|
|
@@ -404,13 +409,14 @@ function gateVerdict(
|
|
|
404
409
|
label: string,
|
|
405
410
|
gate: (out: { log: (message: string) => void; error: (message: string) => void }) => boolean,
|
|
406
411
|
out: { log: (message: string) => void; error: (message: string) => void },
|
|
412
|
+
failClosed = false,
|
|
407
413
|
): { ok: boolean; measured: boolean } {
|
|
408
414
|
try {
|
|
409
415
|
return { ok: gate(out), measured: true };
|
|
410
416
|
} catch (error) {
|
|
411
417
|
out.error(`⚠ ${label}: 게이트가 «못 쟀다» — ${error instanceof Error ? error.message : String(error)}`);
|
|
412
|
-
out.error(`⚠ ${label}: 「위반 없음」이 아니라 「검사 못 함」이다. 이 착지는 통과시키되 게이트를
|
|
413
|
-
return { ok:
|
|
418
|
+
out.error(`⚠ ${label}: 「위반 없음」이 아니라 「검사 못 함」이다. ${failClosed ? '착지를 막는다.' : '이 착지는 통과시키되 게이트를 고쳐라.'}`);
|
|
419
|
+
return { ok: !failClosed, measured: false };
|
|
414
420
|
}
|
|
415
421
|
}
|
|
416
422
|
|
|
@@ -424,6 +430,21 @@ function record(step: string, ok: boolean, extra: Record<string, unknown> = {}):
|
|
|
424
430
|
debug.log('pr.land', 'step', { step, ok, ...extra });
|
|
425
431
|
}
|
|
426
432
|
|
|
433
|
+
export function nextMdWarning(body: string, changedFiles: readonly string[], out: { log: (message: string) => void }): void {
|
|
434
|
+
const section = body.split(/^## 릴리스 노트[ \t]*\r?$/m).at(-1)!;
|
|
435
|
+
const parsed = parseReleaseNoteSection(body);
|
|
436
|
+
const hasNote = parsed.fragment !== undefined || parsed.problems.length > 0;
|
|
437
|
+
const kind = parsed.fragment?.kind ?? (hasNote
|
|
438
|
+
? /^[ \t]*- 종류:[ \t]*(feat|fix|security|internal)[ \t]*\r?$/m.exec(section.split(/^#{1,6}[ \t]+/m)[0]!)?.[1]
|
|
439
|
+
: undefined);
|
|
440
|
+
const reason = hasNote
|
|
441
|
+
? (kind === 'feat' || kind === 'fix' || kind === 'security' ? `release-note:${kind}` : undefined)
|
|
442
|
+
: (changedFiles.some((file) => /^(?:apps\/pwa\/src\/|src\/cli\/|src\/dashboard\/)/.test(file)) ? 'user-facing-path' : undefined);
|
|
443
|
+
const warn = !!reason && !changedFiles.includes('release/next.md');
|
|
444
|
+
record('next-md', true, { warnOnly: true, reason: reason ?? (hasNote ? 'non-user-facing-note' : 'no-user-facing-path') });
|
|
445
|
+
if (warn) out.log('⚠ next-md: 사용자에게 보이는 변경인데 release/next.md 에 줄이 없습니다 — 공개 노트에서 빠집니다. 「- <kind> — <영어 한 문장>. Documentation: … Target: next.」 한 줄을 더하십시오.');
|
|
446
|
+
}
|
|
447
|
+
|
|
427
448
|
type PrRemoteCapability = 'github' | 'local-path' | 'no-remote' | 'other';
|
|
428
449
|
|
|
429
450
|
function isLocalRemote(raw: string): boolean {
|
|
@@ -822,11 +843,12 @@ function emitRecentLandingRateAdvisory(
|
|
|
822
843
|
cwd: string,
|
|
823
844
|
commits: readonly { subject: string; authorEmail: string; committedAtMs: number }[] | null,
|
|
824
845
|
out: { log: (message: string) => void },
|
|
846
|
+
now: () => number = Date.now,
|
|
825
847
|
): boolean {
|
|
826
848
|
const windowMinutes = RECENT_LANDING_WINDOW_MINUTES;
|
|
827
849
|
const authorEmail = currentAuthorEmail(run, cwd);
|
|
828
850
|
if (!authorEmail || commits === null) {
|
|
829
|
-
const previous = commits === null ? undefined : previousLandingSummary(commits,
|
|
851
|
+
const previous = commits === null ? undefined : previousLandingSummary(commits, now());
|
|
830
852
|
record('recent-landing-rate', true, {
|
|
831
853
|
recentCount: 0,
|
|
832
854
|
windowMinutes,
|
|
@@ -836,7 +858,7 @@ function emitRecentLandingRateAdvisory(
|
|
|
836
858
|
});
|
|
837
859
|
return false;
|
|
838
860
|
}
|
|
839
|
-
const nowMs =
|
|
861
|
+
const nowMs = now();
|
|
840
862
|
const recentCount = countRecentLandings(commits, {
|
|
841
863
|
authorEmail,
|
|
842
864
|
nowMs,
|
|
@@ -1077,6 +1099,7 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
|
|
|
1077
1099
|
cwd,
|
|
1078
1100
|
collectResolvedBaseLandings(run, cwd, DEFAULT_GRANULARITY_SINCE, base),
|
|
1079
1101
|
out,
|
|
1102
|
+
deps.now,
|
|
1080
1103
|
);
|
|
1081
1104
|
|
|
1082
1105
|
// ⛔ local-path / no-remote 는 gh 가 원리상 못 한다. 이미 아는 자리에서 멈춘다.
|
|
@@ -1319,6 +1342,7 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
|
|
|
1319
1342
|
}
|
|
1320
1343
|
|
|
1321
1344
|
const changedPaths = currentChangePaths(run, cwd, staged, base, out);
|
|
1345
|
+
nextMdWarning(body ?? '', changedPaths, out);
|
|
1322
1346
|
publicLeakWarning(changedPaths, deps.runPublicLeakGate ?? runPrLandPublicLeakGate, out);
|
|
1323
1347
|
if (!exportLeakBlock(changedPaths, () => (deps.runExportLeakCheck ?? runPrLandExportLeakCheck)(changedPaths ?? [], cwd), opts.allowPublicLeak === true, out)) return 1;
|
|
1324
1348
|
if (publicDocPaths(changedPaths, () => true).length > 0) {
|
|
@@ -1367,6 +1391,15 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
|
|
|
1367
1391
|
out.log('✓ ios-gate: scripts/ci-ios-unit-tests.ts PASS — iOS 순수-로직 시험이 «실제로 돌았고» 실패 0.');
|
|
1368
1392
|
}
|
|
1369
1393
|
|
|
1394
|
+
// PWA 는 bun test/tsc 밖의 정적 번들이다. 측정 불가도 착지 불가로 판정한다.
|
|
1395
|
+
const pwaGate = gateVerdict('pwa-gate', deps.runPwaGate
|
|
1396
|
+
? (o) => deps.runPwaGate!(o, changedPaths)
|
|
1397
|
+
: (o) => runPwaBuildGate({ args: ['--changed-files', ...changedPaths], cwd, log: o.log, error: o.error }) === 0, out, true);
|
|
1398
|
+
record('pwa-gate', pwaGate.ok && pwaGate.measured, { measured: pwaGate.measured });
|
|
1399
|
+
if (!pwaGate.ok || !pwaGate.measured) {
|
|
1400
|
+
out.error('✗ pwa-gate: scripts/ci-pwa-build-gate.ts blocked pr land — PWA 빌드 실패 또는 측정 불가.');
|
|
1401
|
+
return 1;
|
|
1402
|
+
}
|
|
1370
1403
|
|
|
1371
1404
|
const overlapFound = emitLandingOverlapAdvisory(run, cwd, base, changedPaths, out);
|
|
1372
1405
|
const siblingFound = emitSiblingPrAdvisory(
|
package/src/cli/pwa-stop.ts
CHANGED
|
@@ -23,6 +23,12 @@ import { probeTailscale, type TailscaleProbe } from '../nexus/onboarding/tailsca
|
|
|
23
23
|
import { unregisterPwaInstance } from './pwa-registry.js';
|
|
24
24
|
import { readNexusLock } from '../nexus/supervisor/lock.js';
|
|
25
25
|
import { resolveNexusPwa, type NexusPwaResolution } from './nexus-show.js';
|
|
26
|
+
import { debug } from '../debug/log.js';
|
|
27
|
+
import { isPidAlive } from '../process/pid-liveness.js';
|
|
28
|
+
import { execFileSync } from 'node:child_process';
|
|
29
|
+
import { join as joinPath, resolve as resolvePath } from 'node:path';
|
|
30
|
+
import { hostname } from 'node:os';
|
|
31
|
+
import { safeReadLock } from '../telegram-lock.js';
|
|
26
32
|
|
|
27
33
|
export interface PwaStopOpts {
|
|
28
34
|
out?: { log: (s: string) => void; error: (s: string) => void };
|
|
@@ -45,7 +51,19 @@ export interface PwaStopOpts {
|
|
|
45
51
|
* from `~/.elanous/pwa-registry.json`. */
|
|
46
52
|
unregisterFn?: (pid: number) => void;
|
|
47
53
|
/** P4 — read the nexus lock to recover daemon pid. Test seam. */
|
|
48
|
-
readLockFn?: () => { pid: number } | null;
|
|
54
|
+
readLockFn?: () => { pid: number; host?: string; startedAt?: string } | null;
|
|
55
|
+
/** Isolated test daemon stop only: the project-local root to inspect for orphans. */
|
|
56
|
+
isolatedRoot?: string;
|
|
57
|
+
/** Test seams for signals, liveness and bounded polling. */
|
|
58
|
+
signalFn?: (pid: number, signal: 'SIGINT' | 'SIGTERM') => void;
|
|
59
|
+
pidAliveFn?: (pid: number) => boolean;
|
|
60
|
+
sleepFn?: (ms: number) => Promise<void>;
|
|
61
|
+
stopTimeoutMs?: number;
|
|
62
|
+
nowFn?: () => number;
|
|
63
|
+
/** Process table for isolated daemon identity checks and orphan discovery (pid, start time, argv). */
|
|
64
|
+
processTableFn?: () => Array<{ pid: number; startedAt: string; args: string }>;
|
|
65
|
+
/** Test seam for the raw `ps -eo pid=,lstart=,args=` response. */
|
|
66
|
+
psOutputFn?: () => string;
|
|
49
67
|
}
|
|
50
68
|
|
|
51
69
|
export type PwaStopShareUnmount =
|
|
@@ -57,7 +75,7 @@ export interface PwaStopResult {
|
|
|
57
75
|
exitCode: number;
|
|
58
76
|
/** True when the dev BG was actually killed (vs no-lock no-op). */
|
|
59
77
|
devKilled: boolean;
|
|
60
|
-
/** True when the
|
|
78
|
+
/** True when the daemon stop completed (or the ordinary stop trigger succeeded). */
|
|
61
79
|
nexusStopped: boolean;
|
|
62
80
|
/** True when `tailscale serve reset` was issued (not skipped). */
|
|
63
81
|
shareReset: boolean;
|
|
@@ -73,6 +91,126 @@ async function defaultNexusStop(): Promise<{ exitCode: number }> {
|
|
|
73
91
|
return { exitCode: 0 };
|
|
74
92
|
}
|
|
75
93
|
|
|
94
|
+
function runningProcesses(output: string): Array<{ pid: number; startedAt: string; args: string }> {
|
|
95
|
+
const lines = output.split('\n');
|
|
96
|
+
return lines.flatMap(line => {
|
|
97
|
+
const match = line.match(/^\s*(\d+)\s+((?:\S+\s+){4}\d{4})\s+(.+)$/);
|
|
98
|
+
return match ? [{ pid: Number(match[1]), startedAt: match[2]!, args: match[3]! }] : [];
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
type ProcessRow = ReturnType<typeof runningProcesses>[number];
|
|
103
|
+
|
|
104
|
+
function isolatedDaemons(root: string, table: ProcessRow[]): ProcessRow[] {
|
|
105
|
+
return table.filter(row => {
|
|
106
|
+
if (!/^(?:(?:\S*\/)?(?:bun|node)\s+)?(?:\S*\/)?elanous(?:\.(?:mjs|cjs))?\s/.test(row.args)
|
|
107
|
+
|| !/(?:^|\s)nexus\s+run(?:\s|$)/.test(row.args)) return false;
|
|
108
|
+
const roots = ['--config-dir', '--test-state-dir'].flatMap(flag => {
|
|
109
|
+
const value = row.args.match(new RegExp(`(?:^|\\s)${flag}(?:=|\\s+)(?:"([^"]+)"|'([^']+)'|(\\S+))`));
|
|
110
|
+
const arg = value?.[1] ?? value?.[2] ?? value?.[3];
|
|
111
|
+
return arg === undefined ? [] : [resolvePath(arg)];
|
|
112
|
+
});
|
|
113
|
+
return roots.length > 0 && roots.every(value => value === resolvePath(root));
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function stopIsolatedNexus(
|
|
118
|
+
opts: PwaStopOpts,
|
|
119
|
+
out: NonNullable<PwaStopOpts['out']>,
|
|
120
|
+
lock: ReturnType<NonNullable<PwaStopOpts['readLockFn']>>,
|
|
121
|
+
): Promise<{ exitCode: number }> {
|
|
122
|
+
const root = opts.isolatedRoot!;
|
|
123
|
+
const inspect = (): ProcessRow[] => isolatedDaemons(root, opts.processTableFn
|
|
124
|
+
? opts.processTableFn()
|
|
125
|
+
: runningProcesses((opts.psOutputFn ?? (() => execFileSync('ps', ['-eo', 'pid=,lstart=,args='], { encoding: 'utf8' })))()));
|
|
126
|
+
const reportOrphans = (): { exitCode: number } => {
|
|
127
|
+
const orphans = inspect();
|
|
128
|
+
debug.log('nexus.stop', 'orphans', { pid: 0, waitedMs: 0, count: orphans.length });
|
|
129
|
+
if (orphans.length === 0) {
|
|
130
|
+
out.log(' nothing to stop (no lock)');
|
|
131
|
+
return { exitCode: 0 };
|
|
132
|
+
}
|
|
133
|
+
for (const row of orphans) out.error(` orphan pid ${row.pid} · started ${row.startedAt} · root ${root}`);
|
|
134
|
+
out.error(` cleanup: ${orphans.map(row => `kill ${row.pid}`).join(' ; ')}`);
|
|
135
|
+
return { exitCode: 1 };
|
|
136
|
+
};
|
|
137
|
+
if (!lock) {
|
|
138
|
+
try { return reportOrphans(); }
|
|
139
|
+
catch (error) {
|
|
140
|
+
out.error(` cannot inspect nexus processes: ${error instanceof Error ? error.message : String(error)}`);
|
|
141
|
+
return { exitCode: 1 };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const pid = lock.pid;
|
|
145
|
+
if (lock.host && lock.host !== hostname()) {
|
|
146
|
+
out.error(` nexus lock held by remote host ${lock.host}; cannot stop from here`);
|
|
147
|
+
return { exitCode: 1 };
|
|
148
|
+
}
|
|
149
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) {
|
|
150
|
+
out.error(` invalid nexus lock pid ${pid}`);
|
|
151
|
+
return { exitCode: 1 };
|
|
152
|
+
}
|
|
153
|
+
const signal = opts.signalFn ?? ((target: number, name: 'SIGINT' | 'SIGTERM') => process.kill(target, name));
|
|
154
|
+
const alive = opts.pidAliveFn ?? isPidAlive;
|
|
155
|
+
const sleep = opts.sleepFn ?? ((ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms)));
|
|
156
|
+
const timeout = opts.stopTimeoutMs ?? 10_000;
|
|
157
|
+
const now = opts.nowFn ?? Date.now;
|
|
158
|
+
let waitedMs = 0;
|
|
159
|
+
const wait = async (): Promise<boolean> => {
|
|
160
|
+
const start = now();
|
|
161
|
+
let elapsed = 0;
|
|
162
|
+
while (alive(pid) && elapsed < timeout) {
|
|
163
|
+
const interval = Math.min(100, timeout - elapsed);
|
|
164
|
+
await sleep(interval);
|
|
165
|
+
elapsed = Math.max(elapsed + interval, now() - start);
|
|
166
|
+
}
|
|
167
|
+
waitedMs += elapsed;
|
|
168
|
+
debug.log('nexus.stop', 'wait', { pid, waitedMs, count: 1 });
|
|
169
|
+
return !alive(pid);
|
|
170
|
+
};
|
|
171
|
+
try {
|
|
172
|
+
if (!alive(pid)) return reportOrphans();
|
|
173
|
+
const identity = inspect().find(row => row.pid === pid);
|
|
174
|
+
if (!identity) {
|
|
175
|
+
out.error(` pid ${pid} does not match this tree's isolated nexus daemon; no signal sent`);
|
|
176
|
+
return { exitCode: 1 };
|
|
177
|
+
}
|
|
178
|
+
if (lock.startedAt) {
|
|
179
|
+
const processStart = Date.parse(identity.startedAt);
|
|
180
|
+
const lockStart = Date.parse(lock.startedAt);
|
|
181
|
+
if (!Number.isFinite(processStart) || !Number.isFinite(lockStart) || processStart > lockStart + 1_000) {
|
|
182
|
+
out.error(` pid ${pid} start time cannot match the nexus lock; no signal sent`);
|
|
183
|
+
return { exitCode: 1 };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// Recheck the argv and start time immediately before each signal: a stale
|
|
187
|
+
// lock or a PID recycled during the wait must never target another process.
|
|
188
|
+
const stillOwned = (): boolean => inspect().some(row => row.pid === pid && row.startedAt === identity.startedAt && row.args === identity.args);
|
|
189
|
+
if (!stillOwned()) {
|
|
190
|
+
out.error(` pid ${pid} no longer matches the locked daemon; no signal sent`);
|
|
191
|
+
return { exitCode: 1 };
|
|
192
|
+
}
|
|
193
|
+
signal(pid, 'SIGINT');
|
|
194
|
+
out.log(` SIGINT sent to pid ${pid}`);
|
|
195
|
+
if (await wait()) return { exitCode: 0 };
|
|
196
|
+
if (!alive(pid)) return { exitCode: 0 };
|
|
197
|
+
if (!stillOwned()) {
|
|
198
|
+
out.error(` pid ${pid} no longer matches the locked daemon; SIGTERM not sent`);
|
|
199
|
+
return { exitCode: 1 };
|
|
200
|
+
}
|
|
201
|
+
signal(pid, 'SIGTERM');
|
|
202
|
+
debug.log('nexus.stop', 'escalated', { pid, waitedMs, count: 1 });
|
|
203
|
+
if (await wait()) return { exitCode: 0 };
|
|
204
|
+
} catch (error) {
|
|
205
|
+
out.error(` pid ${pid} 이 멈추지 않았다: ${error instanceof Error ? error.message : String(error)}`);
|
|
206
|
+
debug.log('nexus.stop', 'stuck', { pid, waitedMs, count: 1 });
|
|
207
|
+
return { exitCode: 1 };
|
|
208
|
+
}
|
|
209
|
+
out.error(` pid ${pid} 이 멈추지 않았다`);
|
|
210
|
+
debug.log('nexus.stop', 'stuck', { pid, waitedMs, count: 1 });
|
|
211
|
+
return { exitCode: 1 };
|
|
212
|
+
}
|
|
213
|
+
|
|
76
214
|
async function defaultShareReset(binary: string, port: number): Promise<{ exitCode: number }> {
|
|
77
215
|
const r = await unmountTailscaleServe({
|
|
78
216
|
mode: { kind: 'tls-tcp', port },
|
|
@@ -111,11 +249,12 @@ export async function runPwaStop(opts: PwaStopOpts = {}): Promise<PwaStopResult>
|
|
|
111
249
|
const out = opts.out ?? console;
|
|
112
250
|
const portResolution = resolveShareUnmountPort(opts);
|
|
113
251
|
const devStopFn = opts.devStopFn ?? (() => runPwaDevStop({ out }));
|
|
114
|
-
const nexusStopFn = opts.nexusStopFn ?? defaultNexusStop;
|
|
115
252
|
const shareProbeFn = opts.shareProbeFn ?? (() => probeTailscale());
|
|
116
253
|
const shareResetFn = opts.shareResetFn ?? defaultShareReset;
|
|
117
|
-
const unregisterFn = opts.unregisterFn ?? unregisterPwaInstance;
|
|
118
|
-
const readLockFn = opts.readLockFn ?? (
|
|
254
|
+
const unregisterFn = opts.unregisterFn ?? (opts.isolatedRoot ? undefined : unregisterPwaInstance);
|
|
255
|
+
const readLockFn = opts.readLockFn ?? (opts.isolatedRoot
|
|
256
|
+
? () => safeReadLock(joinPath(opts.isolatedRoot!, 'nexus', '.lock'))
|
|
257
|
+
: () => readNexusLock());
|
|
119
258
|
|
|
120
259
|
out.log('elanous nexus pwa stop: cascade');
|
|
121
260
|
|
|
@@ -157,19 +296,30 @@ export async function runPwaStop(opts: PwaStopOpts = {}): Promise<PwaStopResult>
|
|
|
157
296
|
}
|
|
158
297
|
|
|
159
298
|
// P4 registry unregister — read the lock for the daemon pid before
|
|
160
|
-
// we send SIGINT (the lock is removed during stop).
|
|
161
|
-
//
|
|
299
|
+
// we send SIGINT (the lock is removed during stop). The isolated stop
|
|
300
|
+
// never writes to the production registry. Missing lock/pid is best-effort.
|
|
162
301
|
let unregisteredPid: number | undefined;
|
|
302
|
+
let lock: ReturnType<typeof readLockFn> = null;
|
|
303
|
+
try { lock = readLockFn(); }
|
|
304
|
+
catch (error) {
|
|
305
|
+
if (opts.isolatedRoot) {
|
|
306
|
+
out.error(` cannot read nexus lock: ${error instanceof Error ? error.message : String(error)}`);
|
|
307
|
+
return { exitCode: 1, devKilled: false, nexusStopped: false, shareReset, shareUnmount };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
163
310
|
try {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
311
|
+
if (lock && typeof lock.pid === 'number' && (!opts.isolatedRoot || !lock.host || lock.host === hostname())) {
|
|
312
|
+
if (unregisterFn) {
|
|
313
|
+
unregisterFn(lock.pid);
|
|
314
|
+
unregisteredPid = lock.pid;
|
|
315
|
+
}
|
|
168
316
|
}
|
|
169
|
-
} catch { /*
|
|
317
|
+
} catch { /* registry unregister failure — non-fatal */ }
|
|
170
318
|
|
|
171
319
|
const devR = await devStopFn();
|
|
172
|
-
const nexusR = await nexusStopFn(
|
|
320
|
+
const nexusR = await (opts.nexusStopFn ?? (opts.isolatedRoot
|
|
321
|
+
? () => stopIsolatedNexus(opts, out, lock)
|
|
322
|
+
: defaultNexusStop))();
|
|
173
323
|
const exitCode = devR.exitCode === 0 && nexusR.exitCode === 0 ? 0 : 1;
|
|
174
324
|
return {
|
|
175
325
|
exitCode,
|
package/src/cli/pwa-test.ts
CHANGED
|
@@ -720,8 +720,11 @@ async function runStop(
|
|
|
720
720
|
out,
|
|
721
721
|
shareProbeFn: async () => ({ installed: false, alive: false }),
|
|
722
722
|
shareResetFn: async () => ({ exitCode: 0 }),
|
|
723
|
+
isolatedRoot: layout.stateDir,
|
|
723
724
|
});
|
|
724
725
|
|
|
726
|
+
// Keep the record when the daemon did not stop; status must not imply a clean shutdown.
|
|
727
|
+
if (stopRes.exitCode !== 0) return { exitCode: stopRes.exitCode };
|
|
725
728
|
// Cleanup state files — leave logs/ for post-mortem.
|
|
726
729
|
const stateFile = joinPath(layout.stateDir, 'test-state.json');
|
|
727
730
|
if (existsSync(stateFile)) {
|
package/src/cli/release-cli.ts
CHANGED
|
@@ -23,8 +23,11 @@ import { effectiveInstanceRoot, releaseLedgerRoot } from '../instance/resolve.js
|
|
|
23
23
|
import { getElanousConfigDirOverride } from '../elanous-config-dir.js';
|
|
24
24
|
import { envLiteral } from '../platform/env-literal.js';
|
|
25
25
|
import { userConfigPath } from '../user-config.js';
|
|
26
|
-
import { addItem, devVersion, listChecklist, removeItem, seedFromRoadmap, setItem, summarizeChecklist, type ChecklistStatus, type ChecklistDisposition, type ChecklistKind } from '../release-loop/checklist.js';
|
|
26
|
+
import { addItem, claimItem, decodeClaimHistoryEntry, devVersion, listChecklist, ownerMatches, parseOwner, parityGap, removeItem, seedFromRoadmap, setItem, summarizeChecklist, type ChecklistStatus, type ChecklistDisposition, type ChecklistKind } from '../release-loop/checklist.js';
|
|
27
|
+
import * as features from '../release-loop/feature-store.js';
|
|
28
|
+
import { formatSchedule, getSchedule, listSchedules, setSchedule } from '../release-loop/release-schedule.js';
|
|
27
29
|
import { writeStdoutJson } from './stdout-json.js';
|
|
30
|
+
import { CliUserError } from './cli-user-error.js';
|
|
28
31
|
import { runUnattendedRelease, type UnattendedReleaseDeps } from '../../scripts/release-loop/unattended-release.js';
|
|
29
32
|
import { releaseReadiness } from '../../scripts/release-loop/release-readiness.js';
|
|
30
33
|
import { runReleaseIfReady } from './release-run-if-ready.js';
|
|
@@ -467,15 +470,23 @@ function checklistCodenames(): Record<string, string> {
|
|
|
467
470
|
function checklistVersion(input: string | undefined, codenames: Record<string, string>): string {
|
|
468
471
|
const v = input ?? devVersion().replace(/-dev\.\d+$/, '');
|
|
469
472
|
const resolved = isReleaseVersion(v) ? v : Object.entries(codenames).find(([, name]) => name === v)?.[0] ?? v;
|
|
470
|
-
if (!isReleaseVersion(resolved)) throw new
|
|
473
|
+
if (!isReleaseVersion(resolved)) throw new CliUserError(`체크리스트 판 또는 별칭이 아니다: ${v}`);
|
|
471
474
|
return resolved;
|
|
472
475
|
}
|
|
473
476
|
|
|
474
|
-
async function checklistOutput(v: string, codenames: Record<string, string>, mode: 'list' | 'status', json?: boolean): Promise<void> {
|
|
475
|
-
|
|
477
|
+
async function checklistOutput(v: string, codenames: Record<string, string>, mode: 'list' | 'status', json?: boolean, owner?: string): Promise<void> {
|
|
478
|
+
if (owner !== undefined) parseOwner(owner);
|
|
479
|
+
const snapshot = listChecklist(v);
|
|
480
|
+
const data = owner === undefined ? snapshot : { ...snapshot, items: snapshot.items.filter((item) => ownerMatches(item.owner, owner)) };
|
|
476
481
|
const summary = summarizeChecklist(data);
|
|
477
|
-
const
|
|
482
|
+
const parity = data.items.flatMap((item) => {
|
|
483
|
+
const why = item.kind === 'screen' ? parityGap(item.evidence) : null;
|
|
484
|
+
return why ? [{ id: item.id, why }] : [];
|
|
485
|
+
});
|
|
486
|
+
const result = { ...data, codename: codenames[v] ?? '', ...summary, parity };
|
|
487
|
+
for (const { id, why } of parity) debug.log('release.checklist', 'parity-warning', { version: v, id, why });
|
|
478
488
|
if (json) { await writeStdoutJson(`${JSON.stringify(result)}\n`); return; }
|
|
489
|
+
console.log(`⚠ 짝 경고 ${parity.length}: ${parity.map(({ id }) => id).join(', ') || '없음'}`);
|
|
479
490
|
console.log(`${v}${result.codename ? ` (${result.codename})` : ''} · 공개 ${data.released || '없음'} · 개발 ${data.dev}`);
|
|
480
491
|
if (mode === 'list') {
|
|
481
492
|
for (const item of data.items) console.log(`${({ green: '🟢', yellow: '🟡', red: '🔴', done: '✅' } as const)[item.status]} ${item.id} ${item.title}${item.owner ? ` · ${item.owner}` : ''}`);
|
|
@@ -488,24 +499,44 @@ async function checklistOutput(v: string, codenames: Record<string, string>, mod
|
|
|
488
499
|
|
|
489
500
|
export function registerReleaseCommands(program: Command, releaseRunDeps: UnattendedReleaseDeps = {}): void {
|
|
490
501
|
const release = program.command('release').description('공개 배포 한 판 — prepare(로컬) → publish(--yes) → verify (docs/manual/MANUAL-versioning-and-release-2026-09-25.md)');
|
|
502
|
+
const schedule = release.command('schedule').description('판별 컷·착지 마감 조회·갱신');
|
|
503
|
+
const scheduleLedgerRoot = () => releaseRunDeps.ledgerRoot ?? releaseLedgerRoot();
|
|
504
|
+
schedule.command('list').option('--json', '결과 JSON').action((o: { json?: boolean }) => {
|
|
505
|
+
const rows = listSchedules(scheduleLedgerRoot());
|
|
506
|
+
if (o.json) console.log(JSON.stringify(rows));
|
|
507
|
+
else for (const row of rows) console.log(formatSchedule(row));
|
|
508
|
+
});
|
|
509
|
+
schedule.command('show').requiredOption('--version <v>', '조회할 판').option('--json', '결과 JSON')
|
|
510
|
+
.action((o: { version: string; json?: boolean }) => {
|
|
511
|
+
const row = getSchedule(o.version, scheduleLedgerRoot());
|
|
512
|
+
if (!row) throw new CliUserError(`없는 판 일정: ${o.version}`, '판 일정 등록: elanous release schedule set --version <v> --cut-at <iso> --land-by <iso>');
|
|
513
|
+
if (o.json) console.log(JSON.stringify(row)); else console.log(formatSchedule(row));
|
|
514
|
+
});
|
|
515
|
+
schedule.command('set').requiredOption('--version <v>', '갱신할 판')
|
|
516
|
+
.option('--cut-at <iso>', '오프셋 포함 컷 시각').option('--land-by <iso>', '오프셋 포함 착지 마감').option('--json', '결과 JSON')
|
|
517
|
+
.action((o: { version: string; cutAt?: string; landBy?: string; json?: boolean }) => {
|
|
518
|
+
if (o.cutAt === undefined && o.landBy === undefined) throw new CliUserError('갱신할 시각을 지정하라', '--cut-at <iso> 또는 --land-by <iso>');
|
|
519
|
+
const row = setSchedule(o.version, { cutAt: o.cutAt, landBy: o.landBy }, process.env.ELANOUS_TRACK || 'cli', scheduleLedgerRoot());
|
|
520
|
+
if (o.json) console.log(JSON.stringify(row)); else console.log(formatSchedule(row));
|
|
521
|
+
});
|
|
491
522
|
const checklist = release.command('checklist').description('판별 확인표 조회·갱신')
|
|
492
523
|
.option('--version <v>', '판 또는 별칭(기본: package.json 의 개발판에서 -dev.N 제거)')
|
|
493
524
|
.option('--json', '결과 JSON');
|
|
494
525
|
const context = (cmd: Command) => {
|
|
495
|
-
const opts = { ...(cmd.parent?.opts() as { version?: string; json?: boolean }), ...(cmd.opts() as { version?: string; json?: boolean }) };
|
|
526
|
+
const opts = { ...(cmd.parent?.parent?.opts() as { version?: string; json?: boolean }), ...(cmd.parent?.opts() as { version?: string; json?: boolean }), ...(cmd.opts() as { version?: string; json?: boolean }) };
|
|
496
527
|
const codenames = checklistCodenames();
|
|
497
528
|
return { version: checklistVersion(opts.version, codenames), codenames, json: opts.json };
|
|
498
529
|
};
|
|
499
530
|
const withContext = (cmd: Command) => cmd.option('--version <v>', '판 또는 별칭').option('--json', '결과 JSON');
|
|
500
|
-
withContext(checklist.command('list').description('모든 칸')).action(async (
|
|
531
|
+
withContext(checklist.command('list').description('모든 칸')).option('--owner <owner>', '자리 또는 하위 자리로 거르기').action(async (opts: { owner?: string }, cmd: Command) => {
|
|
501
532
|
const { version, codenames, json } = context(cmd);
|
|
502
|
-
await checklistOutput(version, codenames, 'list', json);
|
|
533
|
+
await checklistOutput(version, codenames, 'list', json, opts.owner);
|
|
503
534
|
});
|
|
504
|
-
withContext(checklist.command('status').description('상태 요약')).action(async (
|
|
535
|
+
withContext(checklist.command('status').description('상태 요약')).option('--owner <owner>', '자리 또는 하위 자리로 거르기').action(async (opts: { owner?: string }, cmd: Command) => {
|
|
505
536
|
const { version, codenames, json } = context(cmd);
|
|
506
|
-
await checklistOutput(version, codenames, 'status', json);
|
|
537
|
+
await checklistOutput(version, codenames, 'status', json, opts.owner);
|
|
507
538
|
});
|
|
508
|
-
withContext(checklist.command('add <id> <title>').description('칸 추가')).option('--owner <owner>', '담당').option('--kind <kind>', 'screen = 다섯 화면 짝 칸')
|
|
539
|
+
withContext(checklist.command('add <id> <title>').description('칸 추가')).option('--owner <owner>', '담당').option('--kind <kind>', 'screen = 다섯 화면 짝 칸 · 근거에 짝: PWA … · 데스크톱 … · 폴드 … · 아이폰 … · 아이패드 … 한 줄')
|
|
509
540
|
.action((id: string, title: string, opts: { owner?: string; kind?: string }, cmd: Command) => {
|
|
510
541
|
const { version, json } = context(cmd);
|
|
511
542
|
const data = addItem(version, { id, title, ...(opts.owner !== undefined ? { owner: opts.owner } : {}), ...(opts.kind !== undefined ? { kind: opts.kind as ChecklistKind } : {}) });
|
|
@@ -513,15 +544,28 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
|
|
|
513
544
|
});
|
|
514
545
|
withContext(checklist.command('set <id>').description('칸 상태·근거·담당·처분 갱신'))
|
|
515
546
|
.option('--status <status>', 'green|yellow|red|done').option('--evidence <evidence>', '근거').option('--owner <owner>', '담당')
|
|
516
|
-
.option('--disposition <disposition>', 'move|known-issue|block').option('--kind <kind>', 'screen = 다섯 화면 짝 칸')
|
|
547
|
+
.option('--disposition <disposition>', 'move|known-issue|block').option('--kind <kind>', 'screen = 다섯 화면 짝 칸 · 근거에 짝: PWA … · 데스크톱 … · 폴드 … · 아이폰 … · 아이패드 … 한 줄')
|
|
517
548
|
.action((id: string, opts: { status?: string; evidence?: string; owner?: string; disposition?: string; kind?: string }, cmd: Command) => {
|
|
518
549
|
const { version, json } = context(cmd);
|
|
519
|
-
if (opts.status !== undefined && !['green', 'yellow', 'red', 'done'].includes(opts.status)) throw new
|
|
520
|
-
if (opts.disposition !== undefined && !['move', 'known-issue', 'block'].includes(opts.disposition)) throw new
|
|
521
|
-
if (opts.status === undefined && opts.evidence === undefined && opts.owner === undefined && opts.disposition === undefined && opts.kind === undefined) throw new
|
|
550
|
+
if (opts.status !== undefined && !['green', 'yellow', 'red', 'done'].includes(opts.status)) throw new CliUserError(`잘못된 상태: ${opts.status}`, 'green|yellow|red|done');
|
|
551
|
+
if (opts.disposition !== undefined && !['move', 'known-issue', 'block'].includes(opts.disposition)) throw new CliUserError(`잘못된 처분: ${opts.disposition}`, 'move|known-issue|block');
|
|
552
|
+
if (opts.status === undefined && opts.evidence === undefined && opts.owner === undefined && opts.disposition === undefined && opts.kind === undefined) throw new CliUserError('갱신할 칸을 지정하라', '--status · --evidence · --owner · --disposition · --kind 중 하나');
|
|
522
553
|
const data = setItem(version, id, { ...(opts.status !== undefined ? { status: opts.status as ChecklistStatus } : {}), ...(opts.evidence !== undefined ? { evidence: opts.evidence } : {}), ...(opts.owner !== undefined ? { owner: opts.owner } : {}), ...(opts.disposition !== undefined ? { disposition: opts.disposition as ChecklistDisposition } : {}), ...(opts.kind !== undefined ? { kind: opts.kind as ChecklistKind } : {}) }, process.env.ELANOUS_TRACK || 'cli');
|
|
554
|
+
const item = data.items.find((entry) => entry.id === id);
|
|
555
|
+
const why = item?.kind === 'screen' && item.status === 'green' ? parityGap(item.evidence) : null;
|
|
556
|
+
if (why) {
|
|
557
|
+
console.error(`⚠ 짝: ${why} — 근거에 «짝: PWA … · 데스크톱 … · 폴드 … · 아이폰 … · 아이패드 …» 한 줄`);
|
|
558
|
+
debug.log('release.checklist', 'parity-warning', { version, id, why });
|
|
559
|
+
}
|
|
523
560
|
if (json) console.log(JSON.stringify(data)); else console.log(`✅ ${id} 갱신`);
|
|
524
561
|
});
|
|
562
|
+
withContext(checklist.command('claim <id>').description('칸의 단일 주인을 잡는다'))
|
|
563
|
+
.requiredOption('--by <owner>', '잡을 자리 또는 하위 자리').option('--force', '기존 주인에서 강제로 바꾼다')
|
|
564
|
+
.action((id: string, opts: { by: string; force?: boolean }, cmd: Command) => {
|
|
565
|
+
const { version, json } = context(cmd);
|
|
566
|
+
const data = claimItem(version, id, opts.by, { force: opts.force });
|
|
567
|
+
if (json) console.log(JSON.stringify(data)); else console.log(`✅ ${id} 주인 ${opts.by}`);
|
|
568
|
+
});
|
|
525
569
|
withContext(checklist.command('rm <id>').description('칸 삭제')).action((id: string, _opts: unknown, cmd: Command) => {
|
|
526
570
|
const { version, json } = context(cmd);
|
|
527
571
|
const data = removeItem(version, id, process.env.ELANOUS_TRACK || 'cli');
|
|
@@ -533,6 +577,45 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
|
|
|
533
577
|
const data = seedFromRoadmap(version, readFileSync(opts.from, 'utf8'));
|
|
534
578
|
if (json) console.log(JSON.stringify(data)); else console.log(`✅ ${version} ${data.items.length}칸`);
|
|
535
579
|
});
|
|
580
|
+
const actor = () => process.env.ELANOUS_TRACK || 'cli';
|
|
581
|
+
withContext(checklist.command('move <id>').description('칸을 한 트랜잭션으로 다른 판에 옮긴다'))
|
|
582
|
+
.requiredOption('--from <v>', '현재 판').requiredOption('--to <v>', '새 판')
|
|
583
|
+
.action((id: string, opts: { from: string; to: string }, cmd: Command) => {
|
|
584
|
+
const { codenames, json } = context(cmd);
|
|
585
|
+
const from = checklistVersion(opts.from, codenames), to = checklistVersion(opts.to, codenames);
|
|
586
|
+
features.move(id, from, to, actor());
|
|
587
|
+
if (json) console.log(JSON.stringify(listChecklist(to))); else console.log(`✅ ${id} ${from} → ${to}`);
|
|
588
|
+
});
|
|
589
|
+
withContext(checklist.command('retitle <id> <title>').description('칸 제목 수정')).action((id: string, title: string, _opts: unknown, cmd: Command) => {
|
|
590
|
+
const { version, json } = context(cmd);
|
|
591
|
+
const snapshot = listChecklist(version);
|
|
592
|
+
features.retitle(id, title, actor(), snapshot.released, snapshot.dev);
|
|
593
|
+
if (json) console.log(JSON.stringify(listChecklist(version))); else console.log(`✅ ${id} 제목 수정`);
|
|
594
|
+
});
|
|
595
|
+
const evidence = checklist.command('evidence').description('칸의 PR·커밋 근거');
|
|
596
|
+
withContext(evidence.command('add <id> <ref>').description('근거 참조 추가')).action((id: string, ref: string, _opts: unknown, cmd: Command) => {
|
|
597
|
+
const { version, json } = context(cmd);
|
|
598
|
+
const snapshot = listChecklist(version);
|
|
599
|
+
features.evidenceAdd(id, version, ref, actor(), snapshot.released, snapshot.dev);
|
|
600
|
+
if (json) console.log(JSON.stringify(features.history(id).map(decodeClaimHistoryEntry))); else console.log(`✅ ${id} 근거 ${ref}`);
|
|
601
|
+
});
|
|
602
|
+
withContext(checklist.command('history <id>').description('칸의 판 이동·변경 이력')).action((id: string, _opts: unknown, cmd: Command) => {
|
|
603
|
+
const { json } = context(cmd);
|
|
604
|
+
const rows = features.history(id).map(decodeClaimHistoryEntry);
|
|
605
|
+
if (json) { console.log(JSON.stringify(rows)); return; }
|
|
606
|
+
const info = features.details(id);
|
|
607
|
+
if (info) {
|
|
608
|
+
console.log(`${info.id} · ${info.title} · 담당 ${info.owner ?? '-'} · 종류 ${info.kind ?? '-'} · 처음 ${info.createdAt}`);
|
|
609
|
+
for (const ev of info.evidence) console.log(` 근거 ${ev.version} ${ev.ref} (${ev.at} ${ev.by})`);
|
|
610
|
+
}
|
|
611
|
+
for (const row of rows) console.log(`${row.at} ${row.version} ${row.by} ${row.field}: ${JSON.stringify(row.from)} → ${JSON.stringify(row.to)}`);
|
|
612
|
+
});
|
|
613
|
+
withContext(checklist.command('export').description('SQLite 에서 checklist.json 스냅샷 생성')).action((_opts: unknown, cmd: Command) => {
|
|
614
|
+
const { version, json } = context(cmd);
|
|
615
|
+
const snapshot = listChecklist(version);
|
|
616
|
+
features.exportJson(version, snapshot.released, snapshot.dev);
|
|
617
|
+
if (json) console.log(JSON.stringify(listChecklist(version))); else console.log(`✅ ${version} checklist.json 내보냄`);
|
|
618
|
+
});
|
|
536
619
|
release.command('prepare')
|
|
537
620
|
.description('깨끗한 원본 → 공개본 → 공개 저장소 이력 위 커밋 → PWA → 묶음·체크섬 → 로컬 끝까지 (네트워크 쓰기 없음)')
|
|
538
621
|
.requiredOption('--version <x.y.z>', 'package.json 과 같은 버전(먼저 버전 PR 을 착지)')
|
package/src/cli/self-update.ts
CHANGED
|
@@ -343,6 +343,14 @@ function pruneReleaseVersions(plan: { current: string; previous: string; keep: n
|
|
|
343
343
|
return { removed, kept };
|
|
344
344
|
}
|
|
345
345
|
|
|
346
|
+
/** The build identity of an install.json — version, version folder and commit; empty when it cannot be read. */
|
|
347
|
+
function installedBuild(metadata: unknown): string {
|
|
348
|
+
if (typeof metadata !== 'object' || metadata === null) return '';
|
|
349
|
+
const { version, versionDir, commit } = metadata as { version?: unknown; versionDir?: unknown; commit?: unknown };
|
|
350
|
+
if (typeof version !== 'string' || !version || typeof versionDir !== 'string' || !versionDir) return '';
|
|
351
|
+
return JSON.stringify([version, versionDir, typeof commit === 'string' ? commit : '']);
|
|
352
|
+
}
|
|
353
|
+
|
|
346
354
|
/** A release update never packs the checkout: the downloaded installer verifies its own release tarball. */
|
|
347
355
|
export async function runReleaseUpdate(options: ReleaseUpdateOptions = {}, deps: ReleaseUpdateDeps = {}): Promise<SelfUpdateResult> {
|
|
348
356
|
const out = deps.out ?? console;
|
|
@@ -390,8 +398,10 @@ export async function runReleaseUpdate(options: ReleaseUpdateOptions = {}, deps:
|
|
|
390
398
|
return fail(1, `설치기 다운로드 실패 (${url}): ${String(error)}`);
|
|
391
399
|
}
|
|
392
400
|
let previous = '';
|
|
401
|
+
let previousBuild = '';
|
|
393
402
|
try {
|
|
394
403
|
const metadata: unknown = JSON.parse(readFileSync(join(prefix, 'install.json'), 'utf8'));
|
|
404
|
+
previousBuild = installedBuild(metadata);
|
|
395
405
|
if (typeof metadata === 'object' && metadata !== null && 'versionDir' in metadata && typeof metadata.versionDir === 'string') {
|
|
396
406
|
const dir = metadata.versionDir;
|
|
397
407
|
if (/^versions\/[a-zA-Z0-9._-]+$/.test(dir) && !dir.includes('..')) previous = basename(dir);
|
|
@@ -421,6 +431,10 @@ export async function runReleaseUpdate(options: ReleaseUpdateOptions = {}, deps:
|
|
|
421
431
|
prune = { removed: [], kept: [], skipped: `정리 실패: ${String(error)}` };
|
|
422
432
|
}
|
|
423
433
|
if (!options.restart) return finish({ exitCode: 0, installedVersion, decision: null, restarted: false, prune, reason: '--restart 없음: 재시작하지 않음' });
|
|
434
|
+
// REL6: the same build again (version · folder · commit unchanged) leaves the running daemon as it is.
|
|
435
|
+
if (previousBuild && previousBuild === installedBuild(metadata)) {
|
|
436
|
+
return finish({ exitCode: 0, installedVersion, decision: null, restarted: false, prune, reason: `already at ${installedVersion} · restart skipped` });
|
|
437
|
+
}
|
|
424
438
|
const os = deps.os ?? platform();
|
|
425
439
|
const command = os === 'darwin' ? 'launchctl' : os === 'linux' ? 'systemctl' : null;
|
|
426
440
|
if (!command) return finish({ exitCode: 1, installedVersion, decision: null, restarted: false, prune, reason: `지원하지 않는 플랫폼: ${os}` });
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
import { parse } from 'yaml';
|
|
4
|
+
import { debug } from '../debug/log.js';
|
|
5
|
+
import { files, lint, render, STORYBOARD_ROOT, type Storyboard } from '../storyboard/storyboard.js';
|
|
6
|
+
|
|
7
|
+
export function runStoryboard(cmd: 'lint' | 'render', args: string[], json = false): number {
|
|
8
|
+
const reports: Array<{ file: string; errors: string[]; warnings: string[] }> = [];
|
|
9
|
+
let errorCount = 0, warningCount = 0;
|
|
10
|
+
for (const f of files(args)) {
|
|
11
|
+
const file = relative(STORYBOARD_ROOT, f);
|
|
12
|
+
let sb: Storyboard | undefined;
|
|
13
|
+
let errors: string[], warnings: string[];
|
|
14
|
+
try {
|
|
15
|
+
sb = parse(readFileSync(f, 'utf8')) as Storyboard;
|
|
16
|
+
({ errors, warnings } = lint(sb, file));
|
|
17
|
+
} catch (cause) {
|
|
18
|
+
errors = [cause instanceof Error ? cause.message : String(cause)];
|
|
19
|
+
warnings = [];
|
|
20
|
+
}
|
|
21
|
+
reports.push({ file, errors, warnings });
|
|
22
|
+
errorCount += errors.length;
|
|
23
|
+
warningCount += warnings.length;
|
|
24
|
+
if (json) continue;
|
|
25
|
+
for (const e of errors) console.log(`✗ ${file}: ${e}`);
|
|
26
|
+
for (const w of warnings) console.log(`⚠ ${file}: ${w}`);
|
|
27
|
+
if (errors.length || !sb) continue;
|
|
28
|
+
if (cmd === 'render') {
|
|
29
|
+
const out = join(STORYBOARD_ROOT, 'docs', 'marketing', `STORYBOARD-${sb.id}-v${sb.version}.md`);
|
|
30
|
+
writeFileSync(out, render(sb, file) + '\n');
|
|
31
|
+
console.log(`✓ ${file} → ${relative(STORYBOARD_ROOT, out)}`);
|
|
32
|
+
} else console.log(`✓ ${file} · v${sb.version} · ${sb.status} · ${sb.kind === 'site' ? `사이트 ${sb.sites?.length ?? 0}` : `샷 ${sb.shots.length}`}`);
|
|
33
|
+
}
|
|
34
|
+
if (cmd === 'lint') debug.log('storyboard.lint', 'checked', { files: reports.length, errorCount, warningCount });
|
|
35
|
+
if (json) console.log(JSON.stringify({ files: reports, errorCount, warningCount }));
|
|
36
|
+
return errorCount ? 1 : 0;
|
|
37
|
+
}
|
|
@@ -71,7 +71,8 @@ export function applyTestStateDirFlagFromArgv(): string | undefined {
|
|
|
71
71
|
* 뒤에만 있어 사람이 부를 입구가 없었고, 그래서 문서 174곳에 수동 주문이 화석으로 남았다.
|
|
72
72
|
*
|
|
73
73
|
* 하는 일: nexus state root ⊕ ELANOUS_STATE_DIR ⊕ config-dir 을 **한 뿌리로** 세우고,
|
|
74
|
-
* config
|
|
74
|
+
* 운영 config 가 있으면 test-safe 사본을 물질화하고, 없으면 격리 기본값을 사용하며,
|
|
75
|
+
* config 경로가 test 루트 밖이면 **기동을 거부**한다. */
|
|
75
76
|
export function applyIsolatedRoot(dir: string): void {
|
|
76
77
|
const { setTestStateRoot } = require('../nexus/paths.js') as typeof import('../nexus/paths.js');
|
|
77
78
|
setTestStateRoot(dir);
|
|
@@ -98,7 +99,7 @@ export function applyIsolatedRoot(dir: string): void {
|
|
|
98
99
|
}
|
|
99
100
|
cfgDir.setElanousConfigDir(dir);
|
|
100
101
|
}
|
|
101
|
-
// config
|
|
102
|
+
// config 사본이 없고 운영 원본이 있으면 물질화하고, 있으면 drift 경고만
|
|
102
103
|
// (자동 덮어쓰기 금지 — 명시적 sync 원칙). 유닛 테스트(NODE_ENV=test)는
|
|
103
104
|
// 실 운영 config/secrets 를 temp 로 복사하지 않도록 sync 스킵.
|
|
104
105
|
if (process.env.NODE_ENV !== 'test') {
|
|
@@ -106,8 +107,14 @@ export function applyIsolatedRoot(dir: string): void {
|
|
|
106
107
|
const { existsSync } = require('node:fs') as typeof import('node:fs');
|
|
107
108
|
const { join } = require('node:path') as typeof import('node:path');
|
|
108
109
|
if (!existsSync(join(dir, 'config.json'))) {
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
// A fresh build/test environment may have no operating config to copy.
|
|
111
|
+
// Keep the isolated config root and use the ordinary safe defaults there.
|
|
112
|
+
if (existsSync(join(sync.prodConfigDir(), 'config.json'))) {
|
|
113
|
+
const r = sync.syncTestConfig(dir);
|
|
114
|
+
console.error(`[test-isolation] 운영 config 물질화 → ${r.testConfigPath} (telegram=${r.telegramMode})`);
|
|
115
|
+
} else {
|
|
116
|
+
console.error(`[test-isolation] 운영 config 없음 — ${dir} 의 기본 설정 사용`);
|
|
117
|
+
}
|
|
111
118
|
} else if (sync.isTestConfigStale(dir)) {
|
|
112
119
|
console.error(`[test-isolation] ⚠️ 운영 config 가 테스트 사본보다 최신 — 'elanous config sync-test' 로 갱신 권장`);
|
|
113
120
|
}
|