@ilya-lesikov/pi-pi 0.1.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/3p/pi-ask-user/index.ts +1835 -0
- package/3p/pi-ask-user/package.json +50 -0
- package/3p/pi-ask-user/single-select-layout.ts +203 -0
- package/3p/pi-lsp/extensions/lsp/client.ts +542 -0
- package/3p/pi-lsp/extensions/lsp/config.ts +135 -0
- package/3p/pi-lsp/extensions/lsp/effects/command.ts +42 -0
- package/3p/pi-lsp/extensions/lsp/effects/filesystem.ts +50 -0
- package/3p/pi-lsp/extensions/lsp/effects/runtime.ts +21 -0
- package/3p/pi-lsp/extensions/lsp/errors.ts +113 -0
- package/3p/pi-lsp/extensions/lsp/formatting.ts +315 -0
- package/3p/pi-lsp/extensions/lsp/index.ts +231 -0
- package/3p/pi-lsp/extensions/lsp/protocol.ts +209 -0
- package/3p/pi-lsp/extensions/lsp/retry.ts +39 -0
- package/3p/pi-lsp/extensions/lsp/tools/programs.ts +293 -0
- package/3p/pi-lsp/extensions/lsp/tools.ts +89 -0
- package/3p/pi-lsp/extensions/lsp/types.ts +243 -0
- package/3p/pi-lsp/package.json +54 -0
- package/3p/pi-plannotator/AGENTS.md +583 -0
- package/3p/pi-plannotator/apps/pi-extension/README.md +228 -0
- package/3p/pi-plannotator/apps/pi-extension/assistant-message.ts +128 -0
- package/3p/pi-plannotator/apps/pi-extension/config.test.ts +166 -0
- package/3p/pi-plannotator/apps/pi-extension/config.ts +318 -0
- package/3p/pi-plannotator/apps/pi-extension/current-pi-session.ts +147 -0
- package/3p/pi-plannotator/apps/pi-extension/index.ts +1279 -0
- package/3p/pi-plannotator/apps/pi-extension/package.json +56 -0
- package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.test.ts +13 -0
- package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.ts +616 -0
- package/3p/pi-plannotator/apps/pi-extension/plannotator-events.ts +342 -0
- package/3p/pi-plannotator/apps/pi-extension/plannotator.json +12 -0
- package/3p/pi-plannotator/apps/pi-extension/server/agent-jobs.ts +515 -0
- package/3p/pi-plannotator/apps/pi-extension/server/ai-runtime.ts +169 -0
- package/3p/pi-plannotator/apps/pi-extension/server/annotations.ts +85 -0
- package/3p/pi-plannotator/apps/pi-extension/server/external-annotations.ts +189 -0
- package/3p/pi-plannotator/apps/pi-extension/server/handlers.ts +210 -0
- package/3p/pi-plannotator/apps/pi-extension/server/helpers.ts +78 -0
- package/3p/pi-plannotator/apps/pi-extension/server/ide.ts +46 -0
- package/3p/pi-plannotator/apps/pi-extension/server/integrations.ts +195 -0
- package/3p/pi-plannotator/apps/pi-extension/server/network.test.ts +158 -0
- package/3p/pi-plannotator/apps/pi-extension/server/network.ts +268 -0
- package/3p/pi-plannotator/apps/pi-extension/server/pr.ts +126 -0
- package/3p/pi-plannotator/apps/pi-extension/server/project.ts +64 -0
- package/3p/pi-plannotator/apps/pi-extension/server/reference.ts +362 -0
- package/3p/pi-plannotator/apps/pi-extension/server/serverAnnotate.ts +199 -0
- package/3p/pi-plannotator/apps/pi-extension/server/serverPlan.ts +505 -0
- package/3p/pi-plannotator/apps/pi-extension/server/serverReview.ts +1229 -0
- package/3p/pi-plannotator/apps/pi-extension/server/vcs.ts +121 -0
- package/3p/pi-plannotator/apps/pi-extension/server.test.ts +966 -0
- package/3p/pi-plannotator/apps/pi-extension/server.ts +44 -0
- package/3p/pi-plannotator/apps/pi-extension/tool-scope.test.ts +88 -0
- package/3p/pi-plannotator/apps/pi-extension/tool-scope.ts +39 -0
- package/3p/pi-plannotator/apps/pi-extension/tsconfig.json +16 -0
- package/3p/pi-plannotator/apps/pi-extension/vendor.sh +48 -0
- package/3p/pi-plannotator/package.json +53 -0
- package/3p/pi-plannotator/packages/ai/ai.test.ts +1407 -0
- package/3p/pi-plannotator/packages/ai/base-session.ts +94 -0
- package/3p/pi-plannotator/packages/ai/context.ts +250 -0
- package/3p/pi-plannotator/packages/ai/endpoints.ts +326 -0
- package/3p/pi-plannotator/packages/ai/index.ts +105 -0
- package/3p/pi-plannotator/packages/ai/package.json +21 -0
- package/3p/pi-plannotator/packages/ai/provider.ts +103 -0
- package/3p/pi-plannotator/packages/ai/providers/claude-agent-sdk.ts +447 -0
- package/3p/pi-plannotator/packages/ai/providers/codex-sdk.ts +430 -0
- package/3p/pi-plannotator/packages/ai/providers/command-path.ts +115 -0
- package/3p/pi-plannotator/packages/ai/providers/opencode-sdk.ts +546 -0
- package/3p/pi-plannotator/packages/ai/providers/pi-events.ts +110 -0
- package/3p/pi-plannotator/packages/ai/providers/pi-sdk-node.ts +425 -0
- package/3p/pi-plannotator/packages/ai/providers/pi-sdk.ts +469 -0
- package/3p/pi-plannotator/packages/ai/session-manager.ts +195 -0
- package/3p/pi-plannotator/packages/ai/tsconfig.json +15 -0
- package/3p/pi-plannotator/packages/ai/types.ts +379 -0
- package/3p/pi-plannotator/packages/server/agent-jobs.ts +538 -0
- package/3p/pi-plannotator/packages/server/agent-review-message.test.ts +135 -0
- package/3p/pi-plannotator/packages/server/agent-review-message.ts +243 -0
- package/3p/pi-plannotator/packages/server/ai-runtime.ts +108 -0
- package/3p/pi-plannotator/packages/server/annotate.ts +401 -0
- package/3p/pi-plannotator/packages/server/browser.test.ts +87 -0
- package/3p/pi-plannotator/packages/server/browser.ts +245 -0
- package/3p/pi-plannotator/packages/server/claude-review.ts +352 -0
- package/3p/pi-plannotator/packages/server/code-nav.ts +73 -0
- package/3p/pi-plannotator/packages/server/codex-review-schema.json +41 -0
- package/3p/pi-plannotator/packages/server/codex-review.ts +330 -0
- package/3p/pi-plannotator/packages/server/config.ts +8 -0
- package/3p/pi-plannotator/packages/server/draft.ts +1 -0
- package/3p/pi-plannotator/packages/server/editor-annotations.ts +76 -0
- package/3p/pi-plannotator/packages/server/external-annotations.test.ts +18 -0
- package/3p/pi-plannotator/packages/server/external-annotations.ts +207 -0
- package/3p/pi-plannotator/packages/server/git.ts +141 -0
- package/3p/pi-plannotator/packages/server/goal-setup.test.ts +55 -0
- package/3p/pi-plannotator/packages/server/goal-setup.ts +248 -0
- package/3p/pi-plannotator/packages/server/ide.ts +43 -0
- package/3p/pi-plannotator/packages/server/image.test.ts +63 -0
- package/3p/pi-plannotator/packages/server/image.ts +66 -0
- package/3p/pi-plannotator/packages/server/index.ts +648 -0
- package/3p/pi-plannotator/packages/server/integrations.test.ts +172 -0
- package/3p/pi-plannotator/packages/server/integrations.ts +214 -0
- package/3p/pi-plannotator/packages/server/jj.test.ts +69 -0
- package/3p/pi-plannotator/packages/server/jj.ts +86 -0
- package/3p/pi-plannotator/packages/server/p4.ts +417 -0
- package/3p/pi-plannotator/packages/server/package.json +40 -0
- package/3p/pi-plannotator/packages/server/path-utils.ts +18 -0
- package/3p/pi-plannotator/packages/server/pr.ts +146 -0
- package/3p/pi-plannotator/packages/server/project.test.ts +115 -0
- package/3p/pi-plannotator/packages/server/project.ts +45 -0
- package/3p/pi-plannotator/packages/server/reference-handlers.ts +404 -0
- package/3p/pi-plannotator/packages/server/remote.test.ts +150 -0
- package/3p/pi-plannotator/packages/server/remote.ts +74 -0
- package/3p/pi-plannotator/packages/server/repo.ts +80 -0
- package/3p/pi-plannotator/packages/server/resolve-file.test.ts +310 -0
- package/3p/pi-plannotator/packages/server/review-workspace.test.ts +1032 -0
- package/3p/pi-plannotator/packages/server/review-workspace.ts +48 -0
- package/3p/pi-plannotator/packages/server/review.ts +1318 -0
- package/3p/pi-plannotator/packages/server/sessions.ts +111 -0
- package/3p/pi-plannotator/packages/server/share-url.ts +53 -0
- package/3p/pi-plannotator/packages/server/shared-handlers.test.ts +43 -0
- package/3p/pi-plannotator/packages/server/shared-handlers.ts +177 -0
- package/3p/pi-plannotator/packages/server/storage.test.ts +176 -0
- package/3p/pi-plannotator/packages/server/storage.ts +17 -0
- package/3p/pi-plannotator/packages/server/tour/tour-review.test.ts +146 -0
- package/3p/pi-plannotator/packages/server/tour/tour-review.ts +604 -0
- package/3p/pi-plannotator/packages/server/tsconfig.json +15 -0
- package/3p/pi-plannotator/packages/server/vcs.test.ts +48 -0
- package/3p/pi-plannotator/packages/server/vcs.ts +80 -0
- package/3p/pi-plannotator/packages/shared/agent-jobs.ts +132 -0
- package/3p/pi-plannotator/packages/shared/agents.ts +53 -0
- package/3p/pi-plannotator/packages/shared/annotate-args.test.ts +386 -0
- package/3p/pi-plannotator/packages/shared/annotate-args.ts +107 -0
- package/3p/pi-plannotator/packages/shared/at-reference.test.ts +99 -0
- package/3p/pi-plannotator/packages/shared/at-reference.ts +52 -0
- package/3p/pi-plannotator/packages/shared/checklist.ts +52 -0
- package/3p/pi-plannotator/packages/shared/code-file.test.ts +112 -0
- package/3p/pi-plannotator/packages/shared/code-file.ts +41 -0
- package/3p/pi-plannotator/packages/shared/code-nav.test.ts +515 -0
- package/3p/pi-plannotator/packages/shared/code-nav.ts +436 -0
- package/3p/pi-plannotator/packages/shared/compress.ts +51 -0
- package/3p/pi-plannotator/packages/shared/config.ts +262 -0
- package/3p/pi-plannotator/packages/shared/crypto.test.ts +172 -0
- package/3p/pi-plannotator/packages/shared/crypto.ts +97 -0
- package/3p/pi-plannotator/packages/shared/data-dir.ts +42 -0
- package/3p/pi-plannotator/packages/shared/diff-paths.test.ts +30 -0
- package/3p/pi-plannotator/packages/shared/diff-paths.ts +137 -0
- package/3p/pi-plannotator/packages/shared/draft.ts +64 -0
- package/3p/pi-plannotator/packages/shared/external-annotation.ts +397 -0
- package/3p/pi-plannotator/packages/shared/extract-code-paths.test.ts +59 -0
- package/3p/pi-plannotator/packages/shared/extract-code-paths.ts +66 -0
- package/3p/pi-plannotator/packages/shared/favicon.ts +5 -0
- package/3p/pi-plannotator/packages/shared/feedback-templates.test.ts +65 -0
- package/3p/pi-plannotator/packages/shared/feedback-templates.ts +29 -0
- package/3p/pi-plannotator/packages/shared/goal-setup.test.ts +231 -0
- package/3p/pi-plannotator/packages/shared/goal-setup.ts +336 -0
- package/3p/pi-plannotator/packages/shared/html-to-markdown.test.ts +62 -0
- package/3p/pi-plannotator/packages/shared/html-to-markdown.ts +32 -0
- package/3p/pi-plannotator/packages/shared/improvement-hooks.test.ts +135 -0
- package/3p/pi-plannotator/packages/shared/improvement-hooks.ts +115 -0
- package/3p/pi-plannotator/packages/shared/integrations-common.ts +243 -0
- package/3p/pi-plannotator/packages/shared/jj-core.test.ts +236 -0
- package/3p/pi-plannotator/packages/shared/jj-core.ts +433 -0
- package/3p/pi-plannotator/packages/shared/package.json +55 -0
- package/3p/pi-plannotator/packages/shared/pfm-reminder.test.ts +88 -0
- package/3p/pi-plannotator/packages/shared/pfm-reminder.ts +80 -0
- package/3p/pi-plannotator/packages/shared/pr-github.ts +661 -0
- package/3p/pi-plannotator/packages/shared/pr-gitlab.test.ts +202 -0
- package/3p/pi-plannotator/packages/shared/pr-gitlab.ts +620 -0
- package/3p/pi-plannotator/packages/shared/pr-provider.test.ts +266 -0
- package/3p/pi-plannotator/packages/shared/pr-provider.ts +123 -0
- package/3p/pi-plannotator/packages/shared/pr-stack.test.ts +104 -0
- package/3p/pi-plannotator/packages/shared/pr-stack.ts +194 -0
- package/3p/pi-plannotator/packages/shared/pr-types.ts +326 -0
- package/3p/pi-plannotator/packages/shared/project.ts +71 -0
- package/3p/pi-plannotator/packages/shared/prompts-integration.test.ts +421 -0
- package/3p/pi-plannotator/packages/shared/prompts.test.ts +504 -0
- package/3p/pi-plannotator/packages/shared/prompts.ts +247 -0
- package/3p/pi-plannotator/packages/shared/reference-common.ts +87 -0
- package/3p/pi-plannotator/packages/shared/repo.ts +71 -0
- package/3p/pi-plannotator/packages/shared/resolve-file.test.ts +113 -0
- package/3p/pi-plannotator/packages/shared/resolve-file.ts +509 -0
- package/3p/pi-plannotator/packages/shared/review-args.test.ts +64 -0
- package/3p/pi-plannotator/packages/shared/review-args.ts +85 -0
- package/3p/pi-plannotator/packages/shared/review-core.test.ts +286 -0
- package/3p/pi-plannotator/packages/shared/review-core.ts +895 -0
- package/3p/pi-plannotator/packages/shared/review-workspace-node.ts +230 -0
- package/3p/pi-plannotator/packages/shared/review-workspace.ts +436 -0
- package/3p/pi-plannotator/packages/shared/semantic-diff-types.ts +76 -0
- package/3p/pi-plannotator/packages/shared/semantic-diff.test.ts +322 -0
- package/3p/pi-plannotator/packages/shared/semantic-diff.ts +520 -0
- package/3p/pi-plannotator/packages/shared/storage.ts +378 -0
- package/3p/pi-plannotator/packages/shared/tour.ts +61 -0
- package/3p/pi-plannotator/packages/shared/tsconfig.json +15 -0
- package/3p/pi-plannotator/packages/shared/types.ts +29 -0
- package/3p/pi-plannotator/packages/shared/url-to-markdown.test.ts +177 -0
- package/3p/pi-plannotator/packages/shared/url-to-markdown.ts +351 -0
- package/3p/pi-plannotator/packages/shared/vcs-core.test.ts +332 -0
- package/3p/pi-plannotator/packages/shared/vcs-core.ts +482 -0
- package/3p/pi-plannotator/packages/shared/worktree-pool.test.ts +162 -0
- package/3p/pi-plannotator/packages/shared/worktree-pool.ts +103 -0
- package/3p/pi-plannotator/packages/shared/worktree.ts +119 -0
- package/3p/pi-subagents/package.json +50 -0
- package/3p/pi-subagents/src/agent-manager.ts +413 -0
- package/3p/pi-subagents/src/agent-runner.ts +502 -0
- package/3p/pi-subagents/src/agent-types.ts +248 -0
- package/3p/pi-subagents/src/context.ts +58 -0
- package/3p/pi-subagents/src/cross-extension-rpc.ts +135 -0
- package/3p/pi-subagents/src/custom-agents.ts +137 -0
- package/3p/pi-subagents/src/default-agents.ts +144 -0
- package/3p/pi-subagents/src/env.ts +33 -0
- package/3p/pi-subagents/src/group-join.ts +141 -0
- package/3p/pi-subagents/src/index.ts +1811 -0
- package/3p/pi-subagents/src/invocation-config.ts +40 -0
- package/3p/pi-subagents/src/memory.ts +165 -0
- package/3p/pi-subagents/src/model-resolver.ts +81 -0
- package/3p/pi-subagents/src/output-file.ts +77 -0
- package/3p/pi-subagents/src/prompts.ts +85 -0
- package/3p/pi-subagents/src/skill-loader.ts +79 -0
- package/3p/pi-subagents/src/types.ts +111 -0
- package/3p/pi-subagents/src/ui/agent-widget.ts +496 -0
- package/3p/pi-subagents/src/ui/conversation-viewer.ts +250 -0
- package/3p/pi-subagents/src/worktree.ts +162 -0
- package/3p/pi-tasks/package.json +52 -0
- package/3p/pi-tasks/src/auto-clear.ts +91 -0
- package/3p/pi-tasks/src/index.ts +1150 -0
- package/3p/pi-tasks/src/process-tracker.ts +140 -0
- package/3p/pi-tasks/src/reminder-cadence.ts +90 -0
- package/3p/pi-tasks/src/task-store.ts +324 -0
- package/3p/pi-tasks/src/tasks-config.ts +27 -0
- package/3p/pi-tasks/src/types.ts +40 -0
- package/3p/pi-tasks/src/ui/settings-menu.ts +152 -0
- package/3p/pi-tasks/src/ui/task-widget.ts +296 -0
- package/AGENTS.md +28 -0
- package/LICENSE +201 -0
- package/extensions/orchestrator/agents/brainstorm-reviewer.ts +77 -0
- package/extensions/orchestrator/agents/code-reviewer.ts +89 -0
- package/extensions/orchestrator/agents/explore.ts +33 -0
- package/extensions/orchestrator/agents/librarian.ts +43 -0
- package/extensions/orchestrator/agents/plan-reviewer.ts +75 -0
- package/extensions/orchestrator/agents/planner.ts +61 -0
- package/extensions/orchestrator/agents/registry.ts +156 -0
- package/extensions/orchestrator/agents/task.ts +50 -0
- package/extensions/orchestrator/agents/tool-routing.ts +84 -0
- package/extensions/orchestrator/ast-search.ts +85 -0
- package/extensions/orchestrator/cbm.ts +330 -0
- package/extensions/orchestrator/command-handlers.test.ts +163 -0
- package/extensions/orchestrator/command-handlers.ts +116 -0
- package/extensions/orchestrator/commands.test.ts +81 -0
- package/extensions/orchestrator/commands.ts +75 -0
- package/extensions/orchestrator/config.test.ts +221 -0
- package/extensions/orchestrator/config.ts +230 -0
- package/extensions/orchestrator/context.test.ts +293 -0
- package/extensions/orchestrator/context.ts +206 -0
- package/extensions/orchestrator/event-handlers.test.ts +190 -0
- package/extensions/orchestrator/event-handlers.ts +1416 -0
- package/extensions/orchestrator/exa.ts +104 -0
- package/extensions/orchestrator/flant-infra.ts +486 -0
- package/extensions/orchestrator/index.ts +80 -0
- package/extensions/orchestrator/integration.test.ts +1214 -0
- package/extensions/orchestrator/orchestrator.test.ts +252 -0
- package/extensions/orchestrator/orchestrator.ts +538 -0
- package/extensions/orchestrator/phases/brainstorm.test.ts +15 -0
- package/extensions/orchestrator/phases/brainstorm.ts +273 -0
- package/extensions/orchestrator/phases/implementation.ts +40 -0
- package/extensions/orchestrator/phases/machine.test.ts +293 -0
- package/extensions/orchestrator/phases/machine.ts +209 -0
- package/extensions/orchestrator/phases/planning.ts +255 -0
- package/extensions/orchestrator/phases/review.ts +193 -0
- package/extensions/orchestrator/plannotator.ts +56 -0
- package/extensions/orchestrator/pp-menu.ts +866 -0
- package/extensions/orchestrator/state.test.ts +343 -0
- package/extensions/orchestrator/state.ts +237 -0
- package/extensions/orchestrator/validate-artifacts.test.ts +88 -0
- package/extensions/orchestrator/validate-artifacts.ts +272 -0
- package/extensions/orchestrator/vendor.d.ts +26 -0
- package/package.json +73 -0
- package/scripts/postinstall.sh +18 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Detection Tests
|
|
3
|
+
*
|
|
4
|
+
* Run: bun test apps/hook/server/project.test.ts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from "bun:test";
|
|
8
|
+
import { sanitizeTag, extractRepoName, extractDirName, detectProjectName } from "./project";
|
|
9
|
+
|
|
10
|
+
describe("sanitizeTag", () => {
|
|
11
|
+
test("lowercases input", () => {
|
|
12
|
+
expect(sanitizeTag("MyProject")).toBe("myproject");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("replaces spaces with hyphens", () => {
|
|
16
|
+
expect(sanitizeTag("my project")).toBe("my-project");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("replaces underscores with hyphens", () => {
|
|
20
|
+
expect(sanitizeTag("my_project")).toBe("my-project");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("removes special characters", () => {
|
|
24
|
+
expect(sanitizeTag("my@project!name")).toBe("myprojectname");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("collapses multiple hyphens", () => {
|
|
28
|
+
expect(sanitizeTag("my--project")).toBe("my-project");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("trims to 30 chars", () => {
|
|
32
|
+
const long = "a".repeat(50);
|
|
33
|
+
expect(sanitizeTag(long)?.length).toBe(30);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("returns null for empty string", () => {
|
|
37
|
+
expect(sanitizeTag("")).toBeNull();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("returns null for single char", () => {
|
|
41
|
+
expect(sanitizeTag("a")).toBeNull();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("returns null for null/undefined", () => {
|
|
45
|
+
expect(sanitizeTag(null as any)).toBeNull();
|
|
46
|
+
expect(sanitizeTag(undefined as any)).toBeNull();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("extractRepoName", () => {
|
|
51
|
+
test("extracts name from full path", () => {
|
|
52
|
+
expect(extractRepoName("/Users/dev/projects/my-app")).toBe("my-app");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("handles trailing slash", () => {
|
|
56
|
+
expect(extractRepoName("/Users/dev/my-app/")).toBe("my-app");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("handles multiple trailing slashes", () => {
|
|
60
|
+
expect(extractRepoName("/home/user/repo///")).toBe("repo");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("returns null for empty string", () => {
|
|
64
|
+
expect(extractRepoName("")).toBeNull();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("returns null for just slash", () => {
|
|
68
|
+
expect(extractRepoName("/")).toBeNull();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("extractDirName", () => {
|
|
73
|
+
test("extracts directory name", () => {
|
|
74
|
+
expect(extractDirName("/home/user/workspace")).toBe("workspace");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("skips generic names", () => {
|
|
78
|
+
expect(extractDirName("/home")).toBeNull();
|
|
79
|
+
expect(extractDirName("/Users")).toBeNull();
|
|
80
|
+
expect(extractDirName("/root")).toBeNull();
|
|
81
|
+
expect(extractDirName("/tmp")).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("returns null for root path", () => {
|
|
85
|
+
expect(extractDirName("/")).toBeNull();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("sanitizes the result", () => {
|
|
89
|
+
expect(extractDirName("/home/user/My Project")).toBe("my-project");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("detectProjectName", () => {
|
|
94
|
+
test("returns a string or null", async () => {
|
|
95
|
+
const result = await detectProjectName();
|
|
96
|
+
expect(result === null || typeof result === "string").toBe(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("result is sanitized if not null", async () => {
|
|
100
|
+
const result = await detectProjectName();
|
|
101
|
+
if (result) {
|
|
102
|
+
expect(result).toMatch(/^[a-z0-9-]+$/);
|
|
103
|
+
expect(result.length).toBeGreaterThanOrEqual(2);
|
|
104
|
+
expect(result.length).toBeLessThanOrEqual(30);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Verify we detect a repo name from the current working directory.
|
|
109
|
+
// The exact name depends on the checkout path (local vs CI).
|
|
110
|
+
test("detects a valid repo name", async () => {
|
|
111
|
+
const result = await detectProjectName();
|
|
112
|
+
expect(result).not.toBeNull();
|
|
113
|
+
expect(result!.length).toBeGreaterThanOrEqual(2);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Detection Utility
|
|
3
|
+
*
|
|
4
|
+
* Detects the current project name for tagging plans.
|
|
5
|
+
* Priority: git repo name > directory name > null
|
|
6
|
+
*
|
|
7
|
+
* Pure string functions re-exported from @plannotator/shared/project.
|
|
8
|
+
* detectProjectName() is Bun-specific (uses Bun.$).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { $ } from "bun";
|
|
12
|
+
import { extractRepoName, extractDirName } from "@plannotator/shared/project";
|
|
13
|
+
export { sanitizeTag, extractRepoName, extractDirName } from "@plannotator/shared/project";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Detect project name from current context
|
|
17
|
+
*
|
|
18
|
+
* Priority:
|
|
19
|
+
* 1. Git repository name (most reliable)
|
|
20
|
+
* 2. Current directory name (fallback)
|
|
21
|
+
* 3. null (if nothing useful found)
|
|
22
|
+
*/
|
|
23
|
+
export async function detectProjectName(): Promise<string | null> {
|
|
24
|
+
// Try git repo name first
|
|
25
|
+
try {
|
|
26
|
+
const result = await $`git rev-parse --show-toplevel`.quiet().nothrow();
|
|
27
|
+
if (result.exitCode === 0) {
|
|
28
|
+
const repoName = extractRepoName(result.stdout.toString());
|
|
29
|
+
if (repoName) return repoName;
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
// Git not available or not in a repo - continue to fallback
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Fallback to current directory name
|
|
36
|
+
try {
|
|
37
|
+
const cwd = process.cwd();
|
|
38
|
+
const dirName = extractDirName(cwd);
|
|
39
|
+
if (dirName) return dirName;
|
|
40
|
+
} catch {
|
|
41
|
+
// process.cwd() failed (rare)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reference/document route handlers for the plan server.
|
|
3
|
+
*
|
|
4
|
+
* Handles /api/doc, /api/obsidian/vaults, /api/reference/obsidian/files,
|
|
5
|
+
* /api/reference/obsidian/doc, and /api/reference/files. Extracted from index.ts for modularity.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, statSync } from "fs";
|
|
9
|
+
import { resolve } from "path";
|
|
10
|
+
import { buildFileTree, FILE_BROWSER_EXCLUDED } from "@plannotator/shared/reference-common";
|
|
11
|
+
import { parseCodePath } from "@plannotator/shared/code-file";
|
|
12
|
+
import { detectObsidianVaults } from "./integrations";
|
|
13
|
+
import {
|
|
14
|
+
isAbsoluteUserPath,
|
|
15
|
+
isCodeFilePath,
|
|
16
|
+
resolveCodeFile,
|
|
17
|
+
resolveMarkdownFile,
|
|
18
|
+
resolveUserPath,
|
|
19
|
+
isWithinProjectRoot,
|
|
20
|
+
warmFileListCache,
|
|
21
|
+
} from "@plannotator/shared/resolve-file";
|
|
22
|
+
import { htmlToMarkdown } from "@plannotator/shared/html-to-markdown";
|
|
23
|
+
import { preloadFile } from "@pierre/diffs/ssr";
|
|
24
|
+
|
|
25
|
+
// --- Route handlers ---
|
|
26
|
+
|
|
27
|
+
/** Serve a linked markdown document. Resolves absolute, relative, or bare filename paths. */
|
|
28
|
+
export async function handleDoc(req: Request): Promise<Response> {
|
|
29
|
+
const url = new URL(req.url);
|
|
30
|
+
const requestedPath = url.searchParams.get("path");
|
|
31
|
+
if (!requestedPath) {
|
|
32
|
+
return Response.json({ error: "Missing path parameter" }, { status: 400 });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Side-channel: kick off a code-file walk for the project root so that any
|
|
36
|
+
// /api/doc/exists POST issued by the rendered linked-doc lands on warm cache.
|
|
37
|
+
void warmFileListCache(process.cwd(), "code");
|
|
38
|
+
|
|
39
|
+
// If a base directory is provided, try resolving relative to it first
|
|
40
|
+
// (used by annotate mode to resolve paths relative to the source file).
|
|
41
|
+
// No isWithinProjectRoot check here — intentional, matches pre-existing
|
|
42
|
+
// markdown behavior. The base param is set server-side by the annotate
|
|
43
|
+
// server (see annotate.ts /api/doc route). The standalone HTML block
|
|
44
|
+
// below (no base) retains its cwd-based containment check.
|
|
45
|
+
const base = url.searchParams.get("base");
|
|
46
|
+
const resolvedBase = base ? resolveUserPath(base) : null;
|
|
47
|
+
// HTML renders raw by default; `?convert=1` (set by the frontend when the session's
|
|
48
|
+
// --markdown preference is on) forces Turndown conversion instead.
|
|
49
|
+
const convert = url.searchParams.get("convert") === "1";
|
|
50
|
+
if (
|
|
51
|
+
resolvedBase &&
|
|
52
|
+
!isAbsoluteUserPath(requestedPath) &&
|
|
53
|
+
/\.(mdx?|html?)$/i.test(requestedPath)
|
|
54
|
+
) {
|
|
55
|
+
const fromBase = resolveUserPath(requestedPath, resolvedBase);
|
|
56
|
+
try {
|
|
57
|
+
const file = Bun.file(fromBase);
|
|
58
|
+
if (await file.exists()) {
|
|
59
|
+
const raw = await file.text();
|
|
60
|
+
const isHtml = /\.html?$/i.test(requestedPath);
|
|
61
|
+
if (isHtml && !convert) {
|
|
62
|
+
return Response.json({ rawHtml: raw, renderAs: "html", filepath: fromBase });
|
|
63
|
+
}
|
|
64
|
+
const markdown = isHtml ? htmlToMarkdown(raw) : raw;
|
|
65
|
+
return Response.json({ markdown, filepath: fromBase, isConverted: isHtml, renderAs: "markdown" });
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
/* fall through to standard resolution */
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// HTML files: resolve directly (not via resolveMarkdownFile which only handles .md/.mdx)
|
|
73
|
+
const projectRoot = process.cwd();
|
|
74
|
+
if (/\.html?$/i.test(requestedPath)) {
|
|
75
|
+
const resolvedHtml = resolveUserPath(requestedPath, resolvedBase || projectRoot);
|
|
76
|
+
if (!isWithinProjectRoot(resolvedHtml, projectRoot)) {
|
|
77
|
+
return Response.json({ error: "Access denied: path is outside project root" }, { status: 403 });
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const file = Bun.file(resolvedHtml);
|
|
81
|
+
if (await file.exists()) {
|
|
82
|
+
const html = await file.text();
|
|
83
|
+
if (!convert) {
|
|
84
|
+
return Response.json({ rawHtml: html, renderAs: "html", filepath: resolvedHtml });
|
|
85
|
+
}
|
|
86
|
+
const markdown = htmlToMarkdown(html);
|
|
87
|
+
return Response.json({ markdown, filepath: resolvedHtml, isConverted: true, renderAs: "markdown" });
|
|
88
|
+
}
|
|
89
|
+
} catch { /* fall through */ }
|
|
90
|
+
return Response.json({ error: `File not found: ${requestedPath}` }, { status: 404 });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Code files: try literal resolve first; on miss, fall back to the smart
|
|
94
|
+
// resolver which walks the project for case-insensitive / suffix matches.
|
|
95
|
+
if (isCodeFilePath(requestedPath)) {
|
|
96
|
+
const parsed = parseCodePath(requestedPath);
|
|
97
|
+
const cleanPath = parsed.filePath;
|
|
98
|
+
const literalPath = resolveUserPath(cleanPath, resolvedBase || projectRoot);
|
|
99
|
+
const literalAllowed = resolvedBase || isWithinProjectRoot(literalPath, projectRoot);
|
|
100
|
+
|
|
101
|
+
let resolvedCode: string | null = null;
|
|
102
|
+
if (literalAllowed) {
|
|
103
|
+
try {
|
|
104
|
+
const file = Bun.file(literalPath);
|
|
105
|
+
if (await file.exists()) resolvedCode = literalPath;
|
|
106
|
+
} catch { /* fall through */ }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!resolvedCode) {
|
|
110
|
+
const result = await resolveCodeFile(cleanPath, projectRoot);
|
|
111
|
+
if (result.kind === "found") {
|
|
112
|
+
resolvedCode = result.path;
|
|
113
|
+
} else if (result.kind === "ambiguous") {
|
|
114
|
+
const prefix = `${projectRoot}/`;
|
|
115
|
+
const relative = result.matches.map((m) =>
|
|
116
|
+
m.startsWith(prefix) ? m.slice(prefix.length) : m,
|
|
117
|
+
);
|
|
118
|
+
return Response.json(
|
|
119
|
+
{ error: `Ambiguous path '${requestedPath}'`, matches: relative },
|
|
120
|
+
{ status: 400 },
|
|
121
|
+
);
|
|
122
|
+
} else if (result.kind === "unavailable") {
|
|
123
|
+
return Response.json({ error: `Cannot scan project: ${requestedPath}` }, { status: 404 });
|
|
124
|
+
} else {
|
|
125
|
+
return Response.json({ error: `File not found: ${requestedPath}` }, { status: 404 });
|
|
126
|
+
}
|
|
127
|
+
if (!isWithinProjectRoot(resolvedCode, projectRoot)) {
|
|
128
|
+
return Response.json({ error: "Access denied: path is outside project root" }, { status: 403 });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const file = Bun.file(resolvedCode);
|
|
134
|
+
if (file.size > 2 * 1024 * 1024) {
|
|
135
|
+
return Response.json({ error: "File too large (max 2MB)" }, { status: 413 });
|
|
136
|
+
}
|
|
137
|
+
const contents = await file.text();
|
|
138
|
+
const displayName = resolvedCode.split("/").pop() || resolvedCode;
|
|
139
|
+
let prerenderedHTML: string | undefined;
|
|
140
|
+
try {
|
|
141
|
+
const result = await preloadFile({
|
|
142
|
+
file: { name: displayName, contents },
|
|
143
|
+
options: { disableFileHeader: true },
|
|
144
|
+
});
|
|
145
|
+
prerenderedHTML = result.prerenderedHTML;
|
|
146
|
+
} catch {
|
|
147
|
+
// Fall back to client-side rendering
|
|
148
|
+
}
|
|
149
|
+
return Response.json({ codeFile: true, contents, filepath: resolvedCode, prerenderedHTML, line: parsed.line, lineEnd: parsed.lineEnd });
|
|
150
|
+
} catch {
|
|
151
|
+
return Response.json({ error: `File not found: ${requestedPath}` }, { status: 404 });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const result = resolveMarkdownFile(requestedPath, projectRoot);
|
|
156
|
+
|
|
157
|
+
if (result.kind === "ambiguous") {
|
|
158
|
+
return Response.json(
|
|
159
|
+
{
|
|
160
|
+
error: `Ambiguous filename '${result.input}': found ${result.matches.length} matches`,
|
|
161
|
+
matches: result.matches,
|
|
162
|
+
},
|
|
163
|
+
{ status: 400 },
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (result.kind === "not_found" || result.kind === "unavailable") {
|
|
168
|
+
return Response.json(
|
|
169
|
+
{ error: `File not found: ${result.input}` },
|
|
170
|
+
{ status: 404 },
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const markdown = await Bun.file(result.path).text();
|
|
176
|
+
return Response.json({ markdown, filepath: result.path });
|
|
177
|
+
} catch {
|
|
178
|
+
return Response.json({ error: "Failed to read file" }, { status: 500 });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Batch existence check for code-file paths the renderer wants to linkify.
|
|
184
|
+
* POST /api/doc/exists with { paths: string[] } returns { results: { [path]: ValidationEntry } }.
|
|
185
|
+
* Reads from the warm file-list cache populated at plan/annotate load.
|
|
186
|
+
*
|
|
187
|
+
* TODO(security): two related leaks of arbitrary file existence:
|
|
188
|
+
* 1. Absolute paths in `paths[]` are probed verbatim — `resolveCodeFile`
|
|
189
|
+
* returns `{ kind: 'found', path: abs }` for any existing absolute file
|
|
190
|
+
* with no project-root containment check. A malicious shared plan with
|
|
191
|
+
* backtick-wrapped absolute paths (e.g. `/Users/x/.aws/…`) leaks file
|
|
192
|
+
* existence + canonical path back to the caller.
|
|
193
|
+
* 2. The `base` field is honored verbatim — a hostile sender can supply
|
|
194
|
+
* `base=/Users/x/.aws` + `paths=["credentials.json"]` and the resolver
|
|
195
|
+
* will check `<base>/<path>` existence with no containment check.
|
|
196
|
+
* Mitigation: reject absolute inputs and `isWithinProjectRoot`-filter the
|
|
197
|
+
* resolved base before passing it to `resolveCodeFile` (or filter `r.path`
|
|
198
|
+
* before recording a found result). Mirror in apps/pi-extension/server/reference.ts.
|
|
199
|
+
*/
|
|
200
|
+
export async function handleDocExists(req: Request): Promise<Response> {
|
|
201
|
+
let body: unknown;
|
|
202
|
+
try {
|
|
203
|
+
body = await req.json();
|
|
204
|
+
} catch {
|
|
205
|
+
return Response.json({ error: "Invalid JSON" }, { status: 400 });
|
|
206
|
+
}
|
|
207
|
+
const paths = (body as { paths?: unknown })?.paths;
|
|
208
|
+
if (!Array.isArray(paths) || !paths.every((p) => typeof p === "string")) {
|
|
209
|
+
return Response.json({ error: "Expected { paths: string[] }" }, { status: 400 });
|
|
210
|
+
}
|
|
211
|
+
if (paths.length > 500) {
|
|
212
|
+
return Response.json({ error: "Too many paths (max 500)" }, { status: 400 });
|
|
213
|
+
}
|
|
214
|
+
const baseRaw = (body as { base?: unknown })?.base;
|
|
215
|
+
const baseDir = typeof baseRaw === "string" && baseRaw.length > 0
|
|
216
|
+
? resolveUserPath(baseRaw)
|
|
217
|
+
: undefined;
|
|
218
|
+
|
|
219
|
+
const projectRoot = process.cwd();
|
|
220
|
+
const results: Record<
|
|
221
|
+
string,
|
|
222
|
+
| { status: "found"; resolved: string }
|
|
223
|
+
| { status: "ambiguous"; matches: string[] }
|
|
224
|
+
| { status: "missing" }
|
|
225
|
+
| { status: "unavailable" }
|
|
226
|
+
> = {};
|
|
227
|
+
|
|
228
|
+
await Promise.all(
|
|
229
|
+
(paths as string[]).map(async (p) => {
|
|
230
|
+
const cleanP = parseCodePath(p).filePath;
|
|
231
|
+
const r = await resolveCodeFile(cleanP, projectRoot, baseDir);
|
|
232
|
+
if (r.kind === "found") {
|
|
233
|
+
results[p] = { status: "found", resolved: r.path };
|
|
234
|
+
} else if (r.kind === "ambiguous") {
|
|
235
|
+
const prefix = `${projectRoot}/`;
|
|
236
|
+
results[p] = {
|
|
237
|
+
status: "ambiguous",
|
|
238
|
+
matches: r.matches.map((m) => (m.startsWith(prefix) ? m.slice(prefix.length) : m)),
|
|
239
|
+
};
|
|
240
|
+
} else if (r.kind === "unavailable") {
|
|
241
|
+
results[p] = { status: "unavailable" };
|
|
242
|
+
} else {
|
|
243
|
+
results[p] = { status: "missing" };
|
|
244
|
+
}
|
|
245
|
+
}),
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
return Response.json({ results });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Detect available Obsidian vaults. */
|
|
252
|
+
export function handleObsidianVaults(): Response {
|
|
253
|
+
const vaults = detectObsidianVaults();
|
|
254
|
+
return Response.json({ vaults });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** List Obsidian vault files as a nested tree. */
|
|
258
|
+
export async function handleObsidianFiles(req: Request): Promise<Response> {
|
|
259
|
+
const url = new URL(req.url);
|
|
260
|
+
const vaultPath = url.searchParams.get("vaultPath");
|
|
261
|
+
if (!vaultPath) {
|
|
262
|
+
return Response.json(
|
|
263
|
+
{ error: "Missing vaultPath parameter" },
|
|
264
|
+
{ status: 400 },
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const resolvedVault = resolveUserPath(vaultPath);
|
|
269
|
+
if (!existsSync(resolvedVault) || !statSync(resolvedVault).isDirectory()) {
|
|
270
|
+
return Response.json({ error: "Invalid vault path" }, { status: 400 });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
const glob = new Bun.Glob("**/*.{md,mdx}");
|
|
275
|
+
const files: string[] = [];
|
|
276
|
+
for await (const match of glob.scan({
|
|
277
|
+
cwd: resolvedVault,
|
|
278
|
+
onlyFiles: true,
|
|
279
|
+
})) {
|
|
280
|
+
if (match.includes(".obsidian/") || match.includes(".trash/")) continue;
|
|
281
|
+
files.push(match);
|
|
282
|
+
}
|
|
283
|
+
files.sort();
|
|
284
|
+
|
|
285
|
+
const tree = buildFileTree(files);
|
|
286
|
+
return Response.json({ tree });
|
|
287
|
+
} catch {
|
|
288
|
+
return Response.json(
|
|
289
|
+
{ error: "Failed to list vault files" },
|
|
290
|
+
{ status: 500 },
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Read an Obsidian vault document. Supports direct path and bare filename search. */
|
|
296
|
+
export async function handleObsidianDoc(req: Request): Promise<Response> {
|
|
297
|
+
const url = new URL(req.url);
|
|
298
|
+
const vaultPath = url.searchParams.get("vaultPath");
|
|
299
|
+
const filePath = url.searchParams.get("path");
|
|
300
|
+
if (!vaultPath || !filePath) {
|
|
301
|
+
return Response.json(
|
|
302
|
+
{ error: "Missing vaultPath or path parameter" },
|
|
303
|
+
{ status: 400 },
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
if (!/\.mdx?$/i.test(filePath)) {
|
|
307
|
+
return Response.json(
|
|
308
|
+
{ error: "Only markdown files are supported" },
|
|
309
|
+
{ status: 400 },
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const resolvedVault = resolveUserPath(vaultPath);
|
|
314
|
+
let resolvedFile = resolve(resolvedVault, filePath);
|
|
315
|
+
|
|
316
|
+
// If direct path doesn't exist and it's a bare filename, search the vault
|
|
317
|
+
if (!existsSync(resolvedFile) && !filePath.includes("/")) {
|
|
318
|
+
const glob = new Bun.Glob(`**/${filePath}`);
|
|
319
|
+
const matches: string[] = [];
|
|
320
|
+
for await (const match of glob.scan({
|
|
321
|
+
cwd: resolvedVault,
|
|
322
|
+
onlyFiles: true,
|
|
323
|
+
})) {
|
|
324
|
+
if (match.includes(".obsidian/") || match.includes(".trash/")) continue;
|
|
325
|
+
matches.push(resolve(resolvedVault, match));
|
|
326
|
+
}
|
|
327
|
+
if (matches.length === 1) {
|
|
328
|
+
resolvedFile = matches[0];
|
|
329
|
+
} else if (matches.length > 1) {
|
|
330
|
+
const relativePaths = matches.map((m) =>
|
|
331
|
+
m.replace(resolvedVault + "/", ""),
|
|
332
|
+
);
|
|
333
|
+
return Response.json(
|
|
334
|
+
{
|
|
335
|
+
error: `Ambiguous filename '${filePath}': found ${matches.length} matches`,
|
|
336
|
+
matches: relativePaths,
|
|
337
|
+
},
|
|
338
|
+
{ status: 400 },
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Security: must be within vault
|
|
344
|
+
if (!resolvedFile.startsWith(resolvedVault + "/")) {
|
|
345
|
+
return Response.json(
|
|
346
|
+
{ error: "Access denied: path is outside vault" },
|
|
347
|
+
{ status: 403 },
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
const file = Bun.file(resolvedFile);
|
|
353
|
+
if (!(await file.exists())) {
|
|
354
|
+
return Response.json(
|
|
355
|
+
{ error: `File not found: ${filePath}` },
|
|
356
|
+
{ status: 404 },
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
const markdown = await file.text();
|
|
360
|
+
return Response.json({ markdown, filepath: resolvedFile });
|
|
361
|
+
} catch {
|
|
362
|
+
return Response.json({ error: "Failed to read file" }, { status: 500 });
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// --- File Browser ---
|
|
367
|
+
|
|
368
|
+
/** List markdown files in a directory as a nested tree. */
|
|
369
|
+
export async function handleFileBrowserFiles(req: Request): Promise<Response> {
|
|
370
|
+
const url = new URL(req.url);
|
|
371
|
+
const dirPath = url.searchParams.get("dirPath");
|
|
372
|
+
if (!dirPath) {
|
|
373
|
+
return Response.json(
|
|
374
|
+
{ error: "Missing dirPath parameter" },
|
|
375
|
+
{ status: 400 },
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const resolvedDir = resolveUserPath(dirPath);
|
|
380
|
+
if (!existsSync(resolvedDir) || !statSync(resolvedDir).isDirectory()) {
|
|
381
|
+
return Response.json({ error: "Invalid directory path" }, { status: 400 });
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
const glob = new Bun.Glob("**/*.{md,mdx,html,htm}");
|
|
386
|
+
const files: string[] = [];
|
|
387
|
+
for await (const match of glob.scan({
|
|
388
|
+
cwd: resolvedDir,
|
|
389
|
+
onlyFiles: true,
|
|
390
|
+
})) {
|
|
391
|
+
if (FILE_BROWSER_EXCLUDED.some((dir) => match.includes(dir))) continue;
|
|
392
|
+
files.push(match);
|
|
393
|
+
}
|
|
394
|
+
files.sort();
|
|
395
|
+
|
|
396
|
+
const tree = buildFileTree(files);
|
|
397
|
+
return Response.json({ tree });
|
|
398
|
+
} catch {
|
|
399
|
+
return Response.json(
|
|
400
|
+
{ error: "Failed to list directory files" },
|
|
401
|
+
{ status: 500 },
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
}
|