@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,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote Detection & Port Config Tests
|
|
3
|
+
*
|
|
4
|
+
* Run: bun test packages/server/remote.test.ts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
8
|
+
import { isRemoteSession, getServerHostname, getServerPort } from "./remote";
|
|
9
|
+
|
|
10
|
+
// Save and restore env between tests
|
|
11
|
+
const savedEnv: Record<string, string | undefined> = {};
|
|
12
|
+
const envKeys = ["PLANNOTATOR_REMOTE", "PLANNOTATOR_PORT", "SSH_TTY", "SSH_CONNECTION"];
|
|
13
|
+
|
|
14
|
+
function clearEnv() {
|
|
15
|
+
for (const key of envKeys) {
|
|
16
|
+
savedEnv[key] = process.env[key];
|
|
17
|
+
delete process.env[key];
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
for (const key of envKeys) {
|
|
23
|
+
if (savedEnv[key] !== undefined) {
|
|
24
|
+
process.env[key] = savedEnv[key];
|
|
25
|
+
} else {
|
|
26
|
+
delete process.env[key];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("isRemoteSession", () => {
|
|
32
|
+
test("false by default (no env vars)", () => {
|
|
33
|
+
clearEnv();
|
|
34
|
+
expect(isRemoteSession()).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("true when PLANNOTATOR_REMOTE=1", () => {
|
|
38
|
+
clearEnv();
|
|
39
|
+
process.env.PLANNOTATOR_REMOTE = "1";
|
|
40
|
+
expect(isRemoteSession()).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("true when PLANNOTATOR_REMOTE=true", () => {
|
|
44
|
+
clearEnv();
|
|
45
|
+
process.env.PLANNOTATOR_REMOTE = "true";
|
|
46
|
+
expect(isRemoteSession()).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("false when PLANNOTATOR_REMOTE=0", () => {
|
|
50
|
+
clearEnv();
|
|
51
|
+
process.env.PLANNOTATOR_REMOTE = "0";
|
|
52
|
+
expect(isRemoteSession()).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("false when PLANNOTATOR_REMOTE=false", () => {
|
|
56
|
+
clearEnv();
|
|
57
|
+
process.env.PLANNOTATOR_REMOTE = "false";
|
|
58
|
+
expect(isRemoteSession()).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("PLANNOTATOR_REMOTE=false overrides SSH_TTY", () => {
|
|
62
|
+
clearEnv();
|
|
63
|
+
process.env.PLANNOTATOR_REMOTE = "false";
|
|
64
|
+
process.env.SSH_TTY = "/dev/pts/0";
|
|
65
|
+
expect(isRemoteSession()).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("PLANNOTATOR_REMOTE=0 overrides SSH_CONNECTION", () => {
|
|
69
|
+
clearEnv();
|
|
70
|
+
process.env.PLANNOTATOR_REMOTE = "0";
|
|
71
|
+
process.env.SSH_CONNECTION = "192.168.1.1 12345 192.168.1.2 22";
|
|
72
|
+
expect(isRemoteSession()).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("true when SSH_TTY is set (legacy)", () => {
|
|
76
|
+
clearEnv();
|
|
77
|
+
process.env.SSH_TTY = "/dev/pts/0";
|
|
78
|
+
expect(isRemoteSession()).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("true when SSH_CONNECTION is set (legacy)", () => {
|
|
82
|
+
clearEnv();
|
|
83
|
+
process.env.SSH_CONNECTION = "192.168.1.1 12345 192.168.1.2 22";
|
|
84
|
+
expect(isRemoteSession()).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("getServerPort", () => {
|
|
89
|
+
test("returns 0 for local session (random port)", () => {
|
|
90
|
+
clearEnv();
|
|
91
|
+
expect(getServerPort()).toBe(0);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("returns 19432 for remote session", () => {
|
|
95
|
+
clearEnv();
|
|
96
|
+
process.env.PLANNOTATOR_REMOTE = "1";
|
|
97
|
+
expect(getServerPort()).toBe(19432);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("returns 0 when PLANNOTATOR_REMOTE=false overrides SSH", () => {
|
|
101
|
+
clearEnv();
|
|
102
|
+
process.env.PLANNOTATOR_REMOTE = "false";
|
|
103
|
+
process.env.SSH_TTY = "/dev/pts/0";
|
|
104
|
+
expect(getServerPort()).toBe(0);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("explicit PLANNOTATOR_PORT overrides everything", () => {
|
|
108
|
+
clearEnv();
|
|
109
|
+
process.env.PLANNOTATOR_PORT = "8080";
|
|
110
|
+
expect(getServerPort()).toBe(8080);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("explicit port overrides remote default", () => {
|
|
114
|
+
clearEnv();
|
|
115
|
+
process.env.PLANNOTATOR_REMOTE = "1";
|
|
116
|
+
process.env.PLANNOTATOR_PORT = "3000";
|
|
117
|
+
expect(getServerPort()).toBe(3000);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("ignores invalid port (falls back to default)", () => {
|
|
121
|
+
clearEnv();
|
|
122
|
+
process.env.PLANNOTATOR_PORT = "not-a-number";
|
|
123
|
+
expect(getServerPort()).toBe(0);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("ignores out-of-range port", () => {
|
|
127
|
+
clearEnv();
|
|
128
|
+
process.env.PLANNOTATOR_PORT = "99999";
|
|
129
|
+
expect(getServerPort()).toBe(0);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("ignores zero port", () => {
|
|
133
|
+
clearEnv();
|
|
134
|
+
process.env.PLANNOTATOR_PORT = "0";
|
|
135
|
+
expect(getServerPort()).toBe(0);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("getServerHostname", () => {
|
|
140
|
+
test("returns loopback for local sessions", () => {
|
|
141
|
+
clearEnv();
|
|
142
|
+
expect(getServerHostname()).toBe("127.0.0.1");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("returns all interfaces for remote sessions", () => {
|
|
146
|
+
clearEnv();
|
|
147
|
+
process.env.PLANNOTATOR_REMOTE = "1";
|
|
148
|
+
expect(getServerHostname()).toBe("0.0.0.0");
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote session detection and port configuration
|
|
3
|
+
*
|
|
4
|
+
* Environment variables:
|
|
5
|
+
* PLANNOTATOR_REMOTE - Set to "1"/"true" to force remote, "0"/"false" to force local
|
|
6
|
+
* PLANNOTATOR_PORT - Fixed port to use (default: random locally, 19432 for remote)
|
|
7
|
+
*
|
|
8
|
+
* Legacy (still supported): SSH_TTY, SSH_CONNECTION
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const DEFAULT_REMOTE_PORT = 19432;
|
|
12
|
+
const LOOPBACK_HOST = "127.0.0.1";
|
|
13
|
+
|
|
14
|
+
function getRemoteOverride(): boolean | null {
|
|
15
|
+
const remote = process.env.PLANNOTATOR_REMOTE;
|
|
16
|
+
if (remote === undefined) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (remote === "1" || remote?.toLowerCase() === "true") {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (remote === "0" || remote?.toLowerCase() === "false") {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check if running in a remote session (SSH, devcontainer, etc.)
|
|
33
|
+
*/
|
|
34
|
+
export function isRemoteSession(): boolean {
|
|
35
|
+
const remoteOverride = getRemoteOverride();
|
|
36
|
+
if (remoteOverride !== null) {
|
|
37
|
+
return remoteOverride;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Legacy: SSH_TTY/SSH_CONNECTION (deprecated, silent)
|
|
41
|
+
if (process.env.SSH_TTY || process.env.SSH_CONNECTION) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get the server port to use
|
|
50
|
+
*/
|
|
51
|
+
export function getServerPort(): number {
|
|
52
|
+
// Explicit port from environment takes precedence
|
|
53
|
+
const envPort = process.env.PLANNOTATOR_PORT;
|
|
54
|
+
if (envPort) {
|
|
55
|
+
const parsed = parseInt(envPort, 10);
|
|
56
|
+
if (!isNaN(parsed) && parsed >= 0 && parsed < 65536) {
|
|
57
|
+
return parsed;
|
|
58
|
+
}
|
|
59
|
+
console.error(
|
|
60
|
+
`[Plannotator] Warning: Invalid PLANNOTATOR_PORT "${envPort}", using default`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Remote sessions use fixed port for port forwarding; local uses random
|
|
65
|
+
return isRemoteSession() ? DEFAULT_REMOTE_PORT : 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Bind local sessions to loopback, but keep remote sessions reachable via the
|
|
70
|
+
* container or host network interface for SSH/devcontainer/Docker forwarding.
|
|
71
|
+
*/
|
|
72
|
+
export function getServerHostname(): string {
|
|
73
|
+
return isRemoteSession() ? "0.0.0.0" : LOOPBACK_HOST;
|
|
74
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repository Info Detection
|
|
3
|
+
*
|
|
4
|
+
* Detects org/repo from git remote with fallback chain.
|
|
5
|
+
* Priority: org/repo from remote → repo name → directory name
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { $ } from "bun";
|
|
9
|
+
|
|
10
|
+
import type { RepoInfo } from "@plannotator/shared/repo";
|
|
11
|
+
import { parseRemoteUrl, parseRemoteHost, getDirName } from "@plannotator/shared/repo";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get current git branch
|
|
15
|
+
*/
|
|
16
|
+
async function getCurrentBranch(): Promise<string | undefined> {
|
|
17
|
+
try {
|
|
18
|
+
const result = await $`git rev-parse --abbrev-ref HEAD`.quiet().nothrow();
|
|
19
|
+
if (result.exitCode === 0) {
|
|
20
|
+
const branch = result.stdout.toString().trim();
|
|
21
|
+
return branch && branch !== "HEAD" ? branch : undefined;
|
|
22
|
+
}
|
|
23
|
+
} catch {
|
|
24
|
+
// Not in a git repo
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Detect repository info with fallback chain
|
|
31
|
+
*
|
|
32
|
+
* 1. Try org/repo from git remote origin
|
|
33
|
+
* 2. Fall back to git repo root directory name
|
|
34
|
+
* 3. Fall back to current working directory name
|
|
35
|
+
*/
|
|
36
|
+
export async function getRepoInfo(): Promise<RepoInfo | null> {
|
|
37
|
+
let branch: string | undefined;
|
|
38
|
+
|
|
39
|
+
// Try git remote URL first
|
|
40
|
+
try {
|
|
41
|
+
const result = await $`git remote get-url origin`.quiet().nothrow();
|
|
42
|
+
if (result.exitCode === 0) {
|
|
43
|
+
const remoteUrl = result.stdout.toString().trim();
|
|
44
|
+
const orgRepo = parseRemoteUrl(remoteUrl);
|
|
45
|
+
if (orgRepo) {
|
|
46
|
+
branch = await getCurrentBranch();
|
|
47
|
+
const host = parseRemoteHost(remoteUrl) ?? undefined;
|
|
48
|
+
return { display: orgRepo, branch, host };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
// Git not available
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Fallback: git repo root name
|
|
56
|
+
try {
|
|
57
|
+
const result = await $`git rev-parse --show-toplevel`.quiet().nothrow();
|
|
58
|
+
if (result.exitCode === 0) {
|
|
59
|
+
const repoName = getDirName(result.stdout.toString());
|
|
60
|
+
if (repoName) {
|
|
61
|
+
branch = await getCurrentBranch();
|
|
62
|
+
return { display: repoName, branch };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
// Not in a git repo
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Final fallback: current directory (no branch - not a git repo)
|
|
70
|
+
try {
|
|
71
|
+
const dirName = getDirName(process.cwd());
|
|
72
|
+
if (dirName) {
|
|
73
|
+
return { display: dirName };
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
// Shouldn't happen
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Resolution Tests
|
|
3
|
+
*
|
|
4
|
+
* Run: bun test packages/server/resolve-file.test.ts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
8
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { homedir, tmpdir } from "node:os";
|
|
10
|
+
import { join, resolve } from "node:path";
|
|
11
|
+
import {
|
|
12
|
+
expandHomePath,
|
|
13
|
+
isAbsoluteUserPath,
|
|
14
|
+
normalizeUserPathInput,
|
|
15
|
+
resolveMarkdownFile,
|
|
16
|
+
resolveUserPath,
|
|
17
|
+
} from "@plannotator/shared/resolve-file";
|
|
18
|
+
|
|
19
|
+
const tempDirs: string[] = [];
|
|
20
|
+
|
|
21
|
+
function createTempProject(
|
|
22
|
+
files: Record<string, string> = {},
|
|
23
|
+
baseDir = join(tmpdir(), "plannotator-resolve-file-"),
|
|
24
|
+
): string {
|
|
25
|
+
const root = mkdtempSync(baseDir);
|
|
26
|
+
tempDirs.push(root);
|
|
27
|
+
for (const [relativePath, content] of Object.entries(files)) {
|
|
28
|
+
const full = join(root, relativePath);
|
|
29
|
+
mkdirSync(join(full, ".."), { recursive: true });
|
|
30
|
+
writeFileSync(full, content);
|
|
31
|
+
}
|
|
32
|
+
return root;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
for (const dir of tempDirs.splice(0)) {
|
|
37
|
+
rmSync(dir, { recursive: true, force: true });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// --- User path normalization ---
|
|
42
|
+
|
|
43
|
+
describe("normalizeUserPathInput", () => {
|
|
44
|
+
test("expands tilde paths before normalization", () => {
|
|
45
|
+
expect(normalizeUserPathInput("~/test-plan.md")).toBe(
|
|
46
|
+
join(homedir(), "test-plan.md"),
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("strips wrapping quotes", () => {
|
|
51
|
+
expect(normalizeUserPathInput('"~/test-plan.md"')).toBe(
|
|
52
|
+
join(homedir(), "test-plan.md"),
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("converts MSYS paths on Windows", () => {
|
|
57
|
+
expect(normalizeUserPathInput("/c/Users/dev/test-plan.md", "win32")).toBe(
|
|
58
|
+
"C:/Users/dev/test-plan.md",
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("converts Cygwin paths on Windows", () => {
|
|
63
|
+
expect(normalizeUserPathInput("/cygdrive/c/Users/dev/test-plan.md", "win32")).toBe(
|
|
64
|
+
"C:/Users/dev/test-plan.md",
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("leaves non-Windows paths unchanged", () => {
|
|
69
|
+
expect(normalizeUserPathInput("/Users/dev/test-plan.md", "darwin")).toBe(
|
|
70
|
+
"/Users/dev/test-plan.md",
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("expandHomePath", () => {
|
|
76
|
+
test("expands bare home alias", () => {
|
|
77
|
+
expect(expandHomePath("~", "/tmp/home")).toBe("/tmp/home");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("expands home-relative paths", () => {
|
|
81
|
+
expect(expandHomePath("~/docs/plan.md", "/tmp/home")).toBe(
|
|
82
|
+
join("/tmp/home", "docs/plan.md"),
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("does not expand tilde usernames", () => {
|
|
87
|
+
expect(expandHomePath("~alice/docs/plan.md", "/tmp/home")).toBe(
|
|
88
|
+
"~alice/docs/plan.md",
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("isAbsoluteUserPath", () => {
|
|
94
|
+
test("detects Windows drive letter paths", () => {
|
|
95
|
+
expect(isAbsoluteUserPath("C:\\Users\\dev\\test-plan.md", "win32")).toBe(true);
|
|
96
|
+
expect(isAbsoluteUserPath("C:/Users/dev/test-plan.md", "win32")).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("detects converted MSYS paths as absolute on Windows", () => {
|
|
100
|
+
expect(isAbsoluteUserPath("/c/Users/dev/test-plan.md", "win32")).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("resolveUserPath", () => {
|
|
105
|
+
test("resolves relative paths against a base directory", () => {
|
|
106
|
+
expect(resolveUserPath("docs/plan.md", "/tmp/project")).toBe(
|
|
107
|
+
resolve("/tmp/project", "docs/plan.md"),
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("resolves quoted tilde paths", () => {
|
|
112
|
+
expect(resolveUserPath('"~/docs/plan.md"')).toBe(
|
|
113
|
+
resolve(homedir(), "docs/plan.md"),
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("returns empty string for whitespace-only input", () => {
|
|
118
|
+
expect(resolveUserPath(" ", "/tmp/project")).toBe("");
|
|
119
|
+
expect(resolveUserPath("", "/tmp/project")).toBe("");
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// --- Core resolution strategies ---
|
|
124
|
+
|
|
125
|
+
describe("resolveMarkdownFile", () => {
|
|
126
|
+
// Strategy 1: Absolute paths
|
|
127
|
+
|
|
128
|
+
test("resolves absolute path to existing file", async () => {
|
|
129
|
+
const root = createTempProject({ "plan.md": "# Plan" });
|
|
130
|
+
const absPath = resolve(root, "plan.md");
|
|
131
|
+
const result = resolveMarkdownFile(absPath, root);
|
|
132
|
+
expect(result).toEqual({ kind: "found", path: absPath });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("resolves tilde-prefixed absolute paths", async () => {
|
|
136
|
+
const homeRoot = createTempProject({}, join(homedir(), ".plannotator-resolve-file-"));
|
|
137
|
+
const absPath = resolve(homeRoot, "plan.md");
|
|
138
|
+
writeFileSync(absPath, "# Plan");
|
|
139
|
+
const relativeToHome = absPath.slice(homedir().length + 1).replace(/\\/g, "/");
|
|
140
|
+
const result = resolveMarkdownFile(`~/${relativeToHome}`, "/unused");
|
|
141
|
+
expect(result).toEqual({ kind: "found", path: absPath });
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("returns not_found for absolute path that doesn't exist", async () => {
|
|
145
|
+
const root = createTempProject();
|
|
146
|
+
const result = resolveMarkdownFile("/nonexistent/path.md", root);
|
|
147
|
+
expect(result.kind).toBe("not_found");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Strategy 2: Exact relative paths
|
|
151
|
+
|
|
152
|
+
test("resolves exact relative path", async () => {
|
|
153
|
+
const root = createTempProject({ "docs/guide.md": "# Guide" });
|
|
154
|
+
const result = resolveMarkdownFile("docs/guide.md", root);
|
|
155
|
+
expect(result).toEqual({
|
|
156
|
+
kind: "found",
|
|
157
|
+
path: resolve(root, "docs/guide.md"),
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("resolves bare filename in root", async () => {
|
|
162
|
+
const root = createTempProject({ "README.md": "# Hello" });
|
|
163
|
+
const result = resolveMarkdownFile("README.md", root);
|
|
164
|
+
expect(result).toEqual({
|
|
165
|
+
kind: "found",
|
|
166
|
+
path: resolve(root, "README.md"),
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("resolves @ filename via fallback when @ file does not exist", async () => {
|
|
171
|
+
const root = createTempProject({ "README.md": "# Hello" });
|
|
172
|
+
const result = resolveMarkdownFile("@README.md", root);
|
|
173
|
+
expect(result).toEqual({
|
|
174
|
+
kind: "found",
|
|
175
|
+
path: resolve(root, "README.md"),
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("prioritizes real @ filename before fallback", async () => {
|
|
180
|
+
const root = createTempProject({ "@README.md": "# At" });
|
|
181
|
+
const result = resolveMarkdownFile("@README.md", root);
|
|
182
|
+
expect(result).toEqual({
|
|
183
|
+
kind: "found",
|
|
184
|
+
path: resolve(root, "@README.md"),
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("resolves quoted @ filename", async () => {
|
|
189
|
+
const root = createTempProject({ "README.md": "# Hello" });
|
|
190
|
+
const result = resolveMarkdownFile('"@README.md"', root);
|
|
191
|
+
expect(result).toEqual({
|
|
192
|
+
kind: "found",
|
|
193
|
+
path: resolve(root, "README.md"),
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("resolves relative paths with Windows separators", async () => {
|
|
198
|
+
const root = createTempProject({ "docs/test-plan.md": "# Test plan\n" });
|
|
199
|
+
const result = resolveMarkdownFile("docs\\test-plan.md", root);
|
|
200
|
+
expect(result).toEqual({
|
|
201
|
+
kind: "found",
|
|
202
|
+
path: resolve(root, "docs/test-plan.md"),
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Strategy 3: Case-insensitive search
|
|
207
|
+
|
|
208
|
+
test("finds bare filenames case-insensitively", async () => {
|
|
209
|
+
const root = createTempProject({ "notes/Architecture.MD": "# Architecture\n" });
|
|
210
|
+
const result = resolveMarkdownFile("architecture.md", root);
|
|
211
|
+
expect(result).toEqual({
|
|
212
|
+
kind: "found",
|
|
213
|
+
path: resolve(root, "notes/Architecture.MD"),
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("finds relative paths case-insensitively", async () => {
|
|
218
|
+
const root = createTempProject({ "Docs/Specs/Design.MDX": "# Design\n" });
|
|
219
|
+
const result = resolveMarkdownFile("docs/specs/design.mdx", root);
|
|
220
|
+
expect(result.kind).toBe("found");
|
|
221
|
+
if (result.kind === "found") {
|
|
222
|
+
expect(await Bun.file(result.path).text()).toBe("# Design\n");
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("returns ambiguous when bare filename matches multiple files", async () => {
|
|
227
|
+
const root = createTempProject({
|
|
228
|
+
"docs/plan.md": "# Plan 1",
|
|
229
|
+
"api/plan.md": "# Plan 2",
|
|
230
|
+
});
|
|
231
|
+
const result = resolveMarkdownFile("plan.md", root);
|
|
232
|
+
expect(result.kind).toBe("ambiguous");
|
|
233
|
+
if (result.kind === "ambiguous") {
|
|
234
|
+
expect(result.matches).toHaveLength(2);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("returns ambiguous in @ fallback when target exists multiple times", async () => {
|
|
239
|
+
const root = createTempProject({
|
|
240
|
+
"docs/plan.md": "# Plan 1",
|
|
241
|
+
"api/plan.md": "# Plan 2",
|
|
242
|
+
});
|
|
243
|
+
const result = resolveMarkdownFile("@plan.md", root);
|
|
244
|
+
expect(result.kind).toBe("ambiguous");
|
|
245
|
+
if (result.kind === "ambiguous") {
|
|
246
|
+
expect(result.input).toBe("@plan.md");
|
|
247
|
+
expect(result.matches).toHaveLength(2);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Ignored directories
|
|
252
|
+
|
|
253
|
+
test("skips node_modules", async () => {
|
|
254
|
+
const root = createTempProject({
|
|
255
|
+
"node_modules/pkg/README.md": "# Pkg",
|
|
256
|
+
});
|
|
257
|
+
const result = resolveMarkdownFile("readme.md", root);
|
|
258
|
+
expect(result.kind).toBe("not_found");
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("skips .git directory", async () => {
|
|
262
|
+
const root = createTempProject({
|
|
263
|
+
".git/hooks/pre-commit.md": "# hook",
|
|
264
|
+
});
|
|
265
|
+
const result = resolveMarkdownFile("pre-commit.md", root);
|
|
266
|
+
expect(result.kind).toBe("not_found");
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Extension filtering
|
|
270
|
+
|
|
271
|
+
test("rejects non-markdown files", async () => {
|
|
272
|
+
const root = createTempProject({ "script.ts": "export {}" });
|
|
273
|
+
const result = resolveMarkdownFile("script.ts", root);
|
|
274
|
+
expect(result.kind).toBe("not_found");
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("accepts .mdx files", async () => {
|
|
278
|
+
const root = createTempProject({ "page.mdx": "# Page" });
|
|
279
|
+
const result = resolveMarkdownFile("page.mdx", root);
|
|
280
|
+
expect(result).toEqual({
|
|
281
|
+
kind: "found",
|
|
282
|
+
path: resolve(root, "page.mdx"),
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// Edge cases
|
|
287
|
+
|
|
288
|
+
test("returns not_found for nonexistent file", async () => {
|
|
289
|
+
const root = createTempProject();
|
|
290
|
+
const result = resolveMarkdownFile("nope.md", root);
|
|
291
|
+
expect(result.kind).toBe("not_found");
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("returns not_found for @ path that cannot be resolved", async () => {
|
|
295
|
+
const root = createTempProject();
|
|
296
|
+
const result = resolveMarkdownFile("@nope.md", root);
|
|
297
|
+
expect(result).toEqual({ kind: "not_found", input: "@nope.md" });
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("handles deeply nested files", async () => {
|
|
301
|
+
const root = createTempProject({
|
|
302
|
+
"a/b/c/d/deep.md": "# Deep",
|
|
303
|
+
});
|
|
304
|
+
const result = resolveMarkdownFile("deep.md", root);
|
|
305
|
+
expect(result).toEqual({
|
|
306
|
+
kind: "found",
|
|
307
|
+
path: resolve(root, "a/b/c/d/deep.md"),
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
});
|