elanous 0.2.8 → 0.2.9
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/bFI5EEwg7T6aG88ibX4Zq/_buildManifest.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8630-cdfc61e82c653ffc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8894-7317998f6cb97d8c.js → 8894-e263b332208e3957.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/9454-139040d37c79dbc9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-fb7c2d458f32fcad.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-f4d2ecbbae6088d6.js → page-35e2713597dd24d1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/{page-2053e49930c82748.js → page-918b656f064577ba.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-d4b6df905f185055.js → page-f972fc1c00a62e88.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-a70ba27c33003dd0.js → page-5f30e00693527ebd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-7fbae173b3c103a7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/{page-18166f885d831d3e.js → page-3b4edcce364062ea.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/control/{page-1c9d6cfe68b121c2.js → page-c5794f484c251f44.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-1130d8eefcd87e76.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-973996d584f008c1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-147a328753d983e2.js → page-016e2bda04eecb1f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-799395a9df7d95aa.js → page-b4d6d9a3b56f2940.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-6a8158b71d1fe9d7.js → page-ec49a466121233ba.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-f72db356744ceade.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-a6fd9b50d1b85378.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-c0c0d19d51342d3e.js → page-8b0402a46a749a3d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-2e002ad8faa43291.js → page-037f83e2a1edc8e2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-b672e02e4bec8deb.js → page-adfbd2ba9598e59a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-46fedff57d4bec67.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/{page-9db2226f38c542fb.js → page-0cf318e9dcee2bbf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-899b327cb8b6845c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-6c1e6ba743a29dfe.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/{page-70ee65f5f7a151dc.js → page-1a8e4807292f6b17.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-28deba6bcb834916.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-092e1bcf27d751cc.js → page-de68e2205794037d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-eb52580c4b75b905.js → page-c71c33a573230672.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-7464c1987dd76e6d.js → page-0df30e0c7e50148a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-4fc56790fe0d2682.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-59df2aebb38a7c1e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-085847b088675978.js → page-4647be5dbcbd0795.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/{page-9fe9d38c36999eb4.js → page-e025892bda74b743.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-a0955de73b89f078.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-369846d2921af584.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/{page-295a02b1026b8255.js → page-72cbc1c8cdd0f65d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-77b28bfb0996d5ea.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-3220c4934f49c6c2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-90670589b87abea8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/{page-2e367075ffdbc062.js → page-7df9243ed021a4cc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-647ed2b66c1d49f1.js → page-4498c3853f4f688d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-35bedde6d885f08f.js +1 -0
- package/apps/pwa/out/_next/static/css/f92f63a5df0ba229.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/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/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/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/doc-draft/graphs/run-step.ts +1 -1
- package/scripts/release-loop/cutoff.ts +20 -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/test-diet/lib.ts +1 -0
- package/scripts/test-diet/node.ts +56 -14
- package/src/auth/setup-link-tokens.fixture.ts +19 -0
- package/src/auth/setup-link-tokens.ts +153 -0
- package/src/budget/unified-usage.ts +75 -0
- package/src/channel-bot-token.ts +37 -10
- package/src/chat/index.ts +9 -9
- package/src/cli/op-proxy-sync.ts +83 -0
- package/src/cli/pr-cli.ts +6 -2
- package/src/cli/release-cli.ts +41 -1
- package/src/dashboard/dashboard-status-lines.ts +45 -0
- package/src/dashboard/first-screen-band.ts +35 -0
- package/src/dashboard/index.ts +70 -13
- package/src/dashboard/provider-slash-runtime.ts +28 -11
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +10 -0
- package/src/dashboard/turn-stream-formatter.ts +1 -1
- package/src/decisions/decision-cards.ts +1 -1
- package/src/index.ts +16 -4
- package/src/maturity/feature-maturity.ts +2 -0
- package/src/nexus/api/http-server.ts +20 -0
- package/src/nexus/api/meta-api.ts +27 -0
- package/src/nexus/api/operator.ts +61 -0
- package/src/nexus/api/ops-api.ts +139 -0
- package/src/nexus/api/ops-seats-sources.ts +65 -0
- package/src/nexus/api/ops-seats.ts +114 -0
- package/src/nexus/api/public-routes.ts +14 -1
- package/src/nexus/api/setup-claim.ts +21 -0
- package/src/nexus/config/secrets/file-backend.ts +56 -18
- package/src/onboarding/first-run-doctor.ts +54 -0
- package/src/release-loop/checklist.ts +5 -64
- package/src/release-loop/feature-store.ts +327 -0
- package/src/release-loop/manifest.ts +59 -5
- package/src/seat-dispatch/ceo-commands.ts +65 -0
- package/src/skills/tools/usage-slash.ts +8 -7
- package/src/telegram-commands.ts +16 -0
- package/src/telegram-run.ts +83 -13
- package/src/thinking-line.ts +28 -20
- package/src/tui/chat/execution-badge.ts +1 -1
- package/src/ux-sim/turn-states.ts +1 -1
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/4836-3084c58c1725a40b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9454-a3e0953307216c58.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/chat/page-186fc6952f027acc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-cbbe03c0da7b081a.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/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/not-found-1ba4a5d857276334.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/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/showroom/page-d8ab4d1489223459.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/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/worktrees/page-ba058c56e38f2792.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/apps/pwa/out/_next/static/{lywc8m_HDzH9MKSDQhyEj → bFI5EEwg7T6aG88ibX4Zq}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Live sources for /v1/ops/seats: the coordination PR and merged PRs through gh (automation App identity, proxy variables
|
|
2
|
+
// removed — same rule as decision replies), the release checklist, and the decision ledger. Each failure becomes null upstream.
|
|
3
|
+
import { DecisionLedger } from '../../decisions/decision-ledger.js';
|
|
4
|
+
import { devVersion, listChecklist, type ChecklistItem } from '../../release-loop/checklist.js';
|
|
5
|
+
import { getUserConfig } from '../../user-config.js';
|
|
6
|
+
import { kstDayRange, type ChannelComment, type MergedPr, type SeatsSources } from './ops-seats.js';
|
|
7
|
+
|
|
8
|
+
const GH_TIMEOUT_MS = 20_000;
|
|
9
|
+
|
|
10
|
+
async function ghLines(args: string[]): Promise<string[] | null> {
|
|
11
|
+
const env: Record<string, string> = {};
|
|
12
|
+
for (const [k, v] of Object.entries(process.env)) if (v !== undefined && !/^(https?|all|no)_proxy$/i.test(k)) env[k] = v;
|
|
13
|
+
try {
|
|
14
|
+
const { githubAutomationToken } = await import('../../auth/github-app-token.js');
|
|
15
|
+
const token = githubAutomationToken();
|
|
16
|
+
if (token) env.GH_TOKEN = token;
|
|
17
|
+
} catch { /* machine login */ }
|
|
18
|
+
const proc = Bun.spawn(['gh', ...args], { stdin: 'ignore', stdout: 'pipe', stderr: 'ignore', env });
|
|
19
|
+
const timer = setTimeout(() => proc.kill(), GH_TIMEOUT_MS);
|
|
20
|
+
try {
|
|
21
|
+
const [out, code] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
|
22
|
+
return code === 0 ? out.split('\n').filter(Boolean) : null;
|
|
23
|
+
} finally { clearTimeout(timer); }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function channelTarget(): { repo: string; pr: string } | null {
|
|
27
|
+
const raw = (getUserConfig().raw?.decisions as { replyGhPr?: unknown } | undefined)?.replyGhPr;
|
|
28
|
+
const m = typeof raw === 'string' ? /^([\w.-]+\/[\w.-]+)#(\d+)$/.exec(raw.trim()) : null;
|
|
29
|
+
return m ? { repo: m[1]!, pr: m[2]! } : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function nextPatch(version: string): string {
|
|
33
|
+
const [major, minor, patch] = version.split('.').map(Number);
|
|
34
|
+
return `${major}.${minor}.${patch! + 1}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function liveSeatsSources(): SeatsSources {
|
|
38
|
+
return {
|
|
39
|
+
async channel(date) {
|
|
40
|
+
const target = channelTarget();
|
|
41
|
+
if (!target) return null;
|
|
42
|
+
const lines = await ghLines(['api', '--paginate', `repos/${target.repo}/issues/${target.pr}/comments?since=${kstDayRange(date).start}&per_page=100`,
|
|
43
|
+
'--jq', '.[] | {body: .body, createdAt: .created_at} | tojson']);
|
|
44
|
+
return lines?.map((line) => JSON.parse(line) as ChannelComment) ?? null;
|
|
45
|
+
},
|
|
46
|
+
async merged(date) {
|
|
47
|
+
const target = channelTarget();
|
|
48
|
+
if (!target) return null;
|
|
49
|
+
const { start, end } = kstDayRange(date);
|
|
50
|
+
const lines = await ghLines(['pr', 'list', '--repo', target.repo, '--state', 'merged', '--limit', '300',
|
|
51
|
+
'--search', `merged:${start.slice(0, 19)}Z..${end.slice(0, 19)}Z`, '--json', 'number,title,body,mergedAt', '--jq', '.[] | tojson']);
|
|
52
|
+
return lines?.map((line) => JSON.parse(line) as MergedPr) ?? null;
|
|
53
|
+
},
|
|
54
|
+
checklist() {
|
|
55
|
+
const dev = devVersion();
|
|
56
|
+
const next = listChecklist(nextPatch(dev)).items;
|
|
57
|
+
const current = listChecklist(dev).items;
|
|
58
|
+
const all: ChecklistItem[] = [...current, ...next];
|
|
59
|
+
return { current: next.length ? next : current, all };
|
|
60
|
+
},
|
|
61
|
+
openDecisionRaisers() {
|
|
62
|
+
return new DecisionLedger().list({ status: 'open' }).map((entry) => entry.raisedBy.agent);
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// GET /v1/ops/seats?date=YYYY-MM-DD — one row per seat: latest channel line, PRs landed that day, red items, open decisions.
|
|
2
|
+
// A source that could not be read is null, never 0 («못 읽음» ≠ «없음»). Contract: 내부 문서 `DESIGN-ops3-seat-board-and-public-demo-2026-10-02` §1.
|
|
3
|
+
import { debug, redactSecretText } from '../../debug/log.js';
|
|
4
|
+
import type { ChecklistItem } from '../../release-loop/checklist.js';
|
|
5
|
+
import { canonicalSeatId } from '../../msg/msg-store.js';
|
|
6
|
+
|
|
7
|
+
export const SEATS = [
|
|
8
|
+
{ seat: 'OP', role: 'COO' }, { seat: 'TC', role: 'CTO' }, { seat: 'MK', role: 'CMO' }, { seat: 'UX', role: 'CXO' },
|
|
9
|
+
] as const;
|
|
10
|
+
export const SEATS_CACHE_MS = 60_000;
|
|
11
|
+
|
|
12
|
+
export interface ChannelComment { body: string; createdAt: string }
|
|
13
|
+
export interface MergedPr { number: number; title: string; body: string; mergedAt: string }
|
|
14
|
+
export interface SeatsSources {
|
|
15
|
+
/** Coordination-channel comments created on `date` (KST) or later; null when unreadable. */
|
|
16
|
+
channel(date: string): Promise<ChannelComment[] | null>;
|
|
17
|
+
/** PRs merged on `date` (KST); null when unreadable. */
|
|
18
|
+
merged(date: string): Promise<MergedPr[] | null>;
|
|
19
|
+
/** Checklist items of the version being worked on (counts, red) and of every version known (id → owner). */
|
|
20
|
+
checklist(): { current: ChecklistItem[]; all: ChecklistItem[] } | null;
|
|
21
|
+
/** `raisedBy.agent` of every open decision. */
|
|
22
|
+
openDecisionRaisers(): string[] | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SeatRow {
|
|
26
|
+
seat: string; role: string;
|
|
27
|
+
now: { text: string; at: string } | null;
|
|
28
|
+
landed: Array<{ pr: number; title: string; at: string; checklistId: string | null }> | null;
|
|
29
|
+
blocked: Array<{ id: string; title: string; status: 'red' }> | null;
|
|
30
|
+
pendingDecisions: number | null;
|
|
31
|
+
checklist: { green: number; yellow: number; red: number; done: number } | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function seatOf(value: string | undefined): string | null {
|
|
35
|
+
if (!value) return null;
|
|
36
|
+
try { return canonicalSeatId(value); } catch { return null; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const clip = (text: string, max: number) => redactSecretText(text).slice(0, max);
|
|
40
|
+
|
|
41
|
+
/** First checklist id that appears as a whole word in the title, else in the body. */
|
|
42
|
+
function checklistIdOf(pr: MergedPr, ids: string[]): string | null {
|
|
43
|
+
for (const text of [pr.title, pr.body]) {
|
|
44
|
+
const found = ids.map((id) => ({ id, at: text.search(new RegExp(`(?<![A-Za-z0-9])${id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![A-Za-z0-9])`)) }))
|
|
45
|
+
.filter((hit) => hit.at >= 0).sort((a, b) => a.at - b.at || b.id.length - a.id.length);
|
|
46
|
+
if (found.length) return found[0]!.id;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function buildSeatsBoard(date: string, sources: SeatsSources): Promise<{ date: string; seats: SeatRow[] }> {
|
|
52
|
+
const [channel, merged] = await Promise.all([
|
|
53
|
+
sources.channel(date).catch(() => null), sources.merged(date).catch(() => null),
|
|
54
|
+
]);
|
|
55
|
+
let checklist: ReturnType<SeatsSources['checklist']> = null;
|
|
56
|
+
try { checklist = sources.checklist(); } catch { checklist = null; }
|
|
57
|
+
let raisers: string[] | null = null;
|
|
58
|
+
try { raisers = sources.openDecisionRaisers(); } catch { raisers = null; }
|
|
59
|
+
|
|
60
|
+
const owners = new Map<string, string>();
|
|
61
|
+
for (const item of checklist?.all ?? []) { const seat = seatOf(item.owner); if (seat) owners.set(item.id, seat); }
|
|
62
|
+
const ids = [...owners.keys()];
|
|
63
|
+
const landedBySeat = new Map<string, NonNullable<SeatRow['landed']>>();
|
|
64
|
+
for (const pr of merged ?? []) {
|
|
65
|
+
const id = checklistIdOf(pr, ids);
|
|
66
|
+
const seat = id ? owners.get(id) : undefined;
|
|
67
|
+
if (!seat) continue; // ⛔ not by commit author — every seat lands through the same account.
|
|
68
|
+
landedBySeat.set(seat, [...(landedBySeat.get(seat) ?? []), { pr: pr.number, title: clip(pr.title, 80), at: pr.mergedAt, checklistId: id }]);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const seats = SEATS.map(({ seat, role }): SeatRow => {
|
|
72
|
+
const latest = channel?.filter((c) => c.body.trimStart().startsWith(`**[${seat}]**`)).sort((a, b) => b.createdAt.localeCompare(a.createdAt))[0];
|
|
73
|
+
const mine = checklist?.current.filter((item) => seatOf(item.owner) === seat);
|
|
74
|
+
return {
|
|
75
|
+
seat, role,
|
|
76
|
+
now: latest ? { text: clip(latest.body.trimStart().split('\n')[0]!, 120), at: latest.createdAt } : null,
|
|
77
|
+
landed: merged === null || checklist === null ? null : (landedBySeat.get(seat) ?? []).sort((a, b) => a.at.localeCompare(b.at)),
|
|
78
|
+
blocked: mine ? mine.filter((item) => item.status === 'red').map((item) => ({ id: item.id, title: clip(item.title, 80), status: 'red' as const })) : null,
|
|
79
|
+
pendingDecisions: raisers === null ? null : raisers.filter((agent) => seatOf(agent) === seat).length,
|
|
80
|
+
checklist: mine ? {
|
|
81
|
+
green: mine.filter((i) => i.status === 'green').length, yellow: mine.filter((i) => i.status === 'yellow').length,
|
|
82
|
+
red: mine.filter((i) => i.status === 'red').length, done: mine.filter((i) => i.status === 'done').length,
|
|
83
|
+
} : null,
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
debug.log('ops.seats', 'built', {
|
|
87
|
+
date, channel: channel === null ? 'unreadable' : channel.length, merged: merged === null ? 'unreadable' : merged.length,
|
|
88
|
+
checklist: checklist !== null, decisions: raisers !== null,
|
|
89
|
+
});
|
|
90
|
+
return { date, seats };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** KST calendar day → [start, end) in UTC ISO. */
|
|
94
|
+
export function kstDayRange(date: string): { start: string; end: string } {
|
|
95
|
+
const start = new Date(`${date}T00:00:00+09:00`);
|
|
96
|
+
return { start: start.toISOString(), end: new Date(start.getTime() + 86_400_000).toISOString() };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function todayKst(now = new Date()): string {
|
|
100
|
+
return new Intl.DateTimeFormat('sv-SE', { timeZone: 'Asia/Seoul' }).format(now);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** One snapshot per date for 60 s; concurrent requests share one build. */
|
|
104
|
+
export function createSeatsCache(sources: SeatsSources, now: () => number = Date.now, ttlMs = SEATS_CACHE_MS) {
|
|
105
|
+
const slots = new Map<string, { at: number; value: Promise<{ date: string; seats: SeatRow[] }> }>();
|
|
106
|
+
return (date: string) => {
|
|
107
|
+
const slot = slots.get(date);
|
|
108
|
+
if (slot && now() - slot.at < ttlMs) return slot.value;
|
|
109
|
+
const value = buildSeatsBoard(date, sources);
|
|
110
|
+
slots.set(date, { at: now(), value });
|
|
111
|
+
value.catch(() => slots.delete(date));
|
|
112
|
+
return value;
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -36,6 +36,12 @@ export const PUBLIC_ROUTES: Array<PublicRoute> = [
|
|
|
36
36
|
path: '/v1/push/vapid-public-key',
|
|
37
37
|
why: 'PWA fetches the VAPID public key before a bearer is pasted',
|
|
38
38
|
},
|
|
39
|
+
{
|
|
40
|
+
method: 'GET',
|
|
41
|
+
match: 'exact',
|
|
42
|
+
path: '/v1/me',
|
|
43
|
+
why: 'operator signal only (draw the ops area or not); false unless operator.enabled, no other data',
|
|
44
|
+
},
|
|
39
45
|
{
|
|
40
46
|
method: 'GET',
|
|
41
47
|
match: 'exact',
|
|
@@ -56,6 +62,13 @@ export const PUBLIC_ROUTES: Array<PublicRoute> = [
|
|
|
56
62
|
selfVerified: true,
|
|
57
63
|
why: 'Pod GitHub App credential relay verifies its own run-scoped gh-credential token before minting a repository-scoped installation token',
|
|
58
64
|
},
|
|
65
|
+
{
|
|
66
|
+
method: 'POST',
|
|
67
|
+
match: 'exact',
|
|
68
|
+
path: '/v1/setup/claim',
|
|
69
|
+
selfVerified: true,
|
|
70
|
+
why: 'one-use setup link is verified by the claim handler without an owner bearer',
|
|
71
|
+
},
|
|
59
72
|
{
|
|
60
73
|
method: 'GET',
|
|
61
74
|
match: 'prefix',
|
|
@@ -76,7 +89,7 @@ export function isPublicRoute(
|
|
|
76
89
|
): boolean {
|
|
77
90
|
for (const route of PUBLIC_ROUTES) {
|
|
78
91
|
if (route.method !== method) continue;
|
|
79
|
-
if (route.path.startsWith('/v1/setup/') && !ctx.setupMode) continue;
|
|
92
|
+
if (route.path.startsWith('/v1/setup/') && route.path !== '/v1/setup/claim' && !ctx.setupMode) continue;
|
|
80
93
|
if (route.match === 'exact') {
|
|
81
94
|
if (pathname === route.path) return true;
|
|
82
95
|
} else if (pathname === route.path || pathname.startsWith(route.path)) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { claimSetupLinkToken } from '../../auth/setup-link-tokens.js';
|
|
2
|
+
import { jsonResponse } from './json-response.js';
|
|
3
|
+
|
|
4
|
+
/** A setup link is its own credential; this handler never reads the owner bearer. */
|
|
5
|
+
export async function handleSetupClaim(req: Request): Promise<Response> {
|
|
6
|
+
let body: unknown;
|
|
7
|
+
try {
|
|
8
|
+
body = await req.json();
|
|
9
|
+
} catch {
|
|
10
|
+
return jsonResponse({ error: 'bad_request' }, 400);
|
|
11
|
+
}
|
|
12
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)
|
|
13
|
+
|| typeof (body as Record<string, unknown>).t !== 'string'
|
|
14
|
+
|| !(body as { t: string }).t.trim()) {
|
|
15
|
+
return jsonResponse({ error: 'bad_request' }, 400);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const result = claimSetupLinkToken((body as { t: string }).t);
|
|
19
|
+
if (!result.ok) return jsonResponse({ error: result.reason }, 401);
|
|
20
|
+
return jsonResponse({ scope: 'setup', bearer: result.bearer, expiresAt: result.expiresAt });
|
|
21
|
+
}
|
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
// without touching caller code. The legacy module re-exports this
|
|
6
6
|
// backend's accessor functions to preserve backwards compatibility.
|
|
7
7
|
|
|
8
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, copyFileSync, unlinkSync, chmodSync } from 'node:fs';
|
|
9
|
+
import { randomUUID } from 'node:crypto';
|
|
9
10
|
import { dirname } from 'node:path';
|
|
11
|
+
import { acquireLockSync } from '../../../storage/file-lock.js';
|
|
10
12
|
import { SECRETS_VERSION, type SecretsFile } from '../types.js';
|
|
11
13
|
import { secretsPath } from '../paths.js';
|
|
12
14
|
import type { SecretBackend, SecretBackendAvailability, SyncReadableBackend } from './types.js';
|
|
@@ -21,20 +23,47 @@ function readSecretsFile(): SecretsFile {
|
|
|
21
23
|
try {
|
|
22
24
|
const raw = readFileSync(path, 'utf-8');
|
|
23
25
|
const parsed = JSON.parse(raw) as Partial<SecretsFile>;
|
|
24
|
-
if (parsed
|
|
25
|
-
|
|
26
|
-
version
|
|
27
|
-
|
|
28
|
-
};
|
|
29
|
-
} catch {
|
|
30
|
-
|
|
26
|
+
if (parsed?.version !== SECRETS_VERSION || !parsed.secrets || typeof parsed.secrets !== 'object'
|
|
27
|
+
|| Array.isArray(parsed.secrets) || Object.values(parsed.secrets).some(value => typeof value !== 'string')) {
|
|
28
|
+
throw new Error('invalid secrets file version or contents');
|
|
29
|
+
}
|
|
30
|
+
return { version: SECRETS_VERSION, secrets: parsed.secrets };
|
|
31
|
+
} catch (error) {
|
|
32
|
+
throw new Error(`cannot read secrets file at ${path}`, { cause: error });
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
function writeSecretsFile(s: SecretsFile): void {
|
|
35
37
|
const path = secretsPath();
|
|
36
38
|
mkdirSync(dirname(path), { recursive: true });
|
|
37
|
-
|
|
39
|
+
const tmp = `${path}.${randomUUID()}.tmp`;
|
|
40
|
+
const backupTmp = `${path}.${randomUUID()}.bak.tmp`;
|
|
41
|
+
try {
|
|
42
|
+
writeFileSync(tmp, JSON.stringify(s, null, 2), { mode: 0o600, flag: 'wx' });
|
|
43
|
+
if (existsSync(path)) {
|
|
44
|
+
copyFileSync(path, backupTmp);
|
|
45
|
+
chmodSync(backupTmp, 0o600);
|
|
46
|
+
renameSync(backupTmp, `${path}.bak`);
|
|
47
|
+
}
|
|
48
|
+
renameSync(tmp, path);
|
|
49
|
+
} finally {
|
|
50
|
+
if (existsSync(tmp)) unlinkSync(tmp);
|
|
51
|
+
if (existsSync(backupTmp)) unlinkSync(backupTmp);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function modifySecretsFile(change: (s: SecretsFile) => boolean): boolean {
|
|
56
|
+
const path = secretsPath();
|
|
57
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
58
|
+
const release = acquireLockSync(`${path}.lock`);
|
|
59
|
+
try {
|
|
60
|
+
const s = readSecretsFile();
|
|
61
|
+
if (!change(s)) return false;
|
|
62
|
+
writeSecretsFile(s);
|
|
63
|
+
return true;
|
|
64
|
+
} finally {
|
|
65
|
+
release();
|
|
66
|
+
}
|
|
38
67
|
}
|
|
39
68
|
|
|
40
69
|
const syncReadable: SyncReadableBackend = {
|
|
@@ -46,16 +75,14 @@ export const fileBackend: SecretBackend = {
|
|
|
46
75
|
id: 'file',
|
|
47
76
|
async get(id) { return syncReadable.getSync(id); },
|
|
48
77
|
async set(id, value) {
|
|
49
|
-
|
|
50
|
-
s.secrets[id] = value;
|
|
51
|
-
writeSecretsFile(s);
|
|
78
|
+
modifySecretsFile(s => { s.secrets[id] = value; return true; });
|
|
52
79
|
},
|
|
53
80
|
async delete(id) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
81
|
+
return modifySecretsFile(s => {
|
|
82
|
+
if (!Object.hasOwn(s.secrets, id)) return false;
|
|
83
|
+
delete s.secrets[id];
|
|
84
|
+
return true;
|
|
85
|
+
});
|
|
59
86
|
},
|
|
60
87
|
async list() { return syncReadable.listSync(); },
|
|
61
88
|
async isAvailable(): Promise<SecretBackendAvailability> {
|
|
@@ -74,5 +101,16 @@ export function readSecretsFileRaw(): SecretsFile {
|
|
|
74
101
|
}
|
|
75
102
|
|
|
76
103
|
export function writeSecretsFileRaw(s: SecretsFile): void {
|
|
77
|
-
|
|
104
|
+
if (s.version !== SECRETS_VERSION || !s.secrets || typeof s.secrets !== 'object'
|
|
105
|
+
|| Array.isArray(s.secrets) || Object.values(s.secrets).some(value => typeof value !== 'string')) {
|
|
106
|
+
throw new Error('invalid secrets file version or contents');
|
|
107
|
+
}
|
|
108
|
+
const path = secretsPath();
|
|
109
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
110
|
+
const release = acquireLockSync(`${path}.lock`);
|
|
111
|
+
try {
|
|
112
|
+
writeSecretsFile(s);
|
|
113
|
+
} finally {
|
|
114
|
+
release();
|
|
115
|
+
}
|
|
78
116
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { delimiter, join, win32 } from 'node:path';
|
|
2
|
+
import { applyDoctorFixes, planDoctorFixes, type DoctorFixDeps, type DoctorFixItem } from '../cli/doctor-fix.js';
|
|
3
|
+
import { staticToolBinDir } from '../cli/doctor-static-tools.js';
|
|
4
|
+
import { debug } from '../debug/log.js';
|
|
5
|
+
|
|
6
|
+
const AUTOMATIC = new Set<DoctorFixItem['id']>(['private-files', 'key-cache-permissions', 'static-tools']);
|
|
7
|
+
const DEFERRED = new Set<DoctorFixItem['id']>(['install-path', 'bun-tmpdir']);
|
|
8
|
+
const LATER_LINE = '나중에: elanous doctor --fix (셸 시작 파일 수정)';
|
|
9
|
+
|
|
10
|
+
export interface FirstRunDoctorResult {
|
|
11
|
+
fixed: string[];
|
|
12
|
+
skipped: string[];
|
|
13
|
+
failed: string[];
|
|
14
|
+
later: string[];
|
|
15
|
+
lines: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** 첫 실행에서는 동의가 필요 없는 수리만 실행하고, 셸 시작 파일은 손대지 않는다. */
|
|
19
|
+
export function runFirstRunDoctor(deps: DoctorFixDeps): FirstRunDoctorResult {
|
|
20
|
+
const result: FirstRunDoctorResult = { fixed: [], skipped: [], failed: [], later: [], lines: [] };
|
|
21
|
+
const record = (id: string, outcome: 'fixed' | 'skipped' | 'failed', reason?: string) => {
|
|
22
|
+
result[outcome].push(id);
|
|
23
|
+
if (outcome === 'failed') result.lines.push(`doctor: ${id} 수리 실패 — ${reason || '원인 불명'}`);
|
|
24
|
+
try { debug.log('doctor.first-run', outcome, { id }); } catch { /* 관측 실패가 첫 실행을 막지 않는다. */ }
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const windows = deps.readiness?.platform === 'win32';
|
|
29
|
+
const prefix = deps.readiness?.installPrefix;
|
|
30
|
+
const bin = typeof prefix === 'string' && prefix.trim()
|
|
31
|
+
? windows ? win32.join(prefix.trim(), 'bin') : join(prefix.trim(), 'bin')
|
|
32
|
+
: staticToolBinDir(deps.env ?? process.env, deps.home);
|
|
33
|
+
const separator = windows ? ';' : delimiter;
|
|
34
|
+
const entries = (process.env.PATH ?? '').split(separator);
|
|
35
|
+
if (!entries.some((entry) => windows ? entry.toLowerCase() === bin.toLowerCase() : entry === bin)) {
|
|
36
|
+
process.env.PATH = [bin, process.env.PATH].filter(Boolean).join(separator);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const plan = planDoctorFixes(deps);
|
|
40
|
+
for (const item of plan.items) {
|
|
41
|
+
if (DEFERRED.has(item.id) && item.status === 'fixable') result.later.push(item.id);
|
|
42
|
+
}
|
|
43
|
+
const applied = applyDoctorFixes(deps, true, AUTOMATIC);
|
|
44
|
+
for (const item of applied.items) {
|
|
45
|
+
if (AUTOMATIC.has(item.id) && item.result === 'failed') record(item.id, 'failed', item.reason);
|
|
46
|
+
else if (AUTOMATIC.has(item.id) && item.result === 'fixed') record(item.id, 'fixed');
|
|
47
|
+
else record(item.id, 'skipped');
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
record('doctor', 'failed', '계획 또는 적용 중 예외');
|
|
51
|
+
}
|
|
52
|
+
if (result.later.length) result.lines.push(LATER_LINE);
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import { randomUUID } from 'node:crypto';
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import * as store from './feature-store.js';
|
|
5
4
|
import { debug } from '../debug/log.js';
|
|
6
|
-
import { releaseLedgerRoot } from '../instance/resolve.js';
|
|
7
5
|
import { CliUserError } from '../cli/cli-user-error.js';
|
|
8
6
|
|
|
9
7
|
export type ChecklistStatus = 'green' | 'yellow' | 'red' | 'done';
|
|
@@ -43,67 +41,12 @@ export function devVersion(): string {
|
|
|
43
41
|
return (JSON.parse(readFileSync(join(import.meta.dir, '..', '..', 'package.json'), 'utf8')) as { version: string }).version;
|
|
44
42
|
}
|
|
45
43
|
|
|
46
|
-
function releasedVersion(): string {
|
|
47
|
-
const dir = join(releaseLedgerRoot(), 'release');
|
|
48
|
-
if (!existsSync(dir)) return '';
|
|
49
|
-
return readdirSync(dir).filter((v) => /^\d+\.\d+\.\d+$/.test(v) && existsSync(join(dir, v, 'release.json')))
|
|
50
|
-
.filter((v) => {
|
|
51
|
-
try { const record = JSON.parse(readFileSync(join(dir, v, 'release.json'), 'utf8')) as { version?: string; publishedAt?: string }; return record.version === v && typeof record.publishedAt === 'string'; }
|
|
52
|
-
catch { return false; }
|
|
53
|
-
})
|
|
54
|
-
.sort((a, b) => {
|
|
55
|
-
const aa = a.split('.').map(Number), bb = b.split('.').map(Number);
|
|
56
|
-
return (bb[0]! - aa[0]!) || (bb[1]! - aa[1]!) || (bb[2]! - aa[2]!);
|
|
57
|
-
})[0] ?? '';
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function pathFor(v: string): string {
|
|
61
|
-
if (!/^\d+\.\d+\.\d+(?:-(?:rc|alpha|beta)\.\d+)?$/.test(v)) throw new CliUserError(`체크리스트 판이 아니다: ${v}`);
|
|
62
|
-
return join(releaseLedgerRoot(), 'release', v, 'checklist.json');
|
|
63
|
-
}
|
|
64
|
-
|
|
65
44
|
export function listChecklist(v: string): Checklist {
|
|
66
|
-
|
|
67
|
-
if (existsSync(path)) {
|
|
68
|
-
const data = JSON.parse(readFileSync(path, 'utf8')) as Checklist;
|
|
69
|
-
refresh(data);
|
|
70
|
-
return data;
|
|
71
|
-
}
|
|
72
|
-
return { version: v, released: releasedVersion(), dev: devVersion(), items: [], history: [] };
|
|
45
|
+
return store.list(v);
|
|
73
46
|
}
|
|
74
47
|
|
|
75
|
-
// SQLite's OS-backed write lock is released when the owning process exits, even without a finally block.
|
|
76
|
-
// Hold it across the JSON read/modify/atomic-rename sequence so writers never use stale snapshots.
|
|
77
48
|
function mutate(v: string, apply: (data: Checklist) => boolean): Checklist {
|
|
78
|
-
|
|
79
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
80
|
-
const lock = `${path}.mutex.sqlite`;
|
|
81
|
-
const db = new Database(lock, { create: true, strict: true });
|
|
82
|
-
try {
|
|
83
|
-
chmodSync(lock, 0o600);
|
|
84
|
-
db.exec('PRAGMA busy_timeout = 10000');
|
|
85
|
-
db.exec('BEGIN IMMEDIATE');
|
|
86
|
-
try {
|
|
87
|
-
const data = listChecklist(v);
|
|
88
|
-
if (apply(data)) save(v, data);
|
|
89
|
-
db.exec('COMMIT');
|
|
90
|
-
return data;
|
|
91
|
-
} catch (error) {
|
|
92
|
-
db.exec('ROLLBACK');
|
|
93
|
-
throw error;
|
|
94
|
-
}
|
|
95
|
-
} finally { db.close(); }
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function save(v: string, data: Checklist): void {
|
|
99
|
-
const path = pathFor(v);
|
|
100
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
101
|
-
const temp = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
102
|
-
try {
|
|
103
|
-
writeFileSync(temp, `${JSON.stringify(data, null, 2)}\n`, { mode: 0o600 });
|
|
104
|
-
chmodSync(temp, 0o600);
|
|
105
|
-
renameSync(temp, path);
|
|
106
|
-
} finally { if (existsSync(temp)) rmSync(temp); }
|
|
49
|
+
return store.mutate(v, store.releasedVersion(), devVersion(), apply);
|
|
107
50
|
}
|
|
108
51
|
|
|
109
52
|
function change(data: Checklist, id: string, field: string, from: unknown, to: unknown, by: string, at: string): void {
|
|
@@ -111,8 +54,6 @@ function change(data: Checklist, id: string, field: string, from: unknown, to: u
|
|
|
111
54
|
debug.log('release-loop.checklist', 'change', { version: data.version, id, field, from: from ?? null, to: to ?? null, by });
|
|
112
55
|
}
|
|
113
56
|
|
|
114
|
-
function refresh(data: Checklist): void { data.released = releasedVersion(); data.dev = devVersion(); }
|
|
115
|
-
|
|
116
57
|
export function addItem(v: string, input: { id: string; title: string; owner?: string; kind?: ChecklistKind }): Checklist {
|
|
117
58
|
if (!input.id.trim()) throw new CliUserError('칸 id 가 비었다');
|
|
118
59
|
if (!input.title.trim()) throw new CliUserError('칸 제목이 비었다');
|