farming-code 2.2.11 → 2.2.13
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 +9 -8
- package/README.zh_cn.md +9 -8
- package/backend/agent-manager.js +108 -5
- package/backend/codex-session-archive.js +13 -4
- package/backend/config-manager.js +5 -0
- package/backend/executable-discovery.js +2 -0
- package/backend/server.js +72 -0
- package/backend/usage-monitor.js +97 -15
- package/backend/workspace-directory.js +80 -0
- package/dist/assets/App-xASd_Y0F.js +206 -0
- package/dist/assets/{FileEditorMarkdownPreview--Cri80nQ.js → FileEditorMarkdownPreview-UlVd0O4D.js} +1 -1
- package/dist/assets/FileEditorPane-Dxx4rKOg.js +2 -0
- package/dist/assets/IconGlyphs-AU22gPDY.js +1 -0
- package/dist/assets/{ProjectFilesSection-Cz1wG4NT.js → ProjectFilesSection-DE7Nb9Jk.js} +3 -3
- package/dist/assets/ReviewPage-CeDKDyZh.js +3 -0
- package/dist/assets/code-dark-DPiuyeSp.css +1 -0
- package/dist/assets/{index-miO2r9IZ.js → index-CYZKdoqi.js} +2 -2
- package/dist/assets/main-Bqtb5kuz.css +1 -0
- package/dist/assets/{monaco.contribution-CyLosfZI.js → monaco.contribution-BXz-lfFB.js} +2 -2
- package/dist/assets/{tsMode-DGZTlTj8.js → tsMode-B0oUTcXQ.js} +1 -1
- package/dist/assets/workspace-editor-monaco-BICJESG0.js +4 -0
- package/dist/assets/workspace-editor-monaco-DRJ7IaXk.js +1 -0
- package/dist/crt-markdown-assets/KaTeX_AMS-Regular-CYEKBG2K.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_AMS-Regular-JKX5W2C4.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_AMS-Regular-U6PRYMIZ.woff2 +0 -0
- package/dist/crt-markdown-assets/KaTeX_Caligraphic-Bold-5QL5CMTE.woff2 +0 -0
- package/dist/crt-markdown-assets/KaTeX_Caligraphic-Bold-WZ3QSGD3.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_Caligraphic-Bold-ZTS3R3HK.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_Caligraphic-Regular-3LKEU76G.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_Caligraphic-Regular-A7XRTZ5Q.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_Caligraphic-Regular-KX5MEWCF.woff2 +0 -0
- package/dist/crt-markdown-assets/KaTeX_Fraktur-Bold-2QVFK6NQ.woff2 +0 -0
- package/dist/crt-markdown-assets/KaTeX_Fraktur-Bold-T4SWXBMT.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_Fraktur-Bold-WGHVTYOR.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_Fraktur-Regular-2PEIFJSJ.woff2 +0 -0
- package/dist/crt-markdown-assets/KaTeX_Fraktur-Regular-5U4OPH2X.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_Fraktur-Regular-PQMHCIK6.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_Main-Bold-2GA4IZIN.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_Main-Bold-W5FBVCZM.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_Main-Bold-YP5VVQRP.woff2 +0 -0
- package/dist/crt-markdown-assets/KaTeX_Main-BoldItalic-4P4C7HJH.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_Main-BoldItalic-N4V3DX7S.woff2 +0 -0
- package/dist/crt-markdown-assets/KaTeX_Main-BoldItalic-ODMLBJJQ.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_Main-Italic-I43T2HSR.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_Main-Italic-RELBIK7M.woff2 +0 -0
- package/dist/crt-markdown-assets/KaTeX_Main-Italic-SASNQFN2.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_Main-Regular-ARRPAO67.woff2 +0 -0
- package/dist/crt-markdown-assets/KaTeX_Main-Regular-P5I74A2A.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_Main-Regular-W74P5G27.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_Math-BoldItalic-6EBV3DK5.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_Math-BoldItalic-K4WTGH3J.woff2 +0 -0
- package/dist/crt-markdown-assets/KaTeX_Math-BoldItalic-VB447A4D.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_Math-Italic-6KGCHLFN.woff2 +0 -0
- package/dist/crt-markdown-assets/KaTeX_Math-Italic-KKK3USB2.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_Math-Italic-SON4MRCA.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_SansSerif-Bold-RRNVJFFW.woff2 +0 -0
- package/dist/crt-markdown-assets/KaTeX_SansSerif-Bold-STQ6RXC7.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_SansSerif-Bold-X5M5EMOD.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_SansSerif-Italic-HMPFTM52.woff2 +0 -0
- package/dist/crt-markdown-assets/KaTeX_SansSerif-Italic-PSN4QKYX.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_SansSerif-Italic-WTBAZBGY.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_SansSerif-Regular-2TL3USAE.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_SansSerif-Regular-OQCII6EP.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_SansSerif-Regular-XIQ62X4E.woff2 +0 -0
- package/dist/crt-markdown-assets/KaTeX_Script-Regular-72OLXYNA.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_Script-Regular-A5IFOEBS.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_Script-Regular-APUWIHLP.woff2 +0 -0
- package/dist/crt-markdown-assets/KaTeX_Size1-Regular-4HRHTS65.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_Size1-Regular-5LRUTBFT.woff2 +0 -0
- package/dist/crt-markdown-assets/KaTeX_Size1-Regular-7K6AASVL.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_Size2-Regular-222HN3GT.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_Size2-Regular-K5ZHAIS6.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_Size2-Regular-LELKET5D.woff2 +0 -0
- package/dist/crt-markdown-assets/KaTeX_Size3-Regular-TLFPAHDE.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_Size3-Regular-UFCO6WCA.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_Size3-Regular-WQRQ47UD.woff2 +0 -0
- package/dist/crt-markdown-assets/KaTeX_Size4-Regular-7PGNVPQK.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_Size4-Regular-CDMV7U5C.woff2 +0 -0
- package/dist/crt-markdown-assets/KaTeX_Size4-Regular-PKMWZHNC.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_Typewriter-Regular-3F5K6SQ6.ttf +0 -0
- package/dist/crt-markdown-assets/KaTeX_Typewriter-Regular-MJMFSK64.woff +0 -0
- package/dist/crt-markdown-assets/KaTeX_Typewriter-Regular-VBYJ4NRC.woff2 +0 -0
- package/dist/crt-markdown-renderer.css +1 -0
- package/dist/crt-markdown-renderer.js +361 -0
- package/dist/crt-mermaid-renderer.js +3595 -0
- package/dist/farming-2/site.webmanifest +2 -0
- package/dist/index.html +1 -1
- package/frontend/skins/crt/app.js +55 -32
- package/frontend/skins/crt/index.html +47 -31
- package/frontend/skins/crt/styles/markdown.css +251 -0
- package/frontend/skins/crt/styles/monochrome-green.css +14 -3
- package/package.json +5 -2
- package/dist/assets/App-CHYpgECZ.js +0 -208
- package/dist/assets/FileEditorPane-DGJlEKdf.js +0 -2
- package/dist/assets/IconGlyphs-Cc14sepw.js +0 -1
- package/dist/assets/ReviewPage-LonRc6Fz.js +0 -3
- package/dist/assets/code-dark-B1pAtO_P.css +0 -1
- package/dist/assets/main-DNe0jPw-.css +0 -1
- package/dist/assets/workspace-editor-monaco-5gFbeDPF.js +0 -1
- package/dist/assets/workspace-editor-monaco-xPv8JzHH.js +0 -4
package/backend/usage-monitor.js
CHANGED
|
@@ -9,8 +9,8 @@ const { attachQuotaForecasts } = require('./usage-forecast');
|
|
|
9
9
|
const execFileAsync = promisify(execFile);
|
|
10
10
|
|
|
11
11
|
const USAGE_WINDOW_MS = 5 * 60 * 1000;
|
|
12
|
-
const USAGE_TIMELINE_WINDOW_MS = 60 * 60 * 1000;
|
|
13
|
-
const USAGE_TIMELINE_BUCKET_COUNT =
|
|
12
|
+
const USAGE_TIMELINE_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
13
|
+
const USAGE_TIMELINE_BUCKET_COUNT = 24;
|
|
14
14
|
const USAGE_DAILY_DAYS = 52 * 7;
|
|
15
15
|
const USAGE_DAILY_CACHE_MS = 5 * 60 * 1000;
|
|
16
16
|
const USAGE_LIVE_DAY_CACHE_MS = 5 * 1000;
|
|
@@ -152,6 +152,32 @@ function addTokenBreakdown(target, source) {
|
|
|
152
152
|
return target;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
function usageAgentIdFromFilePath(filePath) {
|
|
156
|
+
const basename = path.basename(String(filePath || ''), path.extname(String(filePath || '')));
|
|
157
|
+
const uuid = basename.match(/([0-9a-f]{8}-[0-9a-f-]{27})$/i);
|
|
158
|
+
return uuid?.[1] || basename || 'unattributed';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function usageAgentLabel(provider, agentId) {
|
|
162
|
+
const providerName = {
|
|
163
|
+
codex: 'Codex',
|
|
164
|
+
claude: 'Claude',
|
|
165
|
+
opencode: 'OpenCode',
|
|
166
|
+
qoder: 'Qoder',
|
|
167
|
+
}[provider] || provider;
|
|
168
|
+
if (!agentId || agentId === 'unattributed') return providerName;
|
|
169
|
+
const shortId = agentId.length > 12 ? `…${agentId.slice(-6)}` : agentId;
|
|
170
|
+
return `${providerName} · ${shortId}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function attributeUsageEvent(event, provider, agentId) {
|
|
174
|
+
return {
|
|
175
|
+
...event,
|
|
176
|
+
agentId,
|
|
177
|
+
agentLabel: usageAgentLabel(provider, agentId),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
155
181
|
function subtractTokenBreakdown(current, previous) {
|
|
156
182
|
const result = emptyTokenBreakdown();
|
|
157
183
|
for (const field of Object.keys(result)) {
|
|
@@ -268,7 +294,9 @@ function buildUsageTimeline(providerEvents, options = {}) {
|
|
|
268
294
|
const windowMs = Math.max(60_000, options.windowMs ?? USAGE_TIMELINE_WINDOW_MS);
|
|
269
295
|
const bucketCount = Math.max(1, Math.floor(options.bucketCount ?? USAGE_TIMELINE_BUCKET_COUNT));
|
|
270
296
|
const bucketMs = windowMs / bucketCount;
|
|
271
|
-
const endAt =
|
|
297
|
+
const endAt = options.alignToBucket
|
|
298
|
+
? Math.ceil(now / bucketMs) * bucketMs
|
|
299
|
+
: now;
|
|
272
300
|
const startAt = endAt - windowMs;
|
|
273
301
|
const providerNames = Object.keys(providerEvents || {});
|
|
274
302
|
const points = Array.from({ length: bucketCount }, (_, index) => ({
|
|
@@ -652,11 +680,14 @@ function buildUsageDayDetail(providerEvents, options = {}) {
|
|
|
652
680
|
hour,
|
|
653
681
|
label: String(hour).padStart(2, '0'),
|
|
654
682
|
...emptyTokenBreakdown(),
|
|
683
|
+
agents: {},
|
|
655
684
|
}));
|
|
656
685
|
const providers = Object.fromEntries(
|
|
657
686
|
providerNames.map(provider => [provider, emptyTokenBreakdown()]),
|
|
658
687
|
);
|
|
659
688
|
const total = emptyTokenBreakdown();
|
|
689
|
+
const agents = new Map();
|
|
690
|
+
const agentLabels = options.agentLabels instanceof Map ? options.agentLabels : new Map();
|
|
660
691
|
|
|
661
692
|
for (const provider of providerNames) {
|
|
662
693
|
for (const event of providerEvents[provider] || []) {
|
|
@@ -664,8 +695,24 @@ function buildUsageDayDetail(providerEvents, options = {}) {
|
|
|
664
695
|
if (localDateKey(timestamp) !== date) continue;
|
|
665
696
|
const hour = new Date(timestamp).getHours();
|
|
666
697
|
if (!Number.isInteger(hour) || hour < 0 || hour > 23) continue;
|
|
698
|
+
const agentId = String(event?.agentId || 'unattributed');
|
|
699
|
+
const agentKey = `${provider}:${agentId}`;
|
|
700
|
+
let agent = agents.get(agentKey);
|
|
701
|
+
if (!agent) {
|
|
702
|
+
agent = {
|
|
703
|
+
key: agentKey,
|
|
704
|
+
provider,
|
|
705
|
+
sessionId: agentId === 'unattributed' ? '' : agentId,
|
|
706
|
+
label: agentLabels.get(agentKey) || event?.agentLabel || usageAgentLabel(provider, agentId),
|
|
707
|
+
...emptyTokenBreakdown(),
|
|
708
|
+
};
|
|
709
|
+
agents.set(agentKey, agent);
|
|
710
|
+
}
|
|
711
|
+
if (!hours[hour].agents[agentKey]) hours[hour].agents[agentKey] = emptyTokenBreakdown();
|
|
667
712
|
addTokenBreakdown(hours[hour], event);
|
|
713
|
+
addTokenBreakdown(hours[hour].agents[agentKey], event);
|
|
668
714
|
addTokenBreakdown(providers[provider], event);
|
|
715
|
+
addTokenBreakdown(agent, event);
|
|
669
716
|
addTokenBreakdown(total, event);
|
|
670
717
|
}
|
|
671
718
|
}
|
|
@@ -677,6 +724,9 @@ function buildUsageDayDetail(providerEvents, options = {}) {
|
|
|
677
724
|
total,
|
|
678
725
|
hours,
|
|
679
726
|
providers,
|
|
727
|
+
agents: Array.from(agents.values()).sort((left, right) => (
|
|
728
|
+
right.totalTokens - left.totalTokens || left.label.localeCompare(right.label)
|
|
729
|
+
)),
|
|
680
730
|
};
|
|
681
731
|
}
|
|
682
732
|
|
|
@@ -709,9 +759,11 @@ async function readDailyFileEvents(filePath, provider, minimumMtimeMs = 0) {
|
|
|
709
759
|
}
|
|
710
760
|
}
|
|
711
761
|
|
|
762
|
+
const agentId = usageAgentIdFromFilePath(filePath);
|
|
763
|
+
const attributedEvents = events.map(event => attributeUsageEvent(event, provider, agentId));
|
|
712
764
|
const truncated = stat.size > JSONL_TAIL_BYTES;
|
|
713
|
-
dailyFileEventCache.set(cacheKey, { signature, events, truncated });
|
|
714
|
-
return { events, mtimeMs: stat.mtimeMs, truncated };
|
|
765
|
+
dailyFileEventCache.set(cacheKey, { signature, events: attributedEvents, truncated });
|
|
766
|
+
return { events: attributedEvents, mtimeMs: stat.mtimeMs, truncated };
|
|
715
767
|
}
|
|
716
768
|
|
|
717
769
|
async function collectCodexDailyEventsWithRipgrep(roots, options = {}) {
|
|
@@ -765,7 +817,9 @@ async function collectCodexDailyEventsWithRipgrep(roots, options = {}) {
|
|
|
765
817
|
states.set(filePath, state);
|
|
766
818
|
}
|
|
767
819
|
const event = codexTokenEventFromRecord(record, state);
|
|
768
|
-
if (event && event.timestamp >= cutoffMs && event.timestamp <= now + 60_000)
|
|
820
|
+
if (event && event.timestamp >= cutoffMs && event.timestamp <= now + 60_000) {
|
|
821
|
+
events.push(attributeUsageEvent(event, 'codex', usageAgentIdFromFilePath(filePath)));
|
|
822
|
+
}
|
|
769
823
|
});
|
|
770
824
|
child.once('close', code => {
|
|
771
825
|
if (unavailable || (code !== 0 && code !== 1)) {
|
|
@@ -892,7 +946,8 @@ async function collectOpenCodeDailyEvents(homePaths, options = {}) {
|
|
|
892
946
|
{ openCodeHome: session.openCodeHome, timeoutMs: OPENCODE_COMMAND_TIMEOUT_MS },
|
|
893
947
|
);
|
|
894
948
|
const exported = JSON.parse(String(result?.stdout || '{}'));
|
|
895
|
-
const sessionEvents = openCodeTokenEventsFromExport(exported, { cutoffMs, now })
|
|
949
|
+
const sessionEvents = openCodeTokenEventsFromExport(exported, { cutoffMs, now })
|
|
950
|
+
.map(event => attributeUsageEvent(event, 'opencode', session.id));
|
|
896
951
|
successfulExports += 1;
|
|
897
952
|
openCodeSessionEventCache.set(cacheKey, { updatedAt: session.updatedAt, events: sessionEvents });
|
|
898
953
|
events.push(...sessionEvents);
|
|
@@ -1212,6 +1267,33 @@ class UsageMonitor {
|
|
|
1212
1267
|
this.liveDayCache.fetchedAt = 0;
|
|
1213
1268
|
}
|
|
1214
1269
|
|
|
1270
|
+
usageAgentLabels() {
|
|
1271
|
+
const labels = new Map();
|
|
1272
|
+
const agents = this.agentManager?.getState?.().agents;
|
|
1273
|
+
if (!Array.isArray(agents)) return labels;
|
|
1274
|
+
for (const agent of agents) {
|
|
1275
|
+
const provider = String(agent?.providerSessionProvider || '').trim();
|
|
1276
|
+
const sessionId = String(agent?.providerSessionId || '').trim();
|
|
1277
|
+
if (!provider || !sessionId) continue;
|
|
1278
|
+
const label = String(
|
|
1279
|
+
agent.customTitle
|
|
1280
|
+
|| agent.task
|
|
1281
|
+
|| agent.sessionTitle
|
|
1282
|
+
|| agent.providerSessionTitle
|
|
1283
|
+
|| '',
|
|
1284
|
+
).trim();
|
|
1285
|
+
if (label) labels.set(`${provider}:${sessionId}`, label);
|
|
1286
|
+
}
|
|
1287
|
+
return labels;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
buildUsageDay(providerEvents, date) {
|
|
1291
|
+
return buildUsageDayDetail(providerEvents, {
|
|
1292
|
+
date,
|
|
1293
|
+
agentLabels: this.usageAgentLabels(),
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1215
1297
|
getDailyUsage(options = {}) {
|
|
1216
1298
|
const now = options.now ?? Date.now();
|
|
1217
1299
|
if (
|
|
@@ -1247,7 +1329,7 @@ class UsageMonitor {
|
|
|
1247
1329
|
if (options.live === true && String(date || '').trim() === localDateKey(now)) {
|
|
1248
1330
|
const liveDate = String(date).trim();
|
|
1249
1331
|
const dailyFallback = this.dailyCache.value?.providerEvents
|
|
1250
|
-
?
|
|
1332
|
+
? this.buildUsageDay(this.dailyCache.value.providerEvents, liveDate)
|
|
1251
1333
|
: null;
|
|
1252
1334
|
const cachedFallback = this.liveDayCache.date === liveDate
|
|
1253
1335
|
? this.liveDayCache.value
|
|
@@ -1283,7 +1365,7 @@ class UsageMonitor {
|
|
|
1283
1365
|
now,
|
|
1284
1366
|
days: 1,
|
|
1285
1367
|
}).then(history => {
|
|
1286
|
-
const detail =
|
|
1368
|
+
const detail = this.buildUsageDay(history.providerEvents, liveDate);
|
|
1287
1369
|
if (this.liveDayCache.date === liveDate) {
|
|
1288
1370
|
this.liveDayCache.value = detail;
|
|
1289
1371
|
this.liveDayCache.fetchedAt = now;
|
|
@@ -1299,7 +1381,7 @@ class UsageMonitor {
|
|
|
1299
1381
|
now,
|
|
1300
1382
|
force: options.fresh === true,
|
|
1301
1383
|
});
|
|
1302
|
-
return
|
|
1384
|
+
return this.buildUsageDay(history.providerEvents, date);
|
|
1303
1385
|
}
|
|
1304
1386
|
|
|
1305
1387
|
async getUsageSummary(options = {}) {
|
|
@@ -1330,11 +1412,11 @@ class UsageMonitor {
|
|
|
1330
1412
|
historyWindowMs,
|
|
1331
1413
|
source: 'opencode session export',
|
|
1332
1414
|
});
|
|
1333
|
-
const timeline = buildUsageTimeline({
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
}
|
|
1415
|
+
const timeline = buildUsageTimeline(history.providerEvents, {
|
|
1416
|
+
now,
|
|
1417
|
+
windowMs: historyWindowMs,
|
|
1418
|
+
alignToBucket: true,
|
|
1419
|
+
});
|
|
1338
1420
|
const openCodeCoverage = history.coverage.find(entry => entry.provider === 'opencode');
|
|
1339
1421
|
const qoderCoverage = history.coverage.find(entry => entry.provider === 'qoder');
|
|
1340
1422
|
|
|
@@ -5,6 +5,7 @@ const path = require('path');
|
|
|
5
5
|
|
|
6
6
|
const CREATE_FORBIDDEN_CODES = new Set(['EACCES', 'EPERM', 'EROFS']);
|
|
7
7
|
const INVALID_PATH_CODES = new Set(['EINVAL', 'ENAMETOOLONG']);
|
|
8
|
+
const DEFAULT_BROWSE_LIMIT = 500;
|
|
8
9
|
|
|
9
10
|
function resolveWorkspaceDirectory(value, homeDir = process.env.HOME || os.homedir()) {
|
|
10
11
|
const input = typeof value === 'string' ? value.trim() : '';
|
|
@@ -131,9 +132,87 @@ async function prepareWorkspaceDirectory(value, options = {}) {
|
|
|
131
132
|
}
|
|
132
133
|
}
|
|
133
134
|
|
|
135
|
+
async function browseWorkspaceDirectory(value, options = {}) {
|
|
136
|
+
const workspace = resolveWorkspaceDirectory(value || '~', options.homeDir);
|
|
137
|
+
const fileSystem = options.fileSystem || fs.promises;
|
|
138
|
+
let entries;
|
|
139
|
+
try {
|
|
140
|
+
const stat = await fileSystem.stat(workspace);
|
|
141
|
+
if (!stat.isDirectory()) {
|
|
142
|
+
return {
|
|
143
|
+
status: 409,
|
|
144
|
+
body: {
|
|
145
|
+
status: 'rejected',
|
|
146
|
+
code: 'workspace-not-directory',
|
|
147
|
+
workspace,
|
|
148
|
+
message: `Workspace path is not a directory: ${workspace}`,
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
entries = await fileSystem.readdir(workspace, { withFileTypes: true });
|
|
153
|
+
} catch (error) {
|
|
154
|
+
const forbidden = CREATE_FORBIDDEN_CODES.has(error?.code);
|
|
155
|
+
const missing = error?.code === 'ENOENT';
|
|
156
|
+
return {
|
|
157
|
+
status: forbidden ? 403 : missing ? 404 : 500,
|
|
158
|
+
body: {
|
|
159
|
+
status: 'rejected',
|
|
160
|
+
code: forbidden
|
|
161
|
+
? 'workspace-browse-forbidden'
|
|
162
|
+
: missing
|
|
163
|
+
? 'workspace-not-found'
|
|
164
|
+
: 'workspace-browse-failed',
|
|
165
|
+
workspace,
|
|
166
|
+
message: forbidden
|
|
167
|
+
? `Farming does not have permission to read this directory: ${workspace}`
|
|
168
|
+
: missing
|
|
169
|
+
? `Workspace directory does not exist: ${workspace}`
|
|
170
|
+
: `Failed to read workspace directory: ${workspace}`,
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const directories = (await Promise.all(entries.map(async (entry) => {
|
|
176
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) return null;
|
|
177
|
+
const childPath = path.join(workspace, entry.name);
|
|
178
|
+
if (entry.isSymbolicLink()) {
|
|
179
|
+
try {
|
|
180
|
+
const stat = await fileSystem.stat(childPath);
|
|
181
|
+
if (!stat.isDirectory()) return null;
|
|
182
|
+
} catch {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return { name: entry.name, path: childPath };
|
|
187
|
+
})))
|
|
188
|
+
.filter(Boolean)
|
|
189
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
190
|
+
const limit = Math.max(1, Math.min(Number(options.limit) || DEFAULT_BROWSE_LIMIT, DEFAULT_BROWSE_LIMIT));
|
|
191
|
+
const parentPath = path.dirname(workspace);
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
status: 200,
|
|
195
|
+
body: {
|
|
196
|
+
status: 'ready',
|
|
197
|
+
workspace,
|
|
198
|
+
parent: parentPath === workspace ? null : parentPath,
|
|
199
|
+
directories: directories.slice(0, limit),
|
|
200
|
+
truncated: directories.length > limit,
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
134
205
|
function createWorkspaceDirectoryRouter(options = {}) {
|
|
135
206
|
const router = express.Router();
|
|
136
207
|
router.use(express.json({ limit: '8kb' }));
|
|
208
|
+
router.get('/browse', async (req, res) => {
|
|
209
|
+
const result = await browseWorkspaceDirectory(req.query?.path, {
|
|
210
|
+
fileSystem: options.fileSystem,
|
|
211
|
+
homeDir: options.homeDir,
|
|
212
|
+
limit: req.query?.limit,
|
|
213
|
+
});
|
|
214
|
+
res.status(result.status).json(result.body);
|
|
215
|
+
});
|
|
137
216
|
router.post('/prepare', async (req, res) => {
|
|
138
217
|
const result = await prepareWorkspaceDirectory(req.body?.workspace, {
|
|
139
218
|
create: req.body?.create === true,
|
|
@@ -146,6 +225,7 @@ function createWorkspaceDirectoryRouter(options = {}) {
|
|
|
146
225
|
}
|
|
147
226
|
|
|
148
227
|
module.exports = {
|
|
228
|
+
browseWorkspaceDirectory,
|
|
149
229
|
createWorkspaceDirectoryRouter,
|
|
150
230
|
prepareWorkspaceDirectory,
|
|
151
231
|
resolveWorkspaceDirectory,
|