elanous 0.2.15 → 0.2.16
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/chunks/{1307-33ffcc49ab45123b.js → 1307-b46d45b705e8c9ae.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{9103-7a3ae7f60af3b99f.js → 9103-60fbef33e66f0a46.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-e05a77df7e005b04.js → page-3cb52e5ea8fc2fa5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-5491c115b1f9a923.js → page-c1e20d7d338713f1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-b8c49f6b4fa2df91.js → page-482c5dbde699947c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-1e962b31d1f11081.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-976813c91d7394ea.js → page-8c2d9526164f5d09.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-a0e2a6d1bfb52fc5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-72d3e7ff35a8c954.js → page-c53598cfce6156f2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-60c7a8e1a4fe62ce.js → page-13a63953f53c6a23.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-2a52b316cb143d9c.js → page-5d3c8429d04f3831.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-c3ce483c8d5db370.js → page-3193bdc0bbab047a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-76270efaee638f17.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-96f074f6538e8a2c.js → page-6107ca8ddd5e3bdd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{layout-e97be5334d83a9f5.js → layout-dce686e582c23937.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-d3d53b146416e422.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-1a61c7ca1ceb3ae7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-955ec2da3be4c560.js → page-51f811fa119308b6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-9f88fbc15d9058d1.js → page-dacff46049e1557e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-c1343f9499d86f3f.js → page-4167348049166852.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-8739a2e0ae793130.js → not-found-de52e8fdf9095f7a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/{page-c9fc5706b8abaa11.js → page-36a7d994f4ca0299.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-d23b12bb92242558.js → page-ca3952cd78a83ca2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-5b042736aa113756.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-c7fea475488201a8.js → page-acb749f1e432bcf3.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-5efbf81124c474ae.js → page-1ebfe049c640e171.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-85f1a1fc0a82738c.js → page-57924393e3e4e539.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-06ee266953d9c879.js → page-31fa1ce4c4aff9ae.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-153c2b3bcc89e8de.js → page-0f5cd57ca928f42b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-abe8e3b74bea1ffc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/{page-0d6d23b40263a7ea.js → page-d28573d6792b4275.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-59f03fda76c03d22.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-d751de5f8062ff5e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/{page-f5597e894c02cd33.js → page-fe1759f94003b32b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/{page-0337fdf7fb5e37b6.js → page-d64c984e855c8c68.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-8fcec4c9ea2e7f70.js → page-9afdee66b820c76e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-f7e631e9b4daf66e.js → page-d4aef67abf264874.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-1ea64acadd704ace.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-9817c7f9007466cc.js → page-3544f154c87b43ee.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-30c2073bfc21c004.js +1 -0
- package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +1 -0
- package/apps/pwa/out/_next/static/css/{04dabcbcc4bfa6db.css → 085673d826b98b62.css} +1 -1
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +15 -15
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +15 -15
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +15 -15
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +15 -15
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +15 -15
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +15 -15
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +15 -15
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +15 -15
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +15 -15
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +15 -15
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +15 -15
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +15 -15
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +15 -15
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +15 -15
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +15 -15
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +15 -15
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +15 -15
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +15 -15
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +15 -15
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +15 -15
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +15 -15
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +15 -15
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +15 -15
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +15 -15
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +15 -15
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +15 -15
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +16 -16
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +15 -15
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +15 -15
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +15 -15
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +15 -15
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +15 -15
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +15 -15
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +15 -15
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +15 -15
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +15 -15
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +15 -15
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +15 -15
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +15 -15
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +15 -15
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +15 -15
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +15 -15
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +15 -15
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +15 -15
- package/graphs/doc-gen/doc-gen.yaml +24 -0
- package/graphs/doc-gen/recipes.yaml +3 -0
- package/graphs/orchestrator/orchestrator.yaml +7 -0
- package/graphs/orchestrator/recipes.yaml +3 -0
- package/graphs/steward/recipes.yaml +2 -1
- package/package.json +1 -1
- package/scripts/ci-public-export-import-gate.ts +154 -0
- package/scripts/ci-test-interference-gate.ts +5 -1
- package/scripts/ci-typecheck-changed.ts +34 -15
- package/scripts/docs/doc-rot.ts +164 -0
- package/scripts/docs/generate-graph-anatomy.ts +131 -9
- package/scripts/docs-cli-check.ts +4 -4
- package/scripts/goals/goal-census.ts +343 -0
- package/scripts/measure/author-section-contribution.ts +84 -0
- package/scripts/release-loop/cut-branch.ts +43 -13
- package/scripts/release-loop/gate-node.ts +3 -1
- package/scripts/release-loop/vault-note-node.ts +19 -2
- package/scripts/rhythm/monthly.ts +97 -0
- package/src/acp/client.ts +17 -2
- package/src/agent-mission/driver.ts +51 -1
- package/src/agent-mission/supervisor-metrics-cli.ts +40 -0
- package/src/agent-mission/supervisor-metrics.ts +218 -0
- package/src/autopilot/track-agent-shadow.ts +2 -1
- package/src/briefing/brief-items.ts +191 -36
- package/src/budget/usage-outcomes.ts +204 -0
- package/src/cli/brief-cli.ts +73 -0
- package/src/cli/know-cli.ts +20 -0
- package/src/cli/logs-cli.ts +43 -1
- package/src/cli/pr-cli.ts +80 -12
- package/src/cli/release-cli.ts +11 -8
- package/src/cli/self-update.ts +200 -1
- package/src/cli/usage-cli.ts +26 -0
- package/src/discord-self-message.ts +9 -0
- package/src/flow-loop/split.ts +24 -0
- package/src/harness/harness-cli-command.ts +36 -2
- package/src/harness/harness-pod-dispatch.ts +25 -55
- package/src/harness/harness-seams.ts +13 -3
- package/src/harness/helper-repair.ts +139 -8
- package/src/harness/helper-scan.ts +13 -2
- package/src/harness/nested-elanous-policy.ts +114 -0
- package/src/hq/hq.ts +8 -2
- package/src/hq/lease.ts +5 -3
- package/src/hq/transfer.ts +185 -0
- package/src/index.ts +112 -6
- package/src/intake-plane/digest.ts +48 -26
- package/src/knowledge/know-find.ts +105 -0
- package/src/llm.ts +75 -19
- package/src/loops/orchestrator/finish-rate.ts +14 -3
- package/src/loops/orchestrator/tick.ts +96 -25
- package/src/maturity/bot-slash-catalog.ts +11 -2
- package/src/maturity/feature-maturity.ts +2 -2
- package/src/maturity/tui-slash-availability.ts +9 -0
- package/src/oauth/overload-failover-launch.ts +75 -0
- package/src/oauth/overload-failover.ts +137 -0
- package/src/release-loop/checklist.ts +10 -5
- package/src/release-loop/feature-store.ts +10 -9
- package/src/release-loop/placement.ts +17 -8
- package/src/seat-dispatch/seat-ask.ts +33 -6
- package/src/seat-loop/seat-loop.ts +90 -3
- package/src/self-dev/ask-launch-flow.ts +22 -1
- package/src/self-dev/authored-goal-trace.ts +64 -0
- package/src/self-dev/dev-cli.ts +19 -1
- package/src/self-dev/dev-pipeline.ts +6 -5
- package/src/self-dev/entrance-registry.ts +10 -6
- package/src/self-dev/old-door.ts +73 -0
- package/src/self-implement/app-api-budget.ts +166 -0
- package/src/self-implement/budget-gate.ts +7 -5
- package/src/self-implement/follow-up-goals.ts +289 -0
- package/src/self-implement/goal-author.ts +19 -13
- package/src/self-implement/heal-triage.ts +44 -0
- package/src/self-implement/orchestrator.ts +138 -7
- package/src/self-implement/run-ledger.ts +4 -0
- package/src/self-implement/seams.ts +44 -15
- package/src/session-runtime/index.ts +2 -0
- package/src/session-runtime/retry-api.ts +87 -0
- package/src/session-runtime/retry-policy.ts +107 -6
- package/src/steward/working-backwards.ts +16 -1
- package/src/task-cards/card-cli.ts +25 -0
- package/src/task-cards/card-store.ts +5 -3
- package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +4 -3
- package/src/task-orchestrator/surfaces/pod-ledger-prod-sink.ts +75 -0
- package/src/task-orchestrator/surfaces/pod-pool.ts +14 -5
- package/src/task-orchestrator/surfaces/pod-source-receive.ts +37 -1
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +19 -10
- package/src/task-orchestrator/surfaces/self-implement.ts +2 -0
- package/src/telegram-commands.ts +5 -0
- package/src/telegram.ts +47 -0
- package/src/user-config.ts +36 -3
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-0e2c57fe6add4b2a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-28b9448091776c06.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-1d4901eb7c2d8822.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-e2ee2896811e98d0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-da6c08aae0f92768.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-87a11e6a05f15199.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-7d4182964407c09c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-73735b64bfe1ae67.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-97f9f43172a3a90e.js +0 -1
- package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.css +0 -1
- package/src/launch-head/seat-goal-queue-lock.fixture.ts +0 -15
- package/src/launch-head/seat-goal-queue.ts +0 -198
- /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → rv9cierV0jSOvDBm1g0XB}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → rv9cierV0jSOvDBm1g0XB}/_ssgManifest.js +0 -0
package/src/cli/logs-cli.ts
CHANGED
|
@@ -715,7 +715,7 @@ export function podObservationGapNotice(
|
|
|
715
715
|
.some((prefix) => category.startsWith(prefix));
|
|
716
716
|
const categories = [...(query.categories ?? []), ...(query.exactCategories ?? [])];
|
|
717
717
|
if (!categories.some(childCategory) && !(categories.length === 0 && (query.events?.length ?? 0) > 0)) return null;
|
|
718
|
-
return `안내: 이 창에 Pod 에서 돈 런 ${podRuns}개 — Pod 자식의 관측은
|
|
718
|
+
return `안내: 이 창에 Pod 에서 돈 런 ${podRuns}개 — Pod 자식의 관측은 «발사한 트리의 파생 우주» logs.db 로 회수된다(POD-OBS) — 전체: elanous logs --all --include-test · 런 하나: elanous self run-ledger <runId>`;
|
|
719
719
|
}
|
|
720
720
|
|
|
721
721
|
/** Select ledger roots independently of DB existence; --all includes registered roots with no logs.db. */
|
|
@@ -1038,6 +1038,24 @@ export function otherInstanceHint(matches: readonly { name: string; count: numbe
|
|
|
1038
1038
|
return ` ↳ 다른 인스턴스에는 있다 — ${found}\n 전체를 보려면: elanous logs --all --include-test${filters ? ` ${filters}` : ''}`;
|
|
1039
1039
|
}
|
|
1040
1040
|
|
|
1041
|
+
/** POD-OBS(10-06): 결과가 «0이 아닌데 적을 때»도 조용하지 않게 — 다른 우주에 더 많으면 경고한다.
|
|
1042
|
+
* 실측: 24h `pre-pr-sync` 운영 1건 vs 발사 트리 파생 우주 276건(Pod 자식 재방출은 부모 하니스의 우주에 쌓인다). */
|
|
1043
|
+
export function otherInstanceMajorityWarning(
|
|
1044
|
+
shown: number,
|
|
1045
|
+
matches: readonly { name: string; count: number }[],
|
|
1046
|
+
opts: LogsCliOpts,
|
|
1047
|
+
): string | null {
|
|
1048
|
+
const elsewhere = matches.reduce((sum, match) => sum + match.count, 0);
|
|
1049
|
+
if (shown <= 0 || elsewhere <= shown) return null;
|
|
1050
|
+
const top = [...matches].sort((a, b) => b.count - a.count);
|
|
1051
|
+
const named = top.slice(0, 3).map((match) => `${match.name} ${match.count}건`).join(' · ');
|
|
1052
|
+
const more = top.length > 3 ? ` 외 ${top.length - 3}곳` : '';
|
|
1053
|
+
const filters = requeryFilterArgs(opts);
|
|
1054
|
+
return `⚠ 여기 ${shown}건 · 다른 우주에 ${elsewhere}건이 더 있다 — ${named}${more}\n`
|
|
1055
|
+
+ ' (Pod 자식 관측은 발사한 트리의 파생 우주에 쌓인다 · 이 수만 보고 «적다»로 읽지 말 것)\n'
|
|
1056
|
+
+ ` 전체를 보려면: elanous logs --all --include-test${filters ? ` ${filters}` : ''}`;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1041
1059
|
export function buildQuery(opts: LogsCliOpts): { query: LogQuery; error?: string } {
|
|
1042
1060
|
const q: LogQuery = {};
|
|
1043
1061
|
if (opts.level) {
|
|
@@ -1789,6 +1807,30 @@ export async function runLogsCli(opts: LogsCliOpts, deps: RunLogsCliDeps = {}):
|
|
|
1789
1807
|
}
|
|
1790
1808
|
const coverageHint = runCoverageHint(out.map(({ row }) => row));
|
|
1791
1809
|
if (coverageHint) console.error(coverageHint);
|
|
1810
|
+
// 좁힌 조회(이벤트·카테고리)만 — 넓은 조회까지 매번 전 우주를 열지 않는다.
|
|
1811
|
+
if (out.length > 0 && ((query.events?.length ?? 0) > 0 || (query.categories?.length ?? 0) > 0
|
|
1812
|
+
|| (query.exactCategories?.length ?? 0) > 0)) {
|
|
1813
|
+
const matches = probeOtherInstanceMatches(opts, query, resolved.targets.map((target) => target.dbPath));
|
|
1814
|
+
// 표시 상한(--limit)이 아니라 «여기» 실제 일치 수와 비교한다(상한 100 을 «100건»으로 말하지 않는다).
|
|
1815
|
+
let here: number | null = null;
|
|
1816
|
+
try {
|
|
1817
|
+
here = Math.max(out.length, (deps.countMatching ?? ((candidate: LogQuery) => opened.reduce(
|
|
1818
|
+
(count, { store }) => count + store.countMatching(candidate), 0)))(query));
|
|
1819
|
+
} catch (error) {
|
|
1820
|
+
// 셈을 못 했으면 경고하지 않는다 — 잘린 표시 수를 «여기 N건»으로 말하지 않는다.
|
|
1821
|
+
debug.log('logs.cli', 'other-instance-count-failed', { shown: out.length, error: error instanceof Error ? error.message : String(error) });
|
|
1822
|
+
}
|
|
1823
|
+
const majority = here === null ? null : otherInstanceMajorityWarning(here, matches, opts);
|
|
1824
|
+
if (majority) {
|
|
1825
|
+
console.error(majority);
|
|
1826
|
+
debug.log('logs.cli', 'other-instance-majority', {
|
|
1827
|
+
shown: out.length,
|
|
1828
|
+
here,
|
|
1829
|
+
elsewhere: matches.reduce((sum, match) => sum + match.count, 0),
|
|
1830
|
+
instances: matches.length,
|
|
1831
|
+
});
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1792
1834
|
if (out.length === 0) {
|
|
1793
1835
|
console.error('(일치하는 로그 없음)');
|
|
1794
1836
|
const instanceHint = otherInstanceHint(
|
package/src/cli/pr-cli.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
2
2
|
import { basename, join, relative, resolve } from 'node:path';
|
|
3
3
|
import { createInterface } from 'node:readline';
|
|
4
4
|
import type { Command } from 'commander';
|
|
@@ -19,6 +19,7 @@ import { runModelHardcodeGate } from '../../scripts/ci-model-hardcode-gate.js';
|
|
|
19
19
|
import { runDaemonPortGate } from '../../scripts/ci-daemon-port-gate.js';
|
|
20
20
|
import { runPublicLeakGate } from '../../scripts/ci-public-leak-gate.js';
|
|
21
21
|
import { exportLeakCheck, publicExportChangedFiles, type ExportLeakCheck } from '../../scripts/ci-public-export-leak-gate.js';
|
|
22
|
+
import { exportImportCheck, type ExportImportCheck } from '../../scripts/ci-public-export-import-gate.js';
|
|
22
23
|
import { checkCommands, extractElanousCommands, type Finding as DocsCliFinding } from '../../scripts/docs-cli-check.js';
|
|
23
24
|
import { runTestInterferenceGate } from '../../scripts/ci-test-interference-gate.js';
|
|
24
25
|
import { isGoalDocumentFileName } from '../self-implement/goal-document.js';
|
|
@@ -111,6 +112,8 @@ export interface PrLandDeps {
|
|
|
111
112
|
runPublicLeakGate?: (changedFiles: readonly string[], out: { log: (message: string) => void; error: (message: string) => void }) => number;
|
|
112
113
|
/** Test seam — the post-transform export leak check on changed files. */
|
|
113
114
|
runExportLeakCheck?: (changedFiles: readonly string[], cwd: string) => ExportLeakCheck;
|
|
115
|
+
/** Test seam — included files must not import a module the public export excludes. */
|
|
116
|
+
runExportImportCheck?: (changedFiles: readonly string[], cwd: string) => ExportImportCheck | Promise<ExportImportCheck>;
|
|
114
117
|
/** 공개 문서의 `elanous …` 호출 ↔ 실제 `--help` 대조(경고 전용) — 바뀐 공개 문서 경로를 받아 어긋남 목록을 돌려준다. */
|
|
115
118
|
runDocsCliCheck?: (docFiles: readonly string[]) => DocsCliFinding[];
|
|
116
119
|
/** 변경 시험 파일 간 간섭 검사 심(시험 주입용). */
|
|
@@ -231,14 +234,14 @@ export async function decideOverlapLanding(input: {
|
|
|
231
234
|
|
|
232
235
|
const liveOut = { log: (message: string) => console.log(message), error: (message: string) => console.error(message) };
|
|
233
236
|
|
|
234
|
-
function runPrLandTypecheckGate(out: { log: (message: string) => void; error: (message: string) => void }): boolean {
|
|
237
|
+
function runPrLandTypecheckGate(out: { log: (message: string) => void; error: (message: string) => void }, cwd: string = process.cwd()): boolean {
|
|
235
238
|
let failed = false;
|
|
236
239
|
const exit = ((code: number): never => {
|
|
237
240
|
failed = code !== 0;
|
|
238
241
|
throw new Error(`ci-typecheck-changed exited ${code}`);
|
|
239
242
|
});
|
|
240
243
|
try {
|
|
241
|
-
runChangedTypecheckGate({ log: out.log, warn: out.error, error: out.error, exit });
|
|
244
|
+
runChangedTypecheckGate({ log: out.log, warn: out.error, error: out.error, exit, cwd });
|
|
242
245
|
} catch (error) {
|
|
243
246
|
if (!failed) throw error;
|
|
244
247
|
}
|
|
@@ -290,7 +293,67 @@ export function runPrLandExportLeakCheck(changedFiles: readonly string[], cwd: s
|
|
|
290
293
|
return exportLeakCheck(changedFiles, top.status === 0 && String(top.stdout ?? '').trim() ? String(top.stdout).trim() : cwd);
|
|
291
294
|
}
|
|
292
295
|
|
|
296
|
+
/** Included files importing an excluded module — scripts/ci-public-export-import-gate.ts. cwd is the tree being landed. */
|
|
297
|
+
export function runPrLandExportImportCheck(changedFiles: readonly string[], cwd: string): Promise<ExportImportCheck> {
|
|
298
|
+
const top = runGitCommand(cwd, ['rev-parse', '--show-toplevel'], { encoding: 'utf-8' });
|
|
299
|
+
return exportImportCheck(changedFiles, top.status === 0 && String(top.stdout ?? '').trim() ? String(top.stdout).trim() : cwd);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Blocks the landing when a changed public file imports a module the public export excludes. */
|
|
303
|
+
export async function exportImportBlock(
|
|
304
|
+
changedFiles: readonly string[] | undefined,
|
|
305
|
+
check: () => ExportImportCheck | Promise<ExportImportCheck>,
|
|
306
|
+
out: { log: (message: string) => void; error: (message: string) => void },
|
|
307
|
+
): Promise<boolean> {
|
|
308
|
+
if (!changedFiles || changedFiles.length === 0) return true;
|
|
309
|
+
let result: ExportImportCheck;
|
|
310
|
+
try { result = await check(); } catch { result = { measured: false, hits: [], unseen: 0, detail: '검사 실행 실패' }; }
|
|
311
|
+
const blocked = !result.measured || result.hits.length > 0;
|
|
312
|
+
record('public-export-import', !blocked, { measured: result.measured, hits: result.hits.length, unseen: result.unseen });
|
|
313
|
+
debug.log('pr.land', 'import-gate', { blocked, hits: result.hits.map(({ file, target }) => ({ file, target })), measured: result.measured, unseen: result.unseen });
|
|
314
|
+
if (!result.measured) {
|
|
315
|
+
out.error(`✗ public-export-import: 못 쟀다 — 막는다(${result.detail ?? '?'})`);
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
if (result.unseen > 0) out.log(`⚠ public-export-import: 비리터럴 지정자 ${result.unseen}곳은 못 봄`);
|
|
319
|
+
if (result.hits.length === 0) {
|
|
320
|
+
out.log('✓ public-export-import: 바뀐 공개 파일이 공개 내보내기에서 제외된 모듈을 import 하지 않는다.');
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
out.error(`✗ public-export-import: 공개 파일이 공개 내보내기에서 제외된 모듈을 import 한다 (${result.hits.length}곳) — 공개본 빌드가 여기서 깨진다:`);
|
|
324
|
+
for (const hit of result.hits.slice(0, 20)) out.error(` ${hit.file}:${hit.line} → ${hit.target}`);
|
|
325
|
+
if (result.hits.length > 20) out.error(` … 외 ${result.hits.length - 20}곳`);
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
|
|
293
329
|
/** Blocks changed public files that leak or cannot be measured. */
|
|
330
|
+
/**
|
|
331
|
+
* MARKER-GUARD — a changed file that still holds merge-conflict markers is refused before landing.
|
|
332
|
+
* A file counts only when it has both a `<<<<<<< ` and a `>>>>>>> ` line (`=======` alone is a Markdown underline).
|
|
333
|
+
* 10-06 02:4x: a hand landing put three marker lines into release/next.md on main (hotfix #24333).
|
|
334
|
+
*/
|
|
335
|
+
export function conflictMarkerFiles(changedFiles: readonly string[] | undefined, cwd: string, read: (path: string) => string | undefined = (path) => {
|
|
336
|
+
// No size cutoff: a large file with markers must still be refused (ACP must-fix). A deleted file reads as undefined.
|
|
337
|
+
try { const full = resolve(cwd, path); return existsSync(full) && statSync(full).isFile() ? readFileSync(full, 'utf8') : undefined; }
|
|
338
|
+
catch { return undefined; }
|
|
339
|
+
}): string[] {
|
|
340
|
+
return (changedFiles ?? []).filter((path) => {
|
|
341
|
+
const text = read(path);
|
|
342
|
+
return text !== undefined && /^<{7} /m.test(text) && /^>{7} /m.test(text);
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export function conflictMarkerBlock(changedFiles: readonly string[] | undefined, cwd: string,
|
|
347
|
+
out: { log: (message: string) => void; error: (message: string) => void }, read?: (path: string) => string | undefined): boolean {
|
|
348
|
+
const hits = read ? conflictMarkerFiles(changedFiles, cwd, read) : conflictMarkerFiles(changedFiles, cwd);
|
|
349
|
+
if (hits.length) {
|
|
350
|
+
debug.log('pr.land', 'conflict-markers-blocked', { files: hits });
|
|
351
|
+
out.error(`✗ conflict-markers: 충돌 표시(<<<<<<< · >>>>>>>)가 남은 파일 ${hits.length}개 — ${hits.join(', ')} · 표시를 풀고 다시 착지하라`);
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
|
|
294
357
|
export function exportLeakBlock(
|
|
295
358
|
changedFiles: readonly string[] | undefined,
|
|
296
359
|
check: () => ExportLeakCheck,
|
|
@@ -401,8 +464,10 @@ function runPrLandDocsCliCheck(root: string): (docFiles: readonly string[]) => D
|
|
|
401
464
|
return (docFiles) => checkCommands(docFiles.flatMap((f) => extractElanousCommands(f, readFileSync(join(root, f), 'utf8'))));
|
|
402
465
|
}
|
|
403
466
|
|
|
404
|
-
function runPrLandMockModuleRestoreGate(out: { log: (message: string) => void; error: (message: string) => void }): boolean {
|
|
405
|
-
|
|
467
|
+
function runPrLandMockModuleRestoreGate(out: { log: (message: string) => void; error: (message: string) => void }, cwd: string = process.cwd()): boolean {
|
|
468
|
+
const top = runGitCommand(cwd, ['rev-parse', '--show-toplevel'], { encoding: 'utf-8', env: { ...process.env, GIT_DIR: undefined, GIT_WORK_TREE: undefined, GIT_COMMON_DIR: undefined } as NodeJS.ProcessEnv });
|
|
469
|
+
const root = top.status === 0 && String(top.stdout ?? '').trim() ? String(top.stdout).trim() : cwd;
|
|
470
|
+
return runMockModuleRestoreGate({ args: [], cwd: root, log: out.log, error: out.error }) === 0;
|
|
406
471
|
}
|
|
407
472
|
|
|
408
473
|
// 🚨 안드로이드 축 — `bun test` 우주 «밖»이라 다른 게이트가 원리상 못 본다.
|
|
@@ -415,8 +480,9 @@ function runPrLandMockModuleRestoreGate(out: { log: (message: string) => void; e
|
|
|
415
480
|
function runPrLandAndroidGate(
|
|
416
481
|
out: { log: (message: string) => void; error: (message: string) => void },
|
|
417
482
|
changedFiles: readonly string[] = [],
|
|
483
|
+
cwd: string = process.cwd(),
|
|
418
484
|
): boolean {
|
|
419
|
-
return runAndroidUnitTestGate({ args: ['--changed-files', ...changedFiles], log: out.log, error: out.error }) === 0;
|
|
485
|
+
return runAndroidUnitTestGate({ args: ['--changed-files', ...changedFiles], cwd, log: out.log, error: out.error }) === 0;
|
|
420
486
|
}
|
|
421
487
|
|
|
422
488
|
/** ⛔ 보호는 «호출부»에 둔다 — 주입된 게이트가 던져도 같은 계약이 서야 한다.
|
|
@@ -1320,7 +1386,7 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
|
|
|
1320
1386
|
}
|
|
1321
1387
|
}
|
|
1322
1388
|
|
|
1323
|
-
const typecheckOk = (deps.runTypecheckGate ?? runPrLandTypecheckGate)(out);
|
|
1389
|
+
const typecheckOk = (deps.runTypecheckGate ?? ((o) => runPrLandTypecheckGate(o, cwd)))(out);
|
|
1324
1390
|
record('typecheck', typecheckOk);
|
|
1325
1391
|
if (!typecheckOk) {
|
|
1326
1392
|
// ⛔⭐ 「왜 막혔나」를 «단정하지 않는다» — 이 게이트는 불리언만 돌려주므로 여기서는 이유를 모른다.
|
|
@@ -1347,7 +1413,7 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
|
|
|
1347
1413
|
out.log('✓ isolation-gate: scripts/ci-isolation-hardcode-gate.ts PASS — no new homedir+.elanous hardcoding.');
|
|
1348
1414
|
}
|
|
1349
1415
|
|
|
1350
|
-
const mockModuleRestore = gateVerdict('mock-module-restore-gate', deps.runMockModuleRestoreGate ?? runPrLandMockModuleRestoreGate, out);
|
|
1416
|
+
const mockModuleRestore = gateVerdict('mock-module-restore-gate', deps.runMockModuleRestoreGate ?? ((o) => runPrLandMockModuleRestoreGate(o, cwd)), out);
|
|
1351
1417
|
record('mock-module-restore-gate', mockModuleRestore.ok, { measured: mockModuleRestore.measured });
|
|
1352
1418
|
if (!mockModuleRestore.ok) {
|
|
1353
1419
|
out.error('✗ mock-module-restore-gate: scripts/ci-mock-module-restore-gate.ts blocked pr land because changed tests introduce mock.module without R-TST23 restoration.');
|
|
@@ -1361,7 +1427,7 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
|
|
|
1361
1427
|
// 근본은 훈계가 아니라 ***분포***였다: 스크립트에 박힌 이름 중 «가장 흔한» 것이 ***낡은 것***이라
|
|
1362
1428
|
// 옆 파일을 베끼면 낡은 이름을 물려받는다. ⇒ 이 자가 «신규»만 막는다(ratchet).
|
|
1363
1429
|
// ⚠️ `.github/workflows` 가 없어 `pr land` 가 «유일한» 강제점이다.
|
|
1364
|
-
const modelHardcode = gateVerdict('model-hardcode-gate', deps.runModelHardcodeGate ?? ((o) => runModelHardcodeGate({ log: o.log, error: o.error, args: [] }) === 0), out);
|
|
1430
|
+
const modelHardcode = gateVerdict('model-hardcode-gate', deps.runModelHardcodeGate ?? ((o) => runModelHardcodeGate({ log: o.log, error: o.error, args: [], cwd }) === 0), out);
|
|
1365
1431
|
record('model-hardcode-gate', modelHardcode.ok, { measured: modelHardcode.measured });
|
|
1366
1432
|
if (!modelHardcode.ok) {
|
|
1367
1433
|
out.error('✗ model-hardcode-gate: scripts/ci-model-hardcode-gate.ts blocked pr land because changed files hardcode a model id — derive it from tierModel()/config instead.');
|
|
@@ -1382,15 +1448,17 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
|
|
|
1382
1448
|
|
|
1383
1449
|
const changedPaths = currentChangePaths(run, cwd, staged, base, out);
|
|
1384
1450
|
nextMdWarning(body ?? '', changedPaths, out);
|
|
1451
|
+
if (!conflictMarkerBlock(changedPaths, cwd, out)) return 1;
|
|
1385
1452
|
publicLeakWarning(changedPaths, deps.runPublicLeakGate ?? ((files, sink) => runPrLandPublicLeakGate(files, sink, cwd, base)), out);
|
|
1386
1453
|
if (!exportLeakBlock(changedPaths, () => (deps.runExportLeakCheck ?? runPrLandExportLeakCheck)(changedPaths ?? [], cwd), opts.allowPublicLeak === true, out, cwd)) return 1;
|
|
1454
|
+
if (!await exportImportBlock(changedPaths, () => (deps.runExportImportCheck ?? runPrLandExportImportCheck)(changedPaths ?? [], cwd), out)) return 1;
|
|
1387
1455
|
if (publicDocPaths(changedPaths, () => true).length > 0) {
|
|
1388
1456
|
const top = run('git', ['rev-parse', '--show-toplevel'], { cwd });
|
|
1389
1457
|
const docsRoot = top.ok && top.out.trim() ? top.out.trim() : cwd;
|
|
1390
1458
|
docsCliWarning(publicDocPaths(changedPaths, (f) => existsSync(join(docsRoot, f))), deps.runDocsCliCheck ?? runPrLandDocsCliCheck(docsRoot), out);
|
|
1391
1459
|
}
|
|
1392
1460
|
const testInterference = await testInterferenceGateVerdict(
|
|
1393
|
-
deps.runTestInterferenceGate ?? ((o, files) => runTestInterferenceGate({ args: ['--changed-files', ...files], log: o.log })),
|
|
1461
|
+
deps.runTestInterferenceGate ?? ((o, files) => runTestInterferenceGate({ args: ['--changed-files', ...files], log: o.log, cwd })),
|
|
1394
1462
|
changedPaths,
|
|
1395
1463
|
out,
|
|
1396
1464
|
);
|
|
@@ -1400,7 +1468,7 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
|
|
|
1400
1468
|
'android-gate',
|
|
1401
1469
|
deps.runAndroidGate
|
|
1402
1470
|
? (o) => deps.runAndroidGate!(o, changedPaths)
|
|
1403
|
-
: (o) => runPrLandAndroidGate(o, changedPaths),
|
|
1471
|
+
: (o) => runPrLandAndroidGate(o, changedPaths, cwd),
|
|
1404
1472
|
out,
|
|
1405
1473
|
);
|
|
1406
1474
|
record('android-gate', androidGate.ok, { measured: androidGate.measured });
|
|
@@ -1418,7 +1486,7 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
|
|
|
1418
1486
|
'ios-gate',
|
|
1419
1487
|
deps.runIosGate
|
|
1420
1488
|
? (o) => deps.runIosGate!(o, changedPaths)
|
|
1421
|
-
: (o) => runIosUnitTestGate({ args: ['--changed-files', ...changedPaths], log: o.log, error: o.error }) === 0,
|
|
1489
|
+
: (o) => runIosUnitTestGate({ args: ['--changed-files', ...changedPaths], cwd, log: o.log, error: o.error }) === 0,
|
|
1422
1490
|
out,
|
|
1423
1491
|
);
|
|
1424
1492
|
record('ios-gate', iosGate.ok, { measured: iosGate.measured });
|
package/src/cli/release-cli.ts
CHANGED
|
@@ -631,7 +631,7 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
|
|
|
631
631
|
if (!opts.dryRun && !mayWriteLedger('release place', false, scheduleLedgerRoot())) return;
|
|
632
632
|
const priority = opts.priority ?? item.priority;
|
|
633
633
|
if (!priority) throw new CliUserError(`우선순위가 없는 칸: ${id} — --priority P0|P1|P2 를 지정하라`);
|
|
634
|
-
const result = placeCell({ id, title: item.title, owner: item.owner, priority: priority as PlacementPriority, predecessors: item.predecessors ?? [], deadlineVersion: item.deadlineVersion, ceoMinutes: item.ceoMinutes, ceoDate: item.ceoDate }, { schedules: rows, dryRun: opts.dryRun, by: who ?? 'cli' });
|
|
634
|
+
const result = placeCell({ id, title: item.title, owner: item.owner, priority: priority as PlacementPriority, predecessors: item.predecessors ?? [], deadlineVersion: item.deadlineVersion, ceoMinutes: item.ceoMinutes, ceoDate: item.ceoDate, ...(item.accelerator === true ? { accelerator: true } : {}) }, { schedules: rows, dryRun: opts.dryRun, by: who ?? 'cli' });
|
|
635
635
|
console.log(`${opts.dryRun ? '· 드라이런' : '✅'} ${id} ${result.from ?? '-'} → ${result.version} · ${result.reason}${result.displaced.length ? ` · P2 이월 ${result.displaced.map((row) => row.id).join(', ')}` : ''}`);
|
|
636
636
|
});
|
|
637
637
|
release.command('rebalance').description('마감 2시간 전 미시작 칸을 다음 판으로')
|
|
@@ -740,11 +740,12 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
|
|
|
740
740
|
withContext(checklist.command('add <id> <title>').description('칸 추가')).option('--owner <owner>', '담당').option('--kind <kind>', 'screen = 다섯 화면 짝 칸 · 근거에 짝: PWA … · 데스크톱 … · 폴드 … · 아이폰 … · 아이패드 … 한 줄')
|
|
741
741
|
.option('--priority <priority>', 'P0|P1|P2').option('--deadline-version <v>', '마감 판').option('--predecessor <id...>', '선행 칸 id')
|
|
742
742
|
.option('--ceo-minutes <minutes>', '대표 손 분량(분)').option('--ceo-date <date>', '대표 손 날짜 YYYY-MM-DD (없으면 판 착지일 KST)')
|
|
743
|
+
.option('--accelerator', '가속 등급 — 다른 칸의 처리량을 올리는 칸(자율성·효율성·동시성). 생략하면 등급 없음')
|
|
743
744
|
.option('--allow-duplicate-id', '다른 판에 같은 id 가 있어도 경고 후 추가')
|
|
744
|
-
.action((id: string, title: string, opts: { owner?: string; kind?: string; priority?: string; deadlineVersion?: string; predecessor?: string[]; ceoMinutes?: string; ceoDate?: string; allowDuplicateId?: boolean }, cmd: Command) => {
|
|
745
|
+
.action((id: string, title: string, opts: { owner?: string; kind?: string; priority?: string; deadlineVersion?: string; predecessor?: string[]; ceoMinutes?: string; ceoDate?: string; accelerator?: boolean; allowDuplicateId?: boolean }, cmd: Command) => {
|
|
745
746
|
const { version, json } = context(cmd);
|
|
746
747
|
if (!mayWrite(cmd)) return;
|
|
747
|
-
const data = addItem(version, { id, title, ...(opts.owner !== undefined ? { owner: opts.owner } : {}), ...(opts.kind !== undefined ? { kind: opts.kind as ChecklistKind } : {}), ...(opts.priority !== undefined ? { priority: opts.priority as PlacementPriority } : {}), ...(opts.deadlineVersion !== undefined ? { deadlineVersion: opts.deadlineVersion } : {}), ...(opts.predecessor !== undefined ? { predecessors: opts.predecessor } : {}), ...(opts.ceoMinutes !== undefined ? { ceoMinutes: ceoMinutes(opts.ceoMinutes) } : {}), ...(opts.ceoDate !== undefined ? { ceoDate: opts.ceoDate } : {}) }, { allowDuplicateId: opts.allowDuplicateId });
|
|
748
|
+
const data = addItem(version, { id, title, ...(opts.owner !== undefined ? { owner: opts.owner } : {}), ...(opts.kind !== undefined ? { kind: opts.kind as ChecklistKind } : {}), ...(opts.priority !== undefined ? { priority: opts.priority as PlacementPriority } : {}), ...(opts.deadlineVersion !== undefined ? { deadlineVersion: opts.deadlineVersion } : {}), ...(opts.predecessor !== undefined ? { predecessors: opts.predecessor } : {}), ...(opts.ceoMinutes !== undefined ? { ceoMinutes: ceoMinutes(opts.ceoMinutes) } : {}), ...(opts.ceoDate !== undefined ? { ceoDate: opts.ceoDate } : {}), ...(opts.accelerator ? { accelerator: true } : {}) }, { allowDuplicateId: opts.allowDuplicateId });
|
|
748
749
|
if (json) console.log(JSON.stringify(data)); else console.log(`✅ ${id} 추가`);
|
|
749
750
|
});
|
|
750
751
|
withContext(checklist.command('set <id>').description('칸 상태·근거·담당·처분 갱신'))
|
|
@@ -752,13 +753,14 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
|
|
|
752
753
|
.option('--disposition <disposition>', 'move|known-issue|block').option('--kind <kind>', 'screen = 다섯 화면 짝 칸 · 근거에 짝: PWA … · 데스크톱 … · 폴드 … · 아이폰 … · 아이패드 … 한 줄')
|
|
753
754
|
.option('--priority <priority>', 'P0|P1|P2').option('--deadline-version <v>', '마감 판').option('--predecessor <id...>', '선행 칸 id')
|
|
754
755
|
.option('--ceo-minutes <minutes>', '대표 손 분량(분)').option('--ceo-date <date>', '대표 손 날짜 YYYY-MM-DD')
|
|
755
|
-
.
|
|
756
|
+
.option('--accelerator', '가속 등급 부여').option('--no-accelerator', '가속 등급 해제(기본은 없음)')
|
|
757
|
+
.action((id: string, opts: { status?: string; evidence?: string; owner?: string; disposition?: string; kind?: string; priority?: string; deadlineVersion?: string; predecessor?: string[]; ceoMinutes?: string; ceoDate?: string; accelerator?: boolean }, cmd: Command) => {
|
|
756
758
|
const { version, json } = context(cmd);
|
|
757
759
|
if (opts.status !== undefined && !['green', 'yellow', 'red', 'done'].includes(opts.status)) throw new CliUserError(`잘못된 상태: ${opts.status}`, 'green|yellow|red|done');
|
|
758
760
|
if (opts.disposition !== undefined && !['move', 'known-issue', 'block'].includes(opts.disposition)) throw new CliUserError(`잘못된 처분: ${opts.disposition}`, 'move|known-issue|block');
|
|
759
|
-
if (opts.status === undefined && opts.evidence === undefined && opts.owner === undefined && opts.disposition === undefined && opts.kind === undefined && opts.priority === undefined && opts.deadlineVersion === undefined && opts.predecessor === undefined && opts.ceoMinutes === undefined && opts.ceoDate === undefined) throw new CliUserError('갱신할 칸을 지정하라', '--status · --evidence · --owner · --disposition · --kind · --priority · --deadline-version · --predecessor · --ceo-minutes · --ceo-date 중 하나');
|
|
761
|
+
if (opts.status === undefined && opts.evidence === undefined && opts.owner === undefined && opts.disposition === undefined && opts.kind === undefined && opts.priority === undefined && opts.deadlineVersion === undefined && opts.predecessor === undefined && opts.ceoMinutes === undefined && opts.ceoDate === undefined && opts.accelerator === undefined) throw new CliUserError('갱신할 칸을 지정하라', '--status · --evidence · --owner · --disposition · --kind · --priority · --deadline-version · --predecessor · --ceo-minutes · --ceo-date · --accelerator · --no-accelerator 중 하나');
|
|
760
762
|
if (!mayWrite(cmd)) return;
|
|
761
|
-
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 } : {}), ...(opts.priority !== undefined ? { priority: opts.priority as PlacementPriority } : {}), ...(opts.deadlineVersion !== undefined ? { deadlineVersion: opts.deadlineVersion } : {}), ...(opts.predecessor !== undefined ? { predecessors: opts.predecessor } : {}), ...(opts.ceoMinutes !== undefined ? { ceoMinutes: ceoMinutes(opts.ceoMinutes) } : {}), ...(opts.ceoDate !== undefined ? { ceoDate: opts.ceoDate } : {}) }, process.env.ELANOUS_TRACK || 'cli');
|
|
763
|
+
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 } : {}), ...(opts.priority !== undefined ? { priority: opts.priority as PlacementPriority } : {}), ...(opts.deadlineVersion !== undefined ? { deadlineVersion: opts.deadlineVersion } : {}), ...(opts.predecessor !== undefined ? { predecessors: opts.predecessor } : {}), ...(opts.ceoMinutes !== undefined ? { ceoMinutes: ceoMinutes(opts.ceoMinutes) } : {}), ...(opts.ceoDate !== undefined ? { ceoDate: opts.ceoDate } : {}), ...(opts.accelerator !== undefined ? { accelerator: opts.accelerator ? true : null } : {}) }, process.env.ELANOUS_TRACK || 'cli');
|
|
762
764
|
const item = data.items.find((entry) => entry.id === id);
|
|
763
765
|
const why = item?.kind === 'screen' && item.status === 'green' ? parityGap(item.evidence) : null;
|
|
764
766
|
if (why) {
|
|
@@ -897,13 +899,14 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
|
|
|
897
899
|
console.log(renderReleaseNotes(draftReleaseNotes(readLandedCommits(o.from, o.to), o.from, o.to)));
|
|
898
900
|
});
|
|
899
901
|
release.command('cut-branch')
|
|
900
|
-
.description('원래 컷에 main 의 수리 커밋만 얹은 release/<v> 가지를 만들고 push')
|
|
902
|
+
.description('원래 컷에 main 의 수리 커밋만 얹은 release/<v> 가지를 만들고 push. --append 는 이미 있는 가지에 fast-forward 로만 더 얹는다')
|
|
901
903
|
.requiredOption('--version <v>', '릴리스 판(x.y.z)')
|
|
902
904
|
.requiredOption('--base <sha>', '원래 컷 SHA')
|
|
903
905
|
.requiredOption('--pick <sha...>', '차례로 얹을 main 커밋 SHA')
|
|
906
|
+
.option('--append', '이미 있는 release/<v> 를 출발점으로 fast-forward push(강제 push 없음)')
|
|
904
907
|
.option('--dry-run', '가지·worktree·push 없이 계획만 출력')
|
|
905
908
|
.option('--json', '결과 한 줄 JSON(stdout)')
|
|
906
|
-
.action(async (o: { version: string; base: string; pick: string[]; dryRun?: boolean; json?: boolean }) => {
|
|
909
|
+
.action(async (o: { version: string; base: string; pick: string[]; append?: boolean; dryRun?: boolean; json?: boolean }) => {
|
|
907
910
|
await jsonAction(o.json, async (log) => cutReleaseBranch({ ...o, log }), () => true);
|
|
908
911
|
});
|
|
909
912
|
const runAction = async (o: { version: string; cutCommit?: string; dryRun?: boolean; ifReady?: boolean; forceFreeze?: boolean; json?: boolean }, logRun = console.log): Promise<boolean | AutoStartDeferral | undefined> => {
|
package/src/cli/self-update.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync, realpathSync, readlinkSync, readdirSync, statSync, rmSync, symlinkSync, renameSync } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, readlinkSync, readdirSync, statSync, rmSync, symlinkSync, renameSync, writeFileSync, lstatSync } from 'node:fs';
|
|
2
2
|
import { resolve, join, basename, dirname } from 'node:path';
|
|
3
3
|
import { homedir, platform, userInfo } from 'node:os';
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
@@ -18,6 +18,10 @@ export interface SelfUpdateOptions {
|
|
|
18
18
|
skipPwaBuild?: boolean;
|
|
19
19
|
/** 설치된 릴리스에서만 사용한다. 체크아웃 갱신에는 적용하지 않는다. */
|
|
20
20
|
version?: string;
|
|
21
|
+
/** 판 사이 dev 빌드 설치 — versions/<판>-dev.<sha12> · install.json channel:'dev'. 사람 승인 명령. */
|
|
22
|
+
dev?: boolean;
|
|
23
|
+
/** install.json 의 이전 versionDir 로 current 를 되돌린다. 이전이 없으면 거부. */
|
|
24
|
+
rollback?: boolean;
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
export interface SelfUpdateDeps {
|
|
@@ -45,6 +49,15 @@ export interface SelfUpdateDeps {
|
|
|
45
49
|
out?: { log: (s: string) => void; error: (s: string) => void };
|
|
46
50
|
/** 판올림 성공 알림(주입 안 하면 sendOutbound 'report' · 시험에선 안 보냄). */
|
|
47
51
|
notice?: (text: string) => void;
|
|
52
|
+
/**
|
|
53
|
+
* 설치 루트(versions · current · install.json). 주입하지 않으면 ~/.local/share/elanous.
|
|
54
|
+
* 시험은 임시 루트만 넘긴다 — 실물 심링크·재시작을 만지지 않는다.
|
|
55
|
+
*/
|
|
56
|
+
installRoot?: string;
|
|
57
|
+
/** 패키지 버전(package.json). 주입하지 않으면 체크아웃 package.json. */
|
|
58
|
+
packageVersion?: (checkout: string) => string;
|
|
59
|
+
/** 재시작을 실제로 호출해도 되는지. NODE_ENV=test · ELANOUS_TEST_HOME 이면 기본 false. */
|
|
60
|
+
allowLiveRestart?: boolean;
|
|
48
61
|
}
|
|
49
62
|
|
|
50
63
|
export interface SelfUpdateResult {
|
|
@@ -63,6 +76,12 @@ export interface SelfUpdateResult {
|
|
|
63
76
|
prune?: VersionPruneOutcome;
|
|
64
77
|
/** 재시작 뒤 건강 — ok · rolled-back(직전 판으로 되돌려 회복) · rollback-failed · no-rollback-target. */
|
|
65
78
|
health?: 'ok' | 'rolled-back' | 'rollback-failed' | 'no-rollback-target' | 'unmeasured';
|
|
79
|
+
/** --dev 설치 또는 --rollback 이 옮긴 current 대상(versions/<name>). */
|
|
80
|
+
current?: string;
|
|
81
|
+
/** install.json channel. dev 설치면 'dev'. */
|
|
82
|
+
channel?: string;
|
|
83
|
+
/** --dev 가 남긴 이전 versionDir. */
|
|
84
|
+
previous?: string;
|
|
66
85
|
rolledBackTo?: string;
|
|
67
86
|
/** relay(`com.elanous.openai-relay`) 판정 — 넥서스와 «따로» 본다(T5 · 2026-09-24). */
|
|
68
87
|
relay?: RelayUpdateOutcome;
|
|
@@ -182,6 +201,142 @@ export function rollbackTarget(versions: readonly string[], daemonSha: string, i
|
|
|
182
201
|
|
|
183
202
|
const VERSIONS_DIR = () => join(homedir(), '.local/share/elanous/versions');
|
|
184
203
|
|
|
204
|
+
/** 시험·ELANOUS_TEST_HOME 에서는 실물 재시작을 호출하지 않는다. */
|
|
205
|
+
export function liveRestartBlocked(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
206
|
+
return env.NODE_ENV === 'test' || Boolean(env.ELANOUS_TEST_HOME?.trim());
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export interface InstallRecord {
|
|
210
|
+
version?: string;
|
|
211
|
+
versionDir?: string;
|
|
212
|
+
source?: string;
|
|
213
|
+
installedAt?: string;
|
|
214
|
+
commit?: string;
|
|
215
|
+
channel?: string;
|
|
216
|
+
previous?: string;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function defaultInstallRoot(): string {
|
|
220
|
+
return join(homedir(), '.local/share/elanous');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function readInstallRecord(root: string): InstallRecord | null {
|
|
224
|
+
try {
|
|
225
|
+
const parsed: unknown = JSON.parse(readFileSync(join(root, 'install.json'), 'utf8'));
|
|
226
|
+
if (typeof parsed !== 'object' || parsed === null) return null;
|
|
227
|
+
return parsed as InstallRecord;
|
|
228
|
+
} catch {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** install.json 의 version·commit. 못 읽으면 둘 다 «unknown». */
|
|
234
|
+
export function readInstalledIdentity(root: string = defaultInstallRoot()): { installedVersion: string; installedCommit: string } {
|
|
235
|
+
const record = readInstallRecord(root);
|
|
236
|
+
const version = typeof record?.version === 'string' && record.version.trim() ? record.version.trim() : '';
|
|
237
|
+
const commit = typeof record?.commit === 'string' && record.commit.trim() ? record.commit.trim() : '';
|
|
238
|
+
return { installedVersion: version || 'unknown', installedCommit: commit || 'unknown' };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const VERSION_DIR_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
242
|
+
|
|
243
|
+
function versionDirName(versionDir: string | undefined): string | null {
|
|
244
|
+
if (typeof versionDir !== 'string') return null;
|
|
245
|
+
if (!/^versions\/[A-Za-z0-9][A-Za-z0-9._-]*$/.test(versionDir) || versionDir.includes('..')) return null;
|
|
246
|
+
return basename(versionDir);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** 판 디렉터리에 실행 진입 파일이 있나(설치기가 실제로 내용을 깔았나). */
|
|
250
|
+
export function installEntryExists(root: string, versionName: string): boolean {
|
|
251
|
+
return existsSync(join(root, 'versions', versionName, 'node_modules', 'elanous', 'bin', 'elanous.mjs'));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** 판 이름 «<판>-<커밋12>» · «<판>-dev.<커밋12>» 에서 판·커밋을 읽는다(되돌림 메타 일치용). */
|
|
255
|
+
export function parseVersionDirName(name: string): { version: string; commit: string } | null {
|
|
256
|
+
const m = /^(.+?)-(?:dev\.)?([0-9a-f]{12})(?:-dirty)?$/.exec(name);
|
|
257
|
+
return m ? { version: m[1]!, commit: m[2]! } : null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** current 심링크를 versions/<name> 으로 원자적으로 옮긴다. 대상 폴더가 없으면 거부. */
|
|
261
|
+
export function relinkInstallCurrent(root: string, versionName: string): void {
|
|
262
|
+
if (!VERSION_DIR_NAME.test(versionName) || versionName.includes('..')) throw new Error(`잘못된 판 이름: ${versionName}`);
|
|
263
|
+
if (!existsSync(join(root, 'versions', versionName))) throw new Error(`판 디렉터리 없음: ${versionName}`);
|
|
264
|
+
// EDGE-RAIL-VERIFY(10-06 05:30 사고): 진입 파일이 없는 판으로는 current 를 옮기지 않는다(빈 판 → 전역 elanous·크론 전부 실패).
|
|
265
|
+
if (!installEntryExists(root, versionName)) throw new Error(`빈 판(진입 파일 없음): ${versionName} — current 를 옮기지 않는다`);
|
|
266
|
+
const tmp = join(root, `.current-dev-${process.pid}`);
|
|
267
|
+
rmSync(tmp, { force: true });
|
|
268
|
+
symlinkSync(`versions/${versionName}`, tmp);
|
|
269
|
+
renameSync(tmp, join(root, 'current'));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function checkoutPackageVersion(checkout: string): string {
|
|
273
|
+
const parsed = JSON.parse(readFileSync(join(checkout, 'package.json'), 'utf8')) as { version?: unknown };
|
|
274
|
+
if (typeof parsed.version !== 'string' || !parsed.version.trim()) throw new Error('package.json version 없음');
|
|
275
|
+
return parsed.version.trim();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* 판 사이 dev 빌드 — versions/<판>-dev.<sha12> 를 만들고 current 를 옮긴다.
|
|
280
|
+
* install.json 에 channel:'dev' 와 이전 versionDir(previous) 를 남긴다. 실제 설치기·재시작은 부르지 않는다.
|
|
281
|
+
*/
|
|
282
|
+
export function installDevBuild(checkout: string, commit: string, deps: Pick<SelfUpdateDeps, 'installRoot' | 'packageVersion'> & { runInstaller?: () => { status: number | null; stderr: string } } = {}): { versionName: string; versionDir: string; previous: string; record: InstallRecord } {
|
|
283
|
+
const sha12 = commit.trim().toLowerCase().slice(0, 12);
|
|
284
|
+
if (!/^[0-9a-f]{12}$/.test(sha12)) throw new Error(`dev 설치 커밋이 12자가 아님: ${commit}`);
|
|
285
|
+
const root = deps.installRoot ?? defaultInstallRoot();
|
|
286
|
+
const prior = readInstallRecord(root);
|
|
287
|
+
const previous = versionDirName(prior?.versionDir) ? prior!.versionDir! : (typeof prior?.previous === 'string' ? prior.previous : '');
|
|
288
|
+
// EDGE-RAIL-VERIFY: 빈 디렉터리를 만들고 current 만 옮기던 길(10-06 05:30 사고)을 버리고, 정식 길과 같은 설치기로 내용을 깐다.
|
|
289
|
+
if (!deps.runInstaller) throw new Error('dev 설치기 없음 — 빈 판을 깔지 않는다');
|
|
290
|
+
// install.sh 는 current 를 «먼저» 옮기고 뒤에서 실패할 수 있다 — 실패·빈 판 어느 쪽이든 이전 판·install.json 으로 되돌린다.
|
|
291
|
+
const restore = (why: string, versionName: string | null): never => {
|
|
292
|
+
const previousName = versionDirName(previous);
|
|
293
|
+
if (previousName && installEntryExists(root, previousName)) relinkInstallCurrent(root, previousName);
|
|
294
|
+
if (prior) writeFileSync(join(root, 'install.json'), `${JSON.stringify(prior, null, 2)}\n`);
|
|
295
|
+
try { debug.log('self-update', 'dev-install-refused', { why, versionName, kept: previousName ?? null }); } catch { /* 관측 실패가 막지 않는다 */ }
|
|
296
|
+
throw new Error(`${why}: ${versionName ?? '?'} — current 를 이전 판(${previousName ?? '없음'})으로 유지`);
|
|
297
|
+
};
|
|
298
|
+
let result: { status: number | null; stderr: string };
|
|
299
|
+
try { result = deps.runInstaller(); }
|
|
300
|
+
catch (error) { return restore(`설치기 예외(${String(error).slice(0, 200)})`, null); }
|
|
301
|
+
const after = readInstallRecord(root);
|
|
302
|
+
const versionName = versionDirName(after?.versionDir);
|
|
303
|
+
if (result.status !== 0) return restore(`설치기 실패(rc=${result.status}: ${result.stderr.slice(0, 200)})`, versionName);
|
|
304
|
+
if (!versionName || !installEntryExists(root, versionName)) return restore('빈 판(진입 파일 없음)', versionName);
|
|
305
|
+
const versionDir = `versions/${versionName}`;
|
|
306
|
+
const record: InstallRecord = {
|
|
307
|
+
...(after ?? {}),
|
|
308
|
+
versionDir,
|
|
309
|
+
channel: 'dev',
|
|
310
|
+
...(previous && previous !== versionDir ? { previous } : {}),
|
|
311
|
+
};
|
|
312
|
+
writeFileSync(join(root, 'install.json'), `${JSON.stringify(record, null, 2)}\n`);
|
|
313
|
+
return { versionName, versionDir, previous, record };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** install.json.previous 의 판으로 current 를 되돌린다. previous 가 없으면 거부(exit 2). */
|
|
317
|
+
export function rollbackDevInstall(deps: Pick<SelfUpdateDeps, 'installRoot'> = {}): { ok: true; versionName: string; record: InstallRecord } | { ok: false; reason: string } {
|
|
318
|
+
const root = deps.installRoot ?? defaultInstallRoot();
|
|
319
|
+
const prior = readInstallRecord(root);
|
|
320
|
+
const previousName = versionDirName(prior?.previous);
|
|
321
|
+
if (!prior || !previousName) return { ok: false, reason: '이전 판(install.json previous)이 없다 — rollback 거부' };
|
|
322
|
+
if (!existsSync(join(root, 'versions', previousName))) return { ok: false, reason: `이전 판 디렉터리 없음: ${previousName}` };
|
|
323
|
+
const currentName = versionDirName(prior.versionDir);
|
|
324
|
+
relinkInstallCurrent(root, previousName);
|
|
325
|
+
const parsed = parseVersionDirName(previousName);
|
|
326
|
+
const record: InstallRecord = {
|
|
327
|
+
...prior,
|
|
328
|
+
...(parsed ? { version: parsed.version, commit: parsed.commit } : {}),
|
|
329
|
+
versionDir: `versions/${previousName}`,
|
|
330
|
+
channel: prior.channel === 'dev' ? undefined : prior.channel,
|
|
331
|
+
previous: currentName ? `versions/${currentName}` : undefined,
|
|
332
|
+
installedAt: new Date().toISOString(),
|
|
333
|
+
};
|
|
334
|
+
if (!record.channel) delete record.channel;
|
|
335
|
+
if (!record.previous) delete record.previous;
|
|
336
|
+
writeFileSync(join(root, 'install.json'), `${JSON.stringify(record, null, 2)}\n`);
|
|
337
|
+
return { ok: true, versionName: previousName, record };
|
|
338
|
+
}
|
|
339
|
+
|
|
185
340
|
/** `unmeasured` = 건강을 «잴 수» 없었다(REST 주소를 한 번도 못 얻음) — 「건강하지 않다」와 다르다. 되돌리지 않는다. */
|
|
186
341
|
export interface VerifyRestartResult { ok: boolean; daemonSha?: string; reason?: string; unmeasured?: boolean }
|
|
187
342
|
|
|
@@ -399,9 +554,14 @@ export async function runReleaseUpdate(options: ReleaseUpdateOptions = {}, deps:
|
|
|
399
554
|
}
|
|
400
555
|
let previous = '';
|
|
401
556
|
let previousBuild = '';
|
|
557
|
+
let replacedDev: { from: string; to: string } | null = null;
|
|
402
558
|
try {
|
|
403
559
|
const metadata: unknown = JSON.parse(readFileSync(join(prefix, 'install.json'), 'utf8'));
|
|
404
560
|
previousBuild = installedBuild(metadata);
|
|
561
|
+
if (typeof metadata === 'object' && metadata !== null && (metadata as { channel?: unknown }).channel === 'dev') {
|
|
562
|
+
const fromDir = typeof (metadata as { versionDir?: unknown }).versionDir === 'string' ? (metadata as { versionDir: string }).versionDir : '';
|
|
563
|
+
replacedDev = { from: fromDir, to: '' };
|
|
564
|
+
}
|
|
405
565
|
if (typeof metadata === 'object' && metadata !== null && 'versionDir' in metadata && typeof metadata.versionDir === 'string') {
|
|
406
566
|
const dir = metadata.versionDir;
|
|
407
567
|
if (/^versions\/[a-zA-Z0-9._-]+$/.test(dir) && !dir.includes('..')) previous = basename(dir);
|
|
@@ -418,6 +578,12 @@ export async function runReleaseUpdate(options: ReleaseUpdateOptions = {}, deps:
|
|
|
418
578
|
const installedVersion = typeof metadata === 'object' && metadata !== null && 'version' in metadata && typeof metadata.version === 'string'
|
|
419
579
|
? metadata.version : null;
|
|
420
580
|
if (!installedVersion) return fail(1, '설치판 확인 실패: install.json version 없음');
|
|
581
|
+
if (replacedDev) {
|
|
582
|
+
const toDir = typeof metadata === 'object' && metadata !== null && typeof (metadata as { versionDir?: unknown }).versionDir === 'string'
|
|
583
|
+
? (metadata as { versionDir: string }).versionDir : installedVersion;
|
|
584
|
+
replacedDev = { from: replacedDev.from, to: toDir };
|
|
585
|
+
try { debug.log('self-update', 'dev-replaced', replacedDev); } catch { /* 관측 실패가 갱신을 막지 않는다 */ }
|
|
586
|
+
}
|
|
421
587
|
const internalRevision = isTailnet(source) && version?.match(/^(.*)-([0-9a-f]{12})$/);
|
|
422
588
|
if (version && installedVersion !== version && (!internalRevision || installedVersion !== internalRevision[1])) {
|
|
423
589
|
return fail(1, `설치판 버전 불일치: 요청 ${version}, 설치 ${installedVersion}`);
|
|
@@ -491,6 +657,9 @@ export async function runUpdateForInstallation(
|
|
|
491
657
|
out.log(options.json ? JSON.stringify(result) : `self-update: ${result.reason}`);
|
|
492
658
|
return result;
|
|
493
659
|
}
|
|
660
|
+
if (options.dev || options.rollback) {
|
|
661
|
+
return runSelfUpdate(options, { cliRoot, ...deps.checkout });
|
|
662
|
+
}
|
|
494
663
|
if (options.version !== undefined) {
|
|
495
664
|
const result: SelfUpdateResult = { exitCode: 2, installedVersion: null, decision: null, restarted: false, reason: '--version 은 릴리스 설치본에서만 사용 가능' };
|
|
496
665
|
const out = deps.checkout?.out ?? console;
|
|
@@ -537,6 +706,15 @@ export async function runSelfUpdate(options: SelfUpdateOptions = {}, deps: SelfU
|
|
|
537
706
|
return result;
|
|
538
707
|
};
|
|
539
708
|
const reject = (reason: string): SelfUpdateResult => emit({ exitCode: 2, installedVersion: null, decision: null, restarted: false, reason });
|
|
709
|
+
if (options.rollback) {
|
|
710
|
+
const rolled = rollbackDevInstall(deps);
|
|
711
|
+
if (!rolled.ok) return reject(rolled.reason);
|
|
712
|
+
return emit({
|
|
713
|
+
exitCode: 0, installedVersion: rolled.versionName, decision: null, restarted: false,
|
|
714
|
+
current: `versions/${rolled.versionName}`, previous: rolled.record.previous, channel: rolled.record.channel,
|
|
715
|
+
reason: `rollback: current → ${rolled.versionName}`,
|
|
716
|
+
});
|
|
717
|
+
}
|
|
540
718
|
try {
|
|
541
719
|
if (!existsSync(join(checkout, 'scripts/install.sh'))) return reject(`체크아웃 없음 또는 설치기 없음: ${checkout}`);
|
|
542
720
|
const root = git(checkout, ['rev-parse', '--show-toplevel']);
|
|
@@ -546,6 +724,24 @@ export async function runSelfUpdate(options: SelfUpdateOptions = {}, deps: SelfU
|
|
|
546
724
|
const dirty = git(checkout, ['diff', '--quiet', 'HEAD', '--']);
|
|
547
725
|
if (dirty.status !== 0) return reject(dirty.status === 1 ? '추적 파일 변경: 체크아웃이 더러움' : `git diff 실패: ${dirty.stderr}`);
|
|
548
726
|
|
|
727
|
+
if (options.dev) {
|
|
728
|
+
try {
|
|
729
|
+
const installed = installDevBuild(checkout, head.stdout.trim(), { ...deps, runInstaller: () => run('bash', [join(checkout, 'scripts/install.sh'), '--no-modify-path'], checkout) });
|
|
730
|
+
return emit({
|
|
731
|
+
exitCode: 0,
|
|
732
|
+
installedVersion: installed.versionName,
|
|
733
|
+
decision: null,
|
|
734
|
+
restarted: false,
|
|
735
|
+
current: installed.versionDir,
|
|
736
|
+
channel: 'dev',
|
|
737
|
+
previous: installed.previous || undefined,
|
|
738
|
+
reason: `dev 설치: current → ${installed.versionName}`,
|
|
739
|
+
});
|
|
740
|
+
} catch (error) {
|
|
741
|
+
return emit({ exitCode: 1, installedVersion: null, decision: null, restarted: false, reason: `dev 설치 실패: ${error instanceof Error ? error.message : String(error)}` });
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
549
745
|
const silence = { log: (_s: string) => {}, error: (_s: string) => {} };
|
|
550
746
|
let decision: RestartNeededResult;
|
|
551
747
|
try {
|
|
@@ -628,6 +824,9 @@ export async function runSelfUpdate(options: SelfUpdateOptions = {}, deps: SelfU
|
|
|
628
824
|
const os = deps.os ?? platform();
|
|
629
825
|
const command = os === 'darwin' ? 'launchctl' : os === 'linux' ? 'systemctl' : null;
|
|
630
826
|
if (!command) return emit({ exitCode: 1, installedVersion, decision, restarted: false, prune, reason: `지원하지 않는 플랫폼: ${os}` });
|
|
827
|
+
if (deps.allowLiveRestart === false || (deps.allowLiveRestart !== true && liveRestartBlocked())) {
|
|
828
|
+
return emit({ exitCode: 0, installedVersion, decision, restarted: false, prune, reason: '테스트 가드: 실물 재시작 0' });
|
|
829
|
+
}
|
|
631
830
|
const args = os === 'darwin' ? ['kickstart', '-k', `gui/${deps.uid ?? process.getuid?.() ?? userInfo().uid}/com.elanous.nexus`] : ['--user', 'restart', 'elanous-nexus'];
|
|
632
831
|
try {
|
|
633
832
|
const restarted = run(command, args, checkout);
|