dsh-thoughtdag 0.4.19 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist-app/assets/canonical-BlzQbV3x.js +2 -0
- package/dist-app/assets/canvas-record-CHx9KBrZ.js +1 -0
- package/dist-app/assets/{claude-code-session-D7tlxs4D.js → claude-code-session-BfwTY3ne.js} +4 -4
- package/dist-app/assets/{codex-session-DJn3XLgK.js → codex-session-BNnk6Ozy.js} +4 -4
- package/dist-app/assets/{dsh-session-DWyrhHLx.js → dsh-session-BXjdZH-4.js} +4 -4
- package/dist-app/assets/{experiment-loop-BB8N8okB.js → experiment-loop-CBfcRt8t.js} +1 -1
- package/dist-app/assets/http-bridge-CK02uyVL.js +1 -0
- package/dist-app/assets/index-93ujsGjx.css +1 -0
- package/dist-app/assets/index-F8ykUu-e.js +883 -0
- package/dist-app/assets/{index-DLrvVMFh.js → index-U1FDd3d6.js} +1 -1
- package/dist-app/assets/{index-BGRoRzbH.js → index-Vln2-D4p.js} +3 -3
- package/dist-app/assets/{index-DRLN4EE4.js → index-no3SFEm4.js} +1 -1
- package/dist-app/assets/{pi-session-5Tb178Vi.js → pi-session-DfX1QntM.js} +8 -8
- package/dist-app/assets/{sensitive-scan-DoCoBRf1.js → sensitive-scan-NeBfDua2.js} +1 -1
- package/dist-app/assets/{session-handoff-BssCXpUd.js → session-handoff-Bi6J6owx.js} +2 -2
- package/dist-app/assets/{shared-CDDxGtwj.js → shared-B_zE0vyL.js} +4 -4
- package/dist-app/assets/{turndown-plugin-gfm.cjs-CyxpTA69.js → turndown-plugin-gfm.cjs-t7mTv0d4.js} +3 -3
- package/dist-app/assets/{update-check-CwxfRcEJ.js → update-check-CmPLQTC0.js} +1 -1
- package/dist-app/index.html +2 -2
- package/lib/client.js +34 -7
- package/lib/index.js +50 -1
- package/lib/runtime/agents/http.cjs +0 -5
- package/lib/runtime/agents/ops.cjs +1 -35
- package/lib/why.mjs +567 -16
- package/package.json +3 -2
- package/dist-app/assets/canonical-C8GEhZR5.js +0 -2
- package/dist-app/assets/canvas-record-6cBZqSEI.js +0 -1
- package/dist-app/assets/http-bridge-DqMM7zer.js +0 -1
- package/dist-app/assets/index-C9qk6fjz.css +0 -1
- package/dist-app/assets/index-DWMhL6gr.js +0 -789
- package/dist-app/assets/live-mirror-DLoRGse9.js +0 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
import{t as u,a as n,f}from"./index-
|
|
1
|
+
import{t as u,a as n,f}from"./index-F8ykUu-e.js";const h=15*6e4;let i=!1;const d=new URLSearchParams(window.location.search),s=d.get("dv"),p=d.get("su")==="1",w=30*6e4;let c=!1,r=0;async function a(){if(!(!s||p||c)&&!(Date.now()-r<w)){r=Date.now();try{const t=await fetch("https://api.github.com/repos/chenxiachan/thoughtdag/releases?per_page=1");if(!t.ok)return;const o=(await t.json())?.[0]?.tag_name?.replace(/^v/,"");if(!o||o===s)return;c=!0,u("info",f(n("update.desktopAvailable"),{v:o}),0,{label:n("update.desktopDownload"),run:()=>window.open("https://chenxiachan.github.io/thoughtdag/#download","_blank")})}catch{}}}function v(){for(const t of document.querySelectorAll("script[src]")){const e=t.src.match(/assets\/index-[^/?#]+\.js/);if(e)return e[0]}return null}async function m(){try{const t=await fetch(`${window.location.pathname}?u=${Date.now()}`,{cache:"no-store"});if(!t.ok)return null;const e=(await t.text()).match(/assets\/index-[^"'?#]+\.js/);return e?e[0]:null}catch{return null}}async function l(){if(i)return;const t=v();if(!t)return;const e=await m();e&&e!==t&&(i=!0,u("info",n("update.available"),0,{label:n("update.refresh"),run:()=>window.location.reload()}))}function b(){document.addEventListener("visibilitychange",()=>{document.visibilityState==="visible"&&(l(),a())}),setInterval(()=>{l(),a()},h),s&&setTimeout(()=>{a()},5e3)}export{b as bootUpdateCheck};
|
package/dist-app/index.html
CHANGED
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
<meta name="twitter:title" content="ThoughtDAG — Your thinking deserves a map" />
|
|
23
23
|
<meta name="twitter:description" content="Wires are the context: an infinite canvas where LLM conversations grow into an editable thought graph. No signup; the example canvas needs no key." />
|
|
24
24
|
<meta name="twitter:image" content="https://app.thoughtdag.workers.dev/og-twitter.jpg" />
|
|
25
|
-
<script type="module" crossorigin src="/thoughtdag/assets/index-
|
|
26
|
-
<link rel="stylesheet" crossorigin href="/thoughtdag/assets/index-
|
|
25
|
+
<script type="module" crossorigin src="/thoughtdag/assets/index-F8ykUu-e.js"></script>
|
|
26
|
+
<link rel="stylesheet" crossorigin href="/thoughtdag/assets/index-93ujsGjx.css">
|
|
27
27
|
</head>
|
|
28
28
|
<body>
|
|
29
29
|
<div id="root"></div>
|
package/lib/client.js
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
// "思维图", shows a full-screen SAME-ORIGIN iframe at /thoughtdag/ (the SPA
|
|
4
4
|
// is served by the host half on the same web server — no CORS, no second
|
|
5
5
|
// origin). The switch floats on purpose: it is there before any session is
|
|
6
|
-
// open
|
|
7
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// open (0.4.18 moved it into the session header, which hid it on the empty
|
|
7
|
+
// page; withdrawn in 0.4.19). While the canvas is up, the SPA shows the same
|
|
8
|
+
// switch in its own top bar, next to the canvas chip, so nothing floats over
|
|
9
|
+
// the canvas toolbar at any width: the pill hides once the SPA has booted
|
|
10
|
+
// and comes back when the canvas closes (0.4.20). All conversation smarts live inside the ThoughtDAG app; this file
|
|
9
11
|
// only opens the door and forwards the current session id so the canvas can
|
|
10
12
|
// offer to mirror it.
|
|
11
13
|
//
|
|
@@ -18,7 +20,9 @@ window.__ModuleLoader__.load({
|
|
|
18
20
|
factory: () => {
|
|
19
21
|
const module = { exports: {} }
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
// uiWorkspace: the harness's directory picker (the OS dialog when the
|
|
24
|
+
// harness runs on this machine, its in-app browser when reached remotely)
|
|
25
|
+
module.exports.inject = ['sessions', 'uiWorkspace']
|
|
22
26
|
module.exports.apply = ctx => {
|
|
23
27
|
const currentSession = () => {
|
|
24
28
|
const snapshot = ctx.sessions.list.getSnapshot()
|
|
@@ -29,7 +33,7 @@ window.__ModuleLoader__.load({
|
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
const style = document.createElement('style')
|
|
32
|
-
style.textContent = '.dsh-td-switch{position:fixed;z-index:120;top:12px;left:50%;display:flex;gap:2px;transform:translateX(-50%);border:1px solid #d1d5db;border-radius:999px;background:rgba(255,255,255,.96);padding:3px;backdrop-filter:blur(10px)}.dsh-td-switch button{height:28px;border:0;border-radius:999px;background:transparent;padding:0 11px;color:#6b7280;font:600 12px Inter,system-ui,sans-serif;cursor:pointer;white-space:nowrap}.dsh-td-switch button:hover{background:#f3f4f6;color:#111827}.dsh-td-switch button.active{background:#111827;color:#fff}.dsh-td-overlay{position:fixed;z-index:100;inset:0;background:#faf9f7}.dsh-td-overlay[hidden]{display:none}.dsh-td-overlay iframe{display:block;width:100%;height:100%;border:0}'
|
|
36
|
+
style.textContent = '.dsh-td-switch{position:fixed;z-index:120;top:12px;left:50%;display:flex;gap:2px;transform:translateX(-50%);border:1px solid #d1d5db;border-radius:999px;background:rgba(255,255,255,.96);padding:3px;backdrop-filter:blur(10px)}.dsh-td-switch button{height:28px;border:0;border-radius:999px;background:transparent;padding:0 11px;color:#6b7280;font:600 12px Inter,system-ui,sans-serif;cursor:pointer;white-space:nowrap}.dsh-td-switch button:hover{background:#f3f4f6;color:#111827}.dsh-td-switch button.active{background:#111827;color:#fff}.dsh-td-switch[hidden]{display:none}.dsh-td-overlay{position:fixed;z-index:100;inset:0;background:#faf9f7}.dsh-td-overlay[hidden]{display:none}.dsh-td-overlay iframe{display:block;width:100%;height:100%;border:0}'
|
|
33
37
|
document.head.append(style)
|
|
34
38
|
|
|
35
39
|
const host = document.createElement('div')
|
|
@@ -40,6 +44,8 @@ window.__ModuleLoader__.load({
|
|
|
40
44
|
let pluginVersion = null
|
|
41
45
|
fetch('/thoughtdag/api/version').then(r => (r.ok ? r.json() : null)).then(j => { if (j && typeof j.version === 'string') pluginVersion = j.version }).catch(() => {})
|
|
42
46
|
|
|
47
|
+
const switchEl = host.querySelector('.dsh-td-switch')
|
|
48
|
+
let spaReady = false
|
|
43
49
|
const dialogBtn = host.querySelector('[data-view="dialog"]')
|
|
44
50
|
const mapBtn = host.querySelector('[data-view="map"]')
|
|
45
51
|
const overlay = host.querySelector('.dsh-td-overlay')
|
|
@@ -51,7 +57,7 @@ window.__ModuleLoader__.load({
|
|
|
51
57
|
mapBtn.classList.toggle('active', map)
|
|
52
58
|
mapBtn.setAttribute('aria-pressed', String(map))
|
|
53
59
|
}
|
|
54
|
-
const close = () => { overlay.hidden = true; setView(false); send('td:view', { shown: false }) }
|
|
60
|
+
const close = () => { overlay.hidden = true; switchEl.hidden = false; setView(false); send('td:view', { shown: false }) }
|
|
55
61
|
const send = (type, payload) => frame.contentWindow?.postMessage({ source: 'dsh-thoughtdag', type, ...payload }, location.origin)
|
|
56
62
|
|
|
57
63
|
const syncCurrent = () => {
|
|
@@ -62,6 +68,7 @@ window.__ModuleLoader__.load({
|
|
|
62
68
|
mapBtn.addEventListener('click', () => {
|
|
63
69
|
overlay.hidden = false
|
|
64
70
|
setView(true)
|
|
71
|
+
if (spaReady) switchEl.hidden = true
|
|
65
72
|
// the SPA boots on first open, never while hidden: a canvas that
|
|
66
73
|
// measures itself inside a display:none frame fits its view to a 0×0
|
|
67
74
|
// box and shows nothing when revealed
|
|
@@ -76,7 +83,27 @@ window.__ModuleLoader__.load({
|
|
|
76
83
|
window.addEventListener('message', event => {
|
|
77
84
|
if (event.origin !== location.origin || event.data?.source !== 'dsh-thoughtdag') return
|
|
78
85
|
if (event.data.type === 'td:close') return close()
|
|
79
|
-
|
|
86
|
+
// the canvas asks for a working directory: open the harness's own
|
|
87
|
+
// picker and hand the path back (null when cancelled; unsupported
|
|
88
|
+
// when this runtime has no picker, so the canvas shows a typed field)
|
|
89
|
+
if (event.data.type === 'td:pick-cwd') {
|
|
90
|
+
const requestId = event.data.requestId
|
|
91
|
+
const pick = ctx.uiWorkspace?.pickDirectory
|
|
92
|
+
if (typeof pick !== 'function') return send('td:picked-cwd', { requestId, path: null, unsupported: true })
|
|
93
|
+
// null = cancelled; a throw = this harness has no OS dialog to show
|
|
94
|
+
// (reached remotely, its picker is the in-app browse kind): the
|
|
95
|
+
// canvas then offers a typed path instead
|
|
96
|
+
Promise.resolve(pick.call(ctx.uiWorkspace))
|
|
97
|
+
.then(path => send('td:picked-cwd', { requestId, path: typeof path === 'string' ? path : null }))
|
|
98
|
+
.catch(() => send('td:picked-cwd', { requestId, path: null, unsupported: true }))
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
if (event.data.type === 'td:request-current') {
|
|
102
|
+
// the SPA has booted and shows its own switch: the pill steps aside
|
|
103
|
+
spaReady = true
|
|
104
|
+
if (!overlay.hidden) switchEl.hidden = true
|
|
105
|
+
return syncCurrent()
|
|
106
|
+
}
|
|
80
107
|
// the canvas forked or continued a session: stage it and go back to the
|
|
81
108
|
// chat, which now shows exactly the context the canvas produced
|
|
82
109
|
if (event.data.type === 'td:select-session' && typeof event.data.session === 'string') {
|
package/lib/index.js
CHANGED
|
@@ -797,7 +797,7 @@ async function runAgentTurn(ctx, body, emit, isClosed, { answerApprovals = true
|
|
|
797
797
|
const text = blocks.filter(b => b.type === 'text').map(b => b.text).join('')
|
|
798
798
|
if (text) { fullText += text; emit({ text }) }
|
|
799
799
|
}
|
|
800
|
-
} else if (event.type === 'tool/call' || event.type === 'tool/code-dispatch-start') {
|
|
800
|
+
} else if (event.type === 'tool/call' || event.type === 'tool/code-dispatch-start' || event.type === 'tool/ptc-dispatch-start') {
|
|
801
801
|
if (event.data?.callId) turnEntry.calls.set(event.data.callId, { name: event.data?.name ?? 'tool', arguments: event.data?.arguments })
|
|
802
802
|
emit({ tool: { name: event.data?.name ?? 'tool', query: toolQuery(event.data?.name, event.data?.arguments) } })
|
|
803
803
|
} else if (event.type === 'turn/end') {
|
|
@@ -876,6 +876,9 @@ const WHY_PROMPT = `ThoughtDAG why layer. The tools why_check, why_file, why_fin
|
|
|
876
876
|
- why_find(phrase) finds where exact words were asked or answered; why_recall(session, turn) reads one turn in full.
|
|
877
877
|
Cite what you learn briefly; do not restate whole turns.`
|
|
878
878
|
|
|
879
|
+
/** the loaded why library, for the HTTP routes the canvas asks (null until installWhyLayer ran) */
|
|
880
|
+
let whyLib = null
|
|
881
|
+
|
|
879
882
|
async function installWhyLayer(ctx, config) {
|
|
880
883
|
const wantTools = config?.whyTools !== false
|
|
881
884
|
const wantPrompt = config?.whyPrompt !== false
|
|
@@ -891,6 +894,7 @@ async function installWhyLayer(ctx, config) {
|
|
|
891
894
|
return
|
|
892
895
|
}
|
|
893
896
|
whyStatus.loaded = true
|
|
897
|
+
whyLib = why
|
|
894
898
|
const ask = async (mcpName, args, cwd) => {
|
|
895
899
|
const a = { ...args }
|
|
896
900
|
for (const k of PATH_ARGS) if (typeof a[k] === 'string') a[k] = resolveAgainst(cwd, a[k])
|
|
@@ -996,6 +1000,29 @@ export async function apply(ctx, config) {
|
|
|
996
1000
|
return sendJson(res, 200, { session: sessionSummary(session) })
|
|
997
1001
|
}
|
|
998
1002
|
if (path === '/why/status' && req.method === 'GET') return sendJson(res, 200, whyStatus)
|
|
1003
|
+
// ── the why layer for the canvas: find across sessions and memories, recall one turn or entry, list the memory files ──
|
|
1004
|
+
if (path.startsWith('/why/') && req.method === 'GET') {
|
|
1005
|
+
if (!whyLib) return sendJson(res, 503, { error: 'why layer not loaded', ...whyStatus })
|
|
1006
|
+
if (path === '/why/find') {
|
|
1007
|
+
const phrase = url.searchParams.get('phrase') ?? ''
|
|
1008
|
+
const scope = url.searchParams.get('scope') ?? 'all'
|
|
1009
|
+
const limit = Number(url.searchParams.get('limit') ?? 20) || 20
|
|
1010
|
+
const cwd = url.searchParams.get('cwd') ?? undefined
|
|
1011
|
+
return sendJson(res, 200, await whyLib.findJson(phrase, { scope, limit, ...(cwd ? { cwd } : {}) }))
|
|
1012
|
+
}
|
|
1013
|
+
if (path === '/why/recall') {
|
|
1014
|
+
try { return sendJson(res, 200, await whyLib.recallJson(url.searchParams.get('session') ?? '', Number(url.searchParams.get('turn') ?? 0))) }
|
|
1015
|
+
catch (e) { return sendJson(res, 404, { error: e instanceof Error ? e.message : String(e) }) }
|
|
1016
|
+
}
|
|
1017
|
+
if (path === '/why/memories') return sendJson(res, 200, await whyLib.memoriesJson())
|
|
1018
|
+
if (path === '/why/suggest') return sendJson(res, 200, await whyLib.suggestJson(url.searchParams.get('term') ?? '', Number(url.searchParams.get('limit') ?? 8) || 8))
|
|
1019
|
+
if (path === '/why/topics') return sendJson(res, 200, await whyLib.topicsJson())
|
|
1020
|
+
if (path === '/why/by-topic') return sendJson(res, 200, await whyLib.byTopicJson((url.searchParams.get('ids') ?? '').split(',').filter(Boolean), { minP: Number(url.searchParams.get('minP') ?? 0.6) || 0.6, limit: Number(url.searchParams.get('limit') ?? 60) || 60 }))
|
|
1021
|
+
if (path === '/why/sample') return sendJson(res, 200, await whyLib.sampleQuestions(Number(url.searchParams.get('n') ?? 120) || 120))
|
|
1022
|
+
if (path === '/why/dossiers') return sendJson(res, 200, await whyLib.dossiersJson())
|
|
1023
|
+
if (path === '/why/dossier') return sendJson(res, 200, await whyLib.dossierJson(url.searchParams.get('id') ?? ''))
|
|
1024
|
+
if (path === '/why/dossier/new') return sendJson(res, 200, await whyLib.dossierNewTurns(url.searchParams.get('id') ?? '', { limit: Number(url.searchParams.get('limit') ?? 120) || 120 }))
|
|
1025
|
+
}
|
|
999
1026
|
if (path === '/version' && req.method === 'GET') return sendJson(res, 200, { version: PLUGIN_VERSION, latest: await latestPluginVersion(), checkedAt: latestLookup.at || null })
|
|
1000
1027
|
// ── the other agents' session files ──
|
|
1001
1028
|
if (path === '/roots' && req.method === 'GET') {
|
|
@@ -1020,6 +1047,28 @@ export async function apply(ctx, config) {
|
|
|
1020
1047
|
return sendJson(res, 200, await rangeOfFile(abs, url.searchParams.get('start'), url.searchParams.get('length')))
|
|
1021
1048
|
}
|
|
1022
1049
|
// ── link snapshots: the SPA's /api/fetch-url on the harness's bounded fetcher ──
|
|
1050
|
+
// ── topics: the table and the labelling job (POST) ──
|
|
1051
|
+
if (path.startsWith('/why/') && req.method === 'POST') {
|
|
1052
|
+
if (!whyLib) return sendJson(res, 503, { error: 'why layer not loaded', ...whyStatus })
|
|
1053
|
+
const body = await readJson(req)
|
|
1054
|
+
if (path === '/why/topics') return sendJson(res, 200, await whyLib.setTopics(Array.isArray(body?.topics) ? body.topics : []))
|
|
1055
|
+
if (path === '/why/label/start') { try { return sendJson(res, 200, await whyLib.labelStart(body?.call, body?.opts ?? {})) } catch (e) { return sendJson(res, 400, { error: e instanceof Error ? e.message : String(e) }) } }
|
|
1056
|
+
if (path === '/why/label/stop') return sendJson(res, 200, whyLib.labelStop())
|
|
1057
|
+
if (path === '/why/dossier') return sendJson(res, 200, await whyLib.setDossier(String(body?.id ?? ''), body?.dossier ?? {}))
|
|
1058
|
+
if (path === '/why/dossier/delete') { await whyLib.deleteDossier(String(body?.id ?? '')); return sendJson(res, 200, { ok: true }) }
|
|
1059
|
+
if (path === '/why/dossier/pending') return sendJson(res, 200, await whyLib.dossierAddPending(String(body?.id ?? ''), body?.item ?? { text: '' }))
|
|
1060
|
+
}
|
|
1061
|
+
// ── the judge (a System One decision endpoint) forwarded for the canvas; the key rides in the request ──
|
|
1062
|
+
if (path === '/judge' && req.method === 'POST') {
|
|
1063
|
+
const body = await readJson(req)
|
|
1064
|
+
const target = typeof body?.url === 'string' ? body.url.trim() : ''
|
|
1065
|
+
if (!/^https?:\/\//i.test(target)) return sendJson(res, 400, { error: 'url must be http(s)' })
|
|
1066
|
+
try {
|
|
1067
|
+
const r = await fetch(target, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(body.headers && typeof body.headers === 'object' ? body.headers : {}) }, body: JSON.stringify(body.body ?? {}), signal: AbortSignal.timeout(60000) })
|
|
1068
|
+
const text = await r.text()
|
|
1069
|
+
res.writeHead(r.status, { 'content-type': 'application/json; charset=utf-8' }); res.end(text || '{}'); return
|
|
1070
|
+
} catch (error) { return sendJson(res, 502, { error: error instanceof Error ? error.message : String(error) }) }
|
|
1071
|
+
}
|
|
1023
1072
|
if (path === '/fetch-url' && req.method === 'POST') {
|
|
1024
1073
|
const body = await readJson(req)
|
|
1025
1074
|
const target = typeof body?.url === 'string' ? body.url.trim() : ''
|
|
@@ -77,11 +77,6 @@ function createAgentsHttp({ workspaceRoot = path.join(os.homedir(), '.thoughtdag
|
|
|
77
77
|
const id = String(body?.runId ?? '');
|
|
78
78
|
return json(res, 200, { ok: runtime(owners.get(id)).answer(id, String(body?.requestId ?? ''), body?.response) }), true;
|
|
79
79
|
}
|
|
80
|
-
if (p === '/agents/dirs' && method === 'GET') {
|
|
81
|
-
// the directory browser's one level; a bad path is a 404 the client shows in place
|
|
82
|
-
try { return json(res, 200, await ops.listDirectory(url.searchParams.get('path') ?? '')), true; }
|
|
83
|
-
catch (e) { return json(res, 404, { error: String(e?.message ?? e) }), true; }
|
|
84
|
-
}
|
|
85
80
|
if (p === '/agents/workspace' && method === 'POST') return json(res, 200, { dir: await ops.workspaceFor(workspaceRoot, String(body?.canvasId ?? 'default')) }), true;
|
|
86
81
|
if (p === '/agents/guard' && method === 'POST') return json(res, 200, { ok: await ops.writeGuard(String(body?.cwd ?? ''), body?.config) }), true;
|
|
87
82
|
if (p === '/agents/materials' && method === 'POST') return json(res, 200, await ops.writeMaterials(String(body?.cwd ?? ''), body?.files)), true;
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
// the harness plugin host and the local server alike.
|
|
5
5
|
'use strict';
|
|
6
6
|
const path = require('node:path');
|
|
7
|
-
const os = require('node:os');
|
|
8
7
|
const fsp = require('node:fs/promises');
|
|
9
8
|
|
|
10
9
|
const WORKSPACE_ID = /^[\w.-]{1,80}$/;
|
|
@@ -52,37 +51,4 @@ async function writeGuard(cwd, config) {
|
|
|
52
51
|
return true;
|
|
53
52
|
}
|
|
54
53
|
|
|
55
|
-
|
|
56
|
-
* name-sorted tail and the listing says it was cut. */
|
|
57
|
-
const DIR_LIST_LIMIT = 400;
|
|
58
|
-
|
|
59
|
-
/** One level of the host's filesystem, for a browser the person walks to
|
|
60
|
-
* pick a working directory: the directory itself, its ancestry as crumbs
|
|
61
|
-
* (every one a jump target), its child directories name-sorted (symlinks
|
|
62
|
-
* to directories included, hidden ones flagged and left to the client),
|
|
63
|
-
* and the home directory to root a "Home" crumb. No path given lists the
|
|
64
|
-
* home directory; an unreadable or missing one throws. */
|
|
65
|
-
async function listDirectory(target) {
|
|
66
|
-
const home = os.homedir();
|
|
67
|
-
const dir = path.resolve(typeof target === 'string' && target.trim() ? target.trim() : home);
|
|
68
|
-
const st = await fsp.stat(dir);
|
|
69
|
-
if (!st.isDirectory()) throw new Error('not a directory');
|
|
70
|
-
const crumbs = [];
|
|
71
|
-
for (let cur = dir; ; cur = path.dirname(cur)) {
|
|
72
|
-
const parent = path.dirname(cur);
|
|
73
|
-
crumbs.unshift({ name: parent === cur ? cur : path.basename(cur), path: cur, hidden: false });
|
|
74
|
-
if (parent === cur) break;
|
|
75
|
-
}
|
|
76
|
-
const dirents = await fsp.readdir(dir, { withFileTypes: true });
|
|
77
|
-
const entries = [];
|
|
78
|
-
for (const d of dirents) {
|
|
79
|
-
let isDir = d.isDirectory();
|
|
80
|
-
if (!isDir && d.isSymbolicLink()) { try { isDir = (await fsp.stat(path.join(dir, d.name))).isDirectory(); } catch { isDir = false; } }
|
|
81
|
-
if (isDir) entries.push({ name: d.name, path: path.join(dir, d.name), hidden: d.name.startsWith('.') });
|
|
82
|
-
}
|
|
83
|
-
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
84
|
-
const truncated = entries.length > DIR_LIST_LIMIT;
|
|
85
|
-
return { path: dir, home, crumbs, entries: truncated ? entries.slice(0, DIR_LIST_LIMIT) : entries, truncated };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
module.exports = { workspaceFor, writeMaterials, writeGuard, listDirectory };
|
|
54
|
+
module.exports = { workspaceFor, writeMaterials, writeGuard };
|