cc-viewer 1.8.20 → 1.9.1
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/cli.js +33 -4
- package/dist/assets/App-DM-fbhNd.js +2 -0
- package/dist/assets/App-DOZI5HME.css +1 -0
- package/dist/assets/{MdxEditorPanel-Dpn-4ydO.css → MdxEditorPanel-DTfdj6GS.css} +1 -1
- package/dist/assets/{MdxEditorPanel-Dsnv2wIw.js → MdxEditorPanel-DjwOQDh0.js} +1 -1
- package/dist/assets/Mobile-BD-hefIH.js +1 -0
- package/dist/assets/ProxyStatsModal-pYa_0QVL.js +1 -0
- package/dist/assets/{_baseUniq-B3e4JaL0.js → _baseUniq-CwQykUuc.js} +1 -1
- package/dist/assets/{arc-DnoPn_qY.js → arc-EdUdBxtG.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-DrPKTbxH.js → architectureDiagram-Q4EWVU46-DtBIdElY.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-CEaO0Vcw.js → blockDiagram-DXYQGD6D-guNZIKkZ.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-BLaawFeg.js → c4Diagram-AHTNJAMY-xV5vB-Z9.js} +1 -1
- package/dist/assets/{channel-DnYR1fF4.js → channel-DYEktNap.js} +1 -1
- package/dist/assets/{chunk-4BX2VUAB-Bfd4edKZ.js → chunk-4BX2VUAB-ZTkItpvj.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-t2nMQmY7.js → chunk-4TB4RGXK-dYIewyyR.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-C6tLOjGX.js → chunk-55IACEB6-CSsPNgPX.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-dWCvxbCb.js → chunk-EDXVE4YY-CnMiuqzQ.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-BHELZLPK.js → chunk-FMBD7UC4-CVpATPZ3.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-C8XTkH1S.js → chunk-OYMX7WX6-Dpq4s8dW.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-CD0j6SAs.js → chunk-QZHKN3VN-Bnojh6X6.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-D4dKfXJJ.js → chunk-YZCP3GAM-BYqK4Dsa.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-ClGF33bF.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-ClGF33bF.js +1 -0
- package/dist/assets/clone-CFj55h49.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-BF2HsiSD.js → cose-bilkent-S5V4N54A-OJ7mmWyV.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-f3h967yx.js → dagre-KV5264BT-B1Kwd_Rk.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-B9PzHQfh.js → diagram-5BDNPKRD-D8qrw8sp.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-kR953Bqm.js → diagram-G4DWMVQ6-ip-N4ho8.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-DAewa2eg.js → diagram-MMDJMWI5-2PZaskUF.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-C8HNTH6I.js → diagram-TYMM5635-CeAOQUZP.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-BQ5P8wWK.js → erDiagram-SMLLAGMA-BCBaEYEb.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-BIpEkrMR.js → flowDiagram-DWJPFMVM-CsGyIqcX.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-CWqSfB5v.js → ganttDiagram-T4ZO3ILL-COsj3FZ-.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-Ba-9yEbQ.js → gitGraphDiagram-UUTBAWPF-DvQVQGJG.js} +1 -1
- package/dist/assets/{graph-D3zXKQ6P.js → graph-BSB9fDSP.js} +1 -1
- package/dist/assets/{index-G3xd-gLo.js → index-BRw4Iz2O.js} +1 -1
- package/dist/assets/{index-DClD_EkN.js → index-Bo-12bug.js} +1 -1
- package/dist/assets/{index-DSd5P3WQ.js → index-BuVComcY.js} +1 -1
- package/dist/assets/{index-HLrnYsI1.js → index-CNNAYjkF.js} +1 -1
- package/dist/assets/{index-Bm3A5VLb.js → index-DKufsAzx.js} +1 -1
- package/dist/assets/{index-C9N0rZ7Z.js → index-DXBfwPYD.js} +1 -1
- package/dist/assets/index-DkIhbLjr.js +2 -0
- package/dist/assets/{index-Nvk8r3tO.css → index-Dz36p_bA.css} +1 -1
- package/dist/assets/{index-Q8wp4Nof.js → index-eM2ih5n7.js} +1 -1
- package/dist/assets/{infoDiagram-42DDH7IO-Cxm79ZJR.js → infoDiagram-42DDH7IO-eNucS4iW.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-Cd0vZP1B.js → ishikawaDiagram-UXIWVN3A-Dxxj-gUX.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-B2b57HXk.js → journeyDiagram-VCZTEJTY-yJJzVEBb.js} +1 -1
- package/dist/assets/{jszip.min-Bj5uiEb2.js → jszip.min-ByI0ds5u.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-YCERiUCO.js → kanban-definition-6JOO6SKY-2rfTi2zw.js} +1 -1
- package/dist/assets/{layout-Do5TlO2U.js → layout-DTmHj0Wm.js} +1 -1
- package/dist/assets/{linear-BiJNTDgN.js → linear-BOA3a9FE.js} +1 -1
- package/dist/assets/{mermaid.core-Dg0McRZt.js → mermaid.core-5H-NR79H.js} +3 -3
- package/dist/assets/{min-T51iefht.js → min-CpjFTEec.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-Cu_GJxY3.js → mindmap-definition-QFDTVHPH-B8KCjFgB.js} +1 -1
- package/dist/assets/{pieDiagram-DEJITSTG-DldHHZXi.js → pieDiagram-DEJITSTG-BARy96g5.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-BzyTPNAJ.js → quadrantDiagram-34T5L4WZ-DYoTapuI.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-C5xxFkvG.js → requirementDiagram-MS252O5E-EeANLLK2.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-OxdjoaE1.js → sankeyDiagram-XADWPNL6-DmQ-YVPj.js} +1 -1
- package/dist/assets/seqResourceLoaders-BdWMUSrC.css +41 -0
- package/dist/assets/seqResourceLoaders-Dzy1J6G9.js +2 -0
- package/dist/assets/{sequenceDiagram-FGHM5R23-C7c89uIS.js → sequenceDiagram-FGHM5R23-SZtW0rYC.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-ssdJnDrO.js → stateDiagram-FHFEXIEX-CEygsIld.js} +1 -1
- package/dist/assets/{stateDiagram-v2-QKLJ7IA2-BP-0WClK.js → stateDiagram-v2-QKLJ7IA2-BU1XPhBe.js} +1 -1
- package/dist/assets/{timeline-definition-GMOUNBTQ-sWScEpSM.js → timeline-definition-GMOUNBTQ-DhPBaQBt.js} +1 -1
- package/dist/assets/{vendor-antd-DKIytXho.js → vendor-antd-CS_S2mBN.js} +1 -1
- package/dist/assets/{vendor-codemirror-4qgXq03k.js → vendor-codemirror-B5l-VbBO.js} +1 -1
- package/dist/assets/{vendor-mdxeditor-D5Z5D9fq.js → vendor-mdxeditor-Cz09a1_I.js} +2 -2
- package/dist/assets/{vendor-qrcode-B7lb5qk1.js → vendor-qrcode-qbbntD6-.js} +1 -1
- package/dist/assets/{vendor-virtuoso-BqoXlbhH.js → vendor-virtuoso-Sk8fQ4Hx.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-DNfXR0ge.js → vennDiagram-DHZGUBPP-NniTHxDw.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-JXd--CRD.js → wardley-RL74JXVD-D9SNOrDw.js} +1 -1
- package/dist/assets/{wardleyDiagram-NUSXRM2D-DHmNyVPM.js → wardleyDiagram-NUSXRM2D-DvHXKNza.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-CTb0gltZ.js → xychartDiagram-5P7HB3ND-CaiwPTQB.js} +1 -1
- package/dist/index.html +5 -5
- package/package.json +1 -1
- package/server/interceptor.js +119 -13
- package/server/lib/chat-queue.js +67 -4
- package/server/lib/same-origin.js +22 -0
- package/server/lib/v2/convert.js +1 -1
- package/server/lib/v2/layout.js +28 -3
- package/server/lib/v2/live-feed.js +41 -4
- package/server/lib/v2/resume-list.js +150 -0
- package/server/lib/v2/session-select.js +63 -14
- package/server/lib/v2/v2-writer.js +172 -34
- package/server/lib/v2/view-router.js +83 -0
- package/server/lib/view-root.js +209 -0
- package/server/pty-manager.js +790 -159
- package/server/routes/ask-perm.js +21 -1
- package/server/routes/events.js +123 -7
- package/server/routes/expert.js +46 -14
- package/server/routes/files-content.js +72 -7
- package/server/routes/files-fs.js +28 -3
- package/server/routes/git.js +31 -5
- package/server/routes/preferences.js +1 -16
- package/server/routes/project-meta.js +32 -3
- package/server/routes/proxy-stats.js +33 -5
- package/server/routes/resume.js +302 -0
- package/server/routes/search.js +23 -1
- package/server/routes/skills.js +18 -2
- package/server/routes/v2.js +6 -1
- package/server/routes/workspaces.js +10 -3
- package/server/server.js +191 -25
- package/server/system-prompt-templates/presets/kimi-k3.md +1 -1
- package/server/workspace-registry.js +15 -1
- package/dist/assets/App-CesjoFSv.js +0 -2
- package/dist/assets/App-M61pqQEx.css +0 -1
- package/dist/assets/Mobile-bvGZ392d.js +0 -1
- package/dist/assets/ProxyStatsModal-HFk51cI8.js +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-BecPAAuk.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-BecPAAuk.js +0 -1
- package/dist/assets/clone-B0_ghF3F.js +0 -1
- package/dist/assets/index-DanpP6qP.js +0 -2
- package/dist/assets/seqResourceLoaders-BumAjQDW.css +0 -41
- package/dist/assets/seqResourceLoaders-ByHQMNIi.js +0 -2
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// View-root resolution (multi-project, 2026-10): map an optional `?project=`
|
|
2
|
+
// name to the REAL filesystem root of that project, so project-scoped READ
|
|
3
|
+
// routes (file tree / git / skills / memory / stats / search) can follow the
|
|
4
|
+
// viewed parallel project instead of always the bound one.
|
|
5
|
+
//
|
|
6
|
+
// Name → path sources (both consulted, in order):
|
|
7
|
+
// 1. workspace-registry (loadWorkspaces) — persistent { path, projectName },
|
|
8
|
+
// works even when the project has no live PTY;
|
|
9
|
+
// 2. pty-manager live records (listLivePtys) — currentWorkspacePath of a
|
|
10
|
+
// running/exited-but-unreaped record.
|
|
11
|
+
// The bound project is the fallback when no override is given (byte-identical
|
|
12
|
+
// to the pre-multi-project behavior).
|
|
13
|
+
//
|
|
14
|
+
// Name collision arbitration (2026-10-05): two different real dirs can share
|
|
15
|
+
// one basename-derived project name (e.g. ~/proj and ~/work/proj both named
|
|
16
|
+
// "proj"). Never silently pick a WRONG one — but a single live PTY is the
|
|
17
|
+
// authoritative answer (a view attach means "look at the running project"),
|
|
18
|
+
// and a registry entry whose path no longer exists is dead weight. Order:
|
|
19
|
+
// 1. exactly one live PTY candidate → use it;
|
|
20
|
+
// 2. no live PTY → drop registry candidates whose dir is gone;
|
|
21
|
+
// one survivor → use it;
|
|
22
|
+
// 3. still >1 real candidate → 400, never a silent guess.
|
|
23
|
+
//
|
|
24
|
+
// Pure + dependency-injected (fs/registry/pty seams) so it is directly
|
|
25
|
+
// node-testable. WRITE routes must NOT use this — they stay bound-only.
|
|
26
|
+
|
|
27
|
+
import { realpathSync, existsSync, statSync } from 'node:fs';
|
|
28
|
+
import { projectKeyForCwd } from './system-prompt-snapshots.js';
|
|
29
|
+
|
|
30
|
+
// A project name must be sanitizer-stable: projectKeyForCwd maps every legal
|
|
31
|
+
// basename to [a-zA-Z0-9_\-\.] (collapsing only ALL-dot degenerate names to
|
|
32
|
+
// '_'), so a name that still contains anything else could never match a
|
|
33
|
+
// registered/live candidate anyway. Reject it here (along with separators and
|
|
34
|
+
// control chars) as a 400 — never a lookup that can only 404. Interior dots
|
|
35
|
+
// (`foo..bar`, `v2.3`) are legal basenames and pass the round-trip check;
|
|
36
|
+
// traversal is already impossible because separators are rejected.
|
|
37
|
+
function isValidProjectName(name) {
|
|
38
|
+
if (typeof name !== 'string' || !name) return false;
|
|
39
|
+
if (name.includes('/') || name.includes('\\') || name.includes('\0')) return false;
|
|
40
|
+
return projectKeyForCwd(name) === name;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {object} opts
|
|
45
|
+
* @param {string} [opts.projectParam] - the `?project=` override (may be ''/undefined).
|
|
46
|
+
* @param {string} [opts.instanceParam] - the `?instance=` per-spawn instanceKey override
|
|
47
|
+
* (may be ''/undefined). Resolved BEFORE the name path: a valid live instance pins its
|
|
48
|
+
* exact cwd (via 'live-instance'), bypassing same-basename name arbitration.
|
|
49
|
+
* @param {string} opts.boundCwd - the bound project root (CCV_PROJECT_DIR || process.cwd()).
|
|
50
|
+
* @param {() => Array<{path:string, projectName:string}>} opts.loadWorkspaces - registry reader.
|
|
51
|
+
* @param {() => Array<{cwd:string, instanceKey?:string}>} opts.listLivePtys - live PTY records.
|
|
52
|
+
* @returns {{ ok:true, root:string, via:'bound'|'registry'|'live'|'live-instance' } | { ok:false, status:number, error:string }}
|
|
53
|
+
*/
|
|
54
|
+
export function resolveViewRoot({ projectParam, instanceParam, boundCwd, loadWorkspaces, listLivePtys } = {}) {
|
|
55
|
+
const bound = boundCwd || process.cwd();
|
|
56
|
+
// Multi-instance (2026-10-06): `instance` is the per-spawn instanceKey, the
|
|
57
|
+
// authoritative disambiguator when two same-basename projects run at once —
|
|
58
|
+
// the name path below would 400 on that pair as "ambiguous". Resolve it FIRST
|
|
59
|
+
// (before the empty-name bound short-circuit AND name validation): a valid live
|
|
60
|
+
// instance pins the exact cwd regardless of the accompanying name, so
|
|
61
|
+
// `?instance=` works with or without `?project=`. An invalid key (regex miss) or
|
|
62
|
+
// a stale/closed instance is NOT an error — fall through to the name path (a
|
|
63
|
+
// closed twin leaves a single same-name live, which name arbitration then wins).
|
|
64
|
+
const instance = (typeof instanceParam === 'string' && /^ccv-[0-9a-f]+$/.test(instanceParam)) ? instanceParam : '';
|
|
65
|
+
if (instance) {
|
|
66
|
+
try {
|
|
67
|
+
const rec = (listLivePtys() || []).find((p) => p && p.instanceKey === instance);
|
|
68
|
+
if (rec && typeof rec.cwd === 'string' && rec.cwd) {
|
|
69
|
+
if (existsSync(rec.cwd) && statSync(rec.cwd).isDirectory()) {
|
|
70
|
+
return { ok: true, root: realpathSync(rec.cwd), via: 'live-instance' };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} catch { /* pty map unavailable / fs race → fall through to name path */ }
|
|
74
|
+
}
|
|
75
|
+
const name = (typeof projectParam === 'string' && projectParam) || '';
|
|
76
|
+
if (!name) return { ok: true, root: bound, via: 'bound' };
|
|
77
|
+
if (!isValidProjectName(name)) {
|
|
78
|
+
return { ok: false, status: 400, error: 'invalid project name' };
|
|
79
|
+
}
|
|
80
|
+
// Bound project by name → normally the bound root (covers "view switches
|
|
81
|
+
// back"). But do NOT short-circuit past arbitration when a DIFFERENT live
|
|
82
|
+
// PTY holds the same name: bound /a/proj + parallel live /b/proj (both
|
|
83
|
+
// "proj") would otherwise silently resolve the parallel tab's ?project=proj
|
|
84
|
+
// to the bound dir — the exact wrong-project pick this module forbids. So
|
|
85
|
+
// the bound root joins the candidate pool and only wins when no other live
|
|
86
|
+
// PTY claims the name.
|
|
87
|
+
const boundMatches = projectKeyForCwd(bound) === name;
|
|
88
|
+
|
|
89
|
+
const candidates = [];
|
|
90
|
+
if (boundMatches) candidates.push({ path: bound, via: 'bound' });
|
|
91
|
+
try {
|
|
92
|
+
for (const w of loadWorkspaces() || []) {
|
|
93
|
+
if (w && w.projectName === name && typeof w.path === 'string' && w.path) {
|
|
94
|
+
candidates.push({ path: w.path, via: 'registry' });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} catch { /* registry unreadable → fall through to live */ }
|
|
98
|
+
try {
|
|
99
|
+
for (const p of listLivePtys() || []) {
|
|
100
|
+
const cwd = p && p.cwd;
|
|
101
|
+
if (typeof cwd === 'string' && cwd && projectKeyForCwd(cwd) === name) {
|
|
102
|
+
candidates.push({ path: cwd, via: 'live' });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch { /* pty map unavailable → registry-only */ }
|
|
106
|
+
|
|
107
|
+
// Dedupe by path string (a project live AND registered under the SAME path
|
|
108
|
+
// is one candidate). Keys are the raw candidate paths — not realpathSync —
|
|
109
|
+
// so two symlink aliases of one real dir stay distinct and fall through to
|
|
110
|
+
// the arbitration below rather than being merged here. When the SAME path
|
|
111
|
+
// appears in both sources keep via:'live' — the live record must still count
|
|
112
|
+
// as a live candidate in the arbitration below, otherwise a
|
|
113
|
+
// running+registered project plus one stale same-name entry would look like
|
|
114
|
+
// "no live PTY, two real candidates" and wrongly 400.
|
|
115
|
+
const byPath = new Map();
|
|
116
|
+
for (const c of candidates) {
|
|
117
|
+
const prev = byPath.get(c.path);
|
|
118
|
+
if (!prev || c.via === 'live') byPath.set(c.path, c);
|
|
119
|
+
}
|
|
120
|
+
const uniq = [...byPath.values()];
|
|
121
|
+
if (uniq.length === 0) {
|
|
122
|
+
return { ok: false, status: 404, error: 'unknown project' };
|
|
123
|
+
}
|
|
124
|
+
// Arbitration when the name maps to more than one real dir (see header):
|
|
125
|
+
// 1. A single live PTY is authoritative — an attach view means "the running
|
|
126
|
+
// project", so a live record outranks stale registry entries.
|
|
127
|
+
if (uniq.length > 1) {
|
|
128
|
+
const live = uniq.filter((c) => c.via === 'live');
|
|
129
|
+
if (live.length === 1) {
|
|
130
|
+
const root = live[0].path;
|
|
131
|
+
try {
|
|
132
|
+
if (!existsSync(root) || !statSync(root).isDirectory()) {
|
|
133
|
+
return { ok: false, status: 404, error: 'project dir missing' };
|
|
134
|
+
}
|
|
135
|
+
return { ok: true, root: realpathSync(root), via: 'live' };
|
|
136
|
+
} catch {
|
|
137
|
+
return { ok: false, status: 404, error: 'project dir unreadable' };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (live.length > 1) {
|
|
141
|
+
// Two REAL PTYs running in two different dirs under one name — no
|
|
142
|
+
// authoritative tiebreak; refuse rather than guess.
|
|
143
|
+
return { ok: false, status: 400, error: 'ambiguous project name' };
|
|
144
|
+
}
|
|
145
|
+
// 2. No live PTY: drop registry candidates whose dir is gone (dead residue
|
|
146
|
+
// from a moved/deleted checkout), then re-check the survivor count.
|
|
147
|
+
const alive = uniq.filter((c) => {
|
|
148
|
+
try { return existsSync(c.path) && statSync(c.path).isDirectory(); }
|
|
149
|
+
catch { return false; }
|
|
150
|
+
});
|
|
151
|
+
if (alive.length === 0) {
|
|
152
|
+
return { ok: false, status: 404, error: 'unknown project' };
|
|
153
|
+
}
|
|
154
|
+
if (alive.length > 1) {
|
|
155
|
+
return { ok: false, status: 400, error: 'ambiguous project name' };
|
|
156
|
+
}
|
|
157
|
+
const root = alive[0].path;
|
|
158
|
+
try {
|
|
159
|
+
return { ok: true, root: realpathSync(root), via: alive[0].via };
|
|
160
|
+
} catch {
|
|
161
|
+
return { ok: false, status: 404, error: 'project dir unreadable' };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const root = uniq[0].path;
|
|
165
|
+
try {
|
|
166
|
+
if (!existsSync(root) || !statSync(root).isDirectory()) {
|
|
167
|
+
return { ok: false, status: 404, error: 'project dir missing' };
|
|
168
|
+
}
|
|
169
|
+
return { ok: true, root: realpathSync(root), via: uniq[0].via };
|
|
170
|
+
} catch {
|
|
171
|
+
return { ok: false, status: 404, error: 'project dir unreadable' };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* HTTP-facing wrapper shared by every project-scoped READ route (multi-project,
|
|
177
|
+
* 2026-10): resolves `?project=` (or a POST-body override) via resolveViewRoot
|
|
178
|
+
* and, on failure, writes the error response itself. Returns the resolved root
|
|
179
|
+
* to continue with, or null after replying (the caller must return early).
|
|
180
|
+
*
|
|
181
|
+
* Centralizing here removes the three byte-identical per-route copies
|
|
182
|
+
* (files-content / git / skills) — routes → lib is a legal L3→L1 edge.
|
|
183
|
+
*
|
|
184
|
+
* @param {object} req - the incoming request.
|
|
185
|
+
* @param {object} res - the response (error written here on failure).
|
|
186
|
+
* @param {URL} parsedUrl - the request URL (query `project` and `instance` read here).
|
|
187
|
+
* @param {object} opts
|
|
188
|
+
* @param {string} opts.boundCwd - the bound project root (CCV_PROJECT_DIR || process.cwd()).
|
|
189
|
+
* @param {() => Array} opts.loadWorkspaces - registry reader.
|
|
190
|
+
* @param {() => Array} opts.listLivePtys - live PTY records.
|
|
191
|
+
* @param {string} [opts.bodyProject] - POST-body project override (wins over query).
|
|
192
|
+
* @param {string} [opts.bodyInstance] - POST-body instance override (wins over query).
|
|
193
|
+
* @returns {string|null} the resolved absolute root, or null (already replied).
|
|
194
|
+
*/
|
|
195
|
+
export function viewRootOrReply(req, res, parsedUrl, { boundCwd, loadWorkspaces, listLivePtys, bodyProject, bodyInstance } = {}) {
|
|
196
|
+
const r = resolveViewRoot({
|
|
197
|
+
projectParam: (typeof bodyProject === 'string' && bodyProject) || parsedUrl?.searchParams?.get('project'),
|
|
198
|
+
instanceParam: (typeof bodyInstance === 'string' && bodyInstance) || parsedUrl?.searchParams?.get('instance'),
|
|
199
|
+
boundCwd,
|
|
200
|
+
loadWorkspaces,
|
|
201
|
+
listLivePtys,
|
|
202
|
+
});
|
|
203
|
+
if (!r.ok) {
|
|
204
|
+
res.writeHead(r.status, { 'Content-Type': 'application/json' });
|
|
205
|
+
res.end(JSON.stringify({ error: r.error }));
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
return r.root;
|
|
209
|
+
}
|