@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,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git utilities for code review
|
|
3
|
+
*
|
|
4
|
+
* Centralized git operations for diff collection and branch detection.
|
|
5
|
+
* Used by both Claude Code hook and OpenCode plugin.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type DiffOption,
|
|
10
|
+
type DiffResult,
|
|
11
|
+
type DiffType,
|
|
12
|
+
type GitCommandResult,
|
|
13
|
+
type GitContext,
|
|
14
|
+
type GitDiffOptions,
|
|
15
|
+
type ReviewGitRuntime,
|
|
16
|
+
type WorktreeInfo,
|
|
17
|
+
getCurrentBranch as getCurrentBranchCore,
|
|
18
|
+
getDefaultBranch as getDefaultBranchCore,
|
|
19
|
+
getWorktrees as getWorktreesCore,
|
|
20
|
+
getGitContext as getGitContextCore,
|
|
21
|
+
getFileContentsForDiff as getFileContentsForDiffCore,
|
|
22
|
+
gitAddFile as gitAddFileCore,
|
|
23
|
+
gitResetFile as gitResetFileCore,
|
|
24
|
+
parseWorktreeDiffType,
|
|
25
|
+
runGitDiff as runGitDiffCore,
|
|
26
|
+
runGitDiffWithContext as runGitDiffWithContextCore,
|
|
27
|
+
validateFilePath,
|
|
28
|
+
} from "@plannotator/shared/review-core";
|
|
29
|
+
|
|
30
|
+
export type {
|
|
31
|
+
DiffOption,
|
|
32
|
+
DiffType,
|
|
33
|
+
DiffResult,
|
|
34
|
+
GitContext,
|
|
35
|
+
GitDiffOptions,
|
|
36
|
+
WorktreeInfo,
|
|
37
|
+
} from "@plannotator/shared/review-core";
|
|
38
|
+
|
|
39
|
+
async function runGit(
|
|
40
|
+
args: string[],
|
|
41
|
+
options?: { cwd?: string; timeoutMs?: number },
|
|
42
|
+
): Promise<GitCommandResult> {
|
|
43
|
+
const proc = Bun.spawn(["git", "-c", "core.quotePath=false", ...args], {
|
|
44
|
+
cwd: options?.cwd,
|
|
45
|
+
stdout: "pipe",
|
|
46
|
+
stderr: "pipe",
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
50
|
+
if (options?.timeoutMs) {
|
|
51
|
+
timer = setTimeout(() => proc.kill(), options.timeoutMs);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
55
|
+
new Response(proc.stdout).text(),
|
|
56
|
+
new Response(proc.stderr).text(),
|
|
57
|
+
proc.exited,
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
if (timer) clearTimeout(timer);
|
|
61
|
+
|
|
62
|
+
return { stdout, stderr, exitCode };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Bun-based git runtime. Exported for use with shared utilities (worktree, etc.) */
|
|
66
|
+
export const runtime: ReviewGitRuntime = {
|
|
67
|
+
runGit,
|
|
68
|
+
async readTextFile(path: string): Promise<string | null> {
|
|
69
|
+
try {
|
|
70
|
+
return await Bun.file(path).text();
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export function getCurrentBranch(): Promise<string> {
|
|
78
|
+
return getCurrentBranchCore(runtime);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function getDefaultBranch(): Promise<string> {
|
|
82
|
+
return getDefaultBranchCore(runtime);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function getWorktrees(): Promise<WorktreeInfo[]> {
|
|
86
|
+
return getWorktreesCore(runtime);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function getGitContext(cwd?: string): Promise<GitContext> {
|
|
90
|
+
return getGitContextCore(runtime, cwd);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function runGitDiff(
|
|
94
|
+
diffType: DiffType,
|
|
95
|
+
defaultBranch: string = "main",
|
|
96
|
+
cwd?: string,
|
|
97
|
+
options?: GitDiffOptions,
|
|
98
|
+
): Promise<DiffResult> {
|
|
99
|
+
return runGitDiffCore(runtime, diffType, defaultBranch, cwd, options);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function runGitDiffWithContext(
|
|
103
|
+
diffType: DiffType,
|
|
104
|
+
gitContext: GitContext,
|
|
105
|
+
options?: GitDiffOptions,
|
|
106
|
+
): Promise<DiffResult> {
|
|
107
|
+
return runGitDiffWithContextCore(runtime, diffType, gitContext, options);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function getFileContentsForDiff(
|
|
111
|
+
diffType: DiffType,
|
|
112
|
+
defaultBranch: string,
|
|
113
|
+
filePath: string,
|
|
114
|
+
oldPath?: string,
|
|
115
|
+
cwd?: string,
|
|
116
|
+
): Promise<{ oldContent: string | null; newContent: string | null }> {
|
|
117
|
+
return getFileContentsForDiffCore(
|
|
118
|
+
runtime,
|
|
119
|
+
diffType,
|
|
120
|
+
defaultBranch,
|
|
121
|
+
filePath,
|
|
122
|
+
oldPath,
|
|
123
|
+
cwd,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function gitAddFile(
|
|
128
|
+
filePath: string,
|
|
129
|
+
cwd?: string,
|
|
130
|
+
): Promise<void> {
|
|
131
|
+
return gitAddFileCore(runtime, filePath, cwd);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function gitResetFile(
|
|
135
|
+
filePath: string,
|
|
136
|
+
cwd?: string,
|
|
137
|
+
): Promise<void> {
|
|
138
|
+
return gitResetFileCore(runtime, filePath, cwd);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export { parseWorktreeDiffType, validateFilePath };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { normalizeGoalSetupBundle } from "@plannotator/shared/goal-setup";
|
|
3
|
+
import { startGoalSetupServer, type GoalSetupServerResult } from "./goal-setup";
|
|
4
|
+
|
|
5
|
+
let server: GoalSetupServerResult | null = null;
|
|
6
|
+
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
server?.stop();
|
|
9
|
+
server = null;
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe("goal setup server", () => {
|
|
13
|
+
test("serves interview bundle and resolves submitted answers", async () => {
|
|
14
|
+
const bundle = normalizeGoalSetupBundle({
|
|
15
|
+
stage: "interview",
|
|
16
|
+
title: "Goal setup",
|
|
17
|
+
questions: [{ id: "scope", prompt: "Scope?" }],
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
server = await startGoalSetupServer({
|
|
21
|
+
bundle,
|
|
22
|
+
htmlContent: "<html></html>",
|
|
23
|
+
origin: "claude-code",
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const plan = await fetch(`${server.url}/api/goal-setup`).then((res) =>
|
|
27
|
+
res.json()
|
|
28
|
+
);
|
|
29
|
+
expect(plan.mode).toBe("goal-setup");
|
|
30
|
+
expect(plan.goalSetup.questions[0].id).toBe("scope");
|
|
31
|
+
|
|
32
|
+
const decision = server.waitForDecision();
|
|
33
|
+
const submitted = await fetch(`${server.url}/api/goal-setup/submit`, {
|
|
34
|
+
method: "POST",
|
|
35
|
+
headers: { "Content-Type": "application/json" },
|
|
36
|
+
body: JSON.stringify({
|
|
37
|
+
answers: [
|
|
38
|
+
{
|
|
39
|
+
questionId: "scope",
|
|
40
|
+
selectedOptionIds: [],
|
|
41
|
+
customAnswer: "",
|
|
42
|
+
answer: "UI, server, and skill text.",
|
|
43
|
+
completed: true,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
}),
|
|
47
|
+
}).then((res) => res.json());
|
|
48
|
+
|
|
49
|
+
expect(submitted.ok).toBe(true);
|
|
50
|
+
const result = await decision;
|
|
51
|
+
expect(result.result?.stage).toBe("interview");
|
|
52
|
+
if (result.result?.stage !== "interview") throw new Error("expected interview");
|
|
53
|
+
expect(result.result.answers[0].answer).toBe("UI, server, and skill text.");
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Goal Setup Server
|
|
3
|
+
*
|
|
4
|
+
* Serves the Plannotator shell in a goal-setup mode for the setup-goal skill.
|
|
5
|
+
* The interview and facts phases use the same endpoint surface so agents can
|
|
6
|
+
* launch a browser session, wait, and receive a structured JSON result.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Origin } from "@plannotator/shared/agents";
|
|
10
|
+
import {
|
|
11
|
+
createFactsResult,
|
|
12
|
+
createInterviewResult,
|
|
13
|
+
type GoalSetupBundle,
|
|
14
|
+
type GoalSetupFactResult,
|
|
15
|
+
type GoalSetupQuestionAnswer,
|
|
16
|
+
type GoalSetupResult,
|
|
17
|
+
} from "@plannotator/shared/goal-setup";
|
|
18
|
+
import { isRemoteSession, getServerHostname, getServerPort } from "./remote";
|
|
19
|
+
import { getRepoInfo } from "./repo";
|
|
20
|
+
import {
|
|
21
|
+
handleFavicon,
|
|
22
|
+
handleImage,
|
|
23
|
+
handleServerReady,
|
|
24
|
+
handleUpload,
|
|
25
|
+
} from "./shared-handlers";
|
|
26
|
+
import { detectGitUser, getServerConfig, saveConfig } from "./config";
|
|
27
|
+
import { isWSL } from "./browser";
|
|
28
|
+
|
|
29
|
+
export { handleServerReady as handleGoalSetupServerReady } from "./shared-handlers";
|
|
30
|
+
|
|
31
|
+
export interface GoalSetupServerOptions {
|
|
32
|
+
bundle: GoalSetupBundle;
|
|
33
|
+
htmlContent: string;
|
|
34
|
+
origin?: Origin;
|
|
35
|
+
onReady?: (url: string, isRemote: boolean, port: number) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface GoalSetupServerResult {
|
|
39
|
+
port: number;
|
|
40
|
+
url: string;
|
|
41
|
+
isRemote: boolean;
|
|
42
|
+
waitForDecision: () => Promise<{
|
|
43
|
+
result?: GoalSetupResult;
|
|
44
|
+
exit?: boolean;
|
|
45
|
+
}>;
|
|
46
|
+
stop: () => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const MAX_RETRIES = 5;
|
|
50
|
+
const RETRY_DELAY_MS = 500;
|
|
51
|
+
|
|
52
|
+
function coerceAnswers(body: unknown): GoalSetupQuestionAnswer[] {
|
|
53
|
+
if (!body || typeof body !== "object") return [];
|
|
54
|
+
const record = body as Record<string, unknown>;
|
|
55
|
+
const answers = Array.isArray(record.answers)
|
|
56
|
+
? record.answers
|
|
57
|
+
: record.result &&
|
|
58
|
+
typeof record.result === "object" &&
|
|
59
|
+
Array.isArray((record.result as Record<string, unknown>).answers)
|
|
60
|
+
? ((record.result as Record<string, unknown>).answers as unknown[])
|
|
61
|
+
: [];
|
|
62
|
+
return answers as GoalSetupQuestionAnswer[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function coerceFacts(body: unknown): GoalSetupFactResult[] {
|
|
66
|
+
if (!body || typeof body !== "object") return [];
|
|
67
|
+
const record = body as Record<string, unknown>;
|
|
68
|
+
const facts = Array.isArray(record.facts)
|
|
69
|
+
? record.facts
|
|
70
|
+
: record.result &&
|
|
71
|
+
typeof record.result === "object" &&
|
|
72
|
+
Array.isArray((record.result as Record<string, unknown>).facts)
|
|
73
|
+
? ((record.result as Record<string, unknown>).facts as unknown[])
|
|
74
|
+
: [];
|
|
75
|
+
return facts as GoalSetupFactResult[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function startGoalSetupServer(
|
|
79
|
+
options: GoalSetupServerOptions
|
|
80
|
+
): Promise<GoalSetupServerResult> {
|
|
81
|
+
const { bundle, htmlContent, origin = "claude-code", onReady } = options;
|
|
82
|
+
const isRemote = isRemoteSession();
|
|
83
|
+
const configuredPort = getServerPort();
|
|
84
|
+
const wslFlag = await isWSL();
|
|
85
|
+
const repoInfo = await getRepoInfo();
|
|
86
|
+
const gitUser = detectGitUser();
|
|
87
|
+
|
|
88
|
+
let settled = false;
|
|
89
|
+
let resolveDecision: (result: {
|
|
90
|
+
result?: GoalSetupResult;
|
|
91
|
+
exit?: boolean;
|
|
92
|
+
}) => void;
|
|
93
|
+
const decisionPromise = new Promise<{
|
|
94
|
+
result?: GoalSetupResult;
|
|
95
|
+
exit?: boolean;
|
|
96
|
+
}>((resolve) => {
|
|
97
|
+
resolveDecision = resolve;
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const resolveOnce = (result: { result?: GoalSetupResult; exit?: boolean }) => {
|
|
101
|
+
if (settled) return;
|
|
102
|
+
settled = true;
|
|
103
|
+
resolveDecision(result);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
let server: ReturnType<typeof Bun.serve> | null = null;
|
|
107
|
+
|
|
108
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
109
|
+
try {
|
|
110
|
+
server = Bun.serve({
|
|
111
|
+
hostname: getServerHostname(),
|
|
112
|
+
port: configuredPort,
|
|
113
|
+
|
|
114
|
+
async fetch(req) {
|
|
115
|
+
const url = new URL(req.url);
|
|
116
|
+
|
|
117
|
+
if (
|
|
118
|
+
(url.pathname === "/api/plan" ||
|
|
119
|
+
url.pathname === "/api/goal-setup") &&
|
|
120
|
+
req.method === "GET"
|
|
121
|
+
) {
|
|
122
|
+
return Response.json({
|
|
123
|
+
plan: "",
|
|
124
|
+
origin,
|
|
125
|
+
mode: "goal-setup",
|
|
126
|
+
goalSetup: bundle,
|
|
127
|
+
repoInfo,
|
|
128
|
+
projectRoot: process.cwd(),
|
|
129
|
+
isWSL: wslFlag,
|
|
130
|
+
serverConfig: getServerConfig(gitUser),
|
|
131
|
+
sharingEnabled: false,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (url.pathname === "/api/config" && req.method === "POST") {
|
|
136
|
+
try {
|
|
137
|
+
const body = (await req.json()) as {
|
|
138
|
+
displayName?: string;
|
|
139
|
+
diffOptions?: Record<string, unknown>;
|
|
140
|
+
conventionalComments?: boolean;
|
|
141
|
+
conventionalLabels?: unknown[] | null;
|
|
142
|
+
};
|
|
143
|
+
const toSave: Record<string, unknown> = {};
|
|
144
|
+
if (body.displayName !== undefined) {
|
|
145
|
+
toSave.displayName = body.displayName;
|
|
146
|
+
}
|
|
147
|
+
if (body.diffOptions !== undefined) {
|
|
148
|
+
toSave.diffOptions = body.diffOptions;
|
|
149
|
+
}
|
|
150
|
+
if (body.conventionalComments !== undefined) {
|
|
151
|
+
toSave.conventionalComments = body.conventionalComments;
|
|
152
|
+
}
|
|
153
|
+
if (body.conventionalLabels !== undefined) {
|
|
154
|
+
toSave.conventionalLabels = body.conventionalLabels;
|
|
155
|
+
}
|
|
156
|
+
if (Object.keys(toSave).length > 0) {
|
|
157
|
+
saveConfig(toSave as Parameters<typeof saveConfig>[0]);
|
|
158
|
+
}
|
|
159
|
+
return Response.json({ ok: true });
|
|
160
|
+
} catch {
|
|
161
|
+
return Response.json({ error: "Invalid request" }, { status: 400 });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (url.pathname === "/api/image") return handleImage(req);
|
|
166
|
+
if (url.pathname === "/api/upload" && req.method === "POST") {
|
|
167
|
+
return handleUpload(req);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (
|
|
171
|
+
url.pathname === "/api/goal-setup/submit" &&
|
|
172
|
+
req.method === "POST"
|
|
173
|
+
) {
|
|
174
|
+
try {
|
|
175
|
+
const body = await req.json();
|
|
176
|
+
const result =
|
|
177
|
+
bundle.stage === "interview"
|
|
178
|
+
? createInterviewResult(bundle, coerceAnswers(body))
|
|
179
|
+
: createFactsResult(bundle, coerceFacts(body));
|
|
180
|
+
resolveOnce({ result });
|
|
181
|
+
return Response.json({ ok: true, result });
|
|
182
|
+
} catch (err) {
|
|
183
|
+
const message =
|
|
184
|
+
err instanceof Error ? err.message : "Failed to submit result";
|
|
185
|
+
return Response.json({ error: message }, { status: 400 });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (url.pathname === "/api/exit" && req.method === "POST") {
|
|
190
|
+
resolveOnce({ exit: true });
|
|
191
|
+
return Response.json({ ok: true });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (url.pathname === "/favicon.svg") return handleFavicon();
|
|
195
|
+
|
|
196
|
+
return new Response(htmlContent, {
|
|
197
|
+
headers: { "Content-Type": "text/html" },
|
|
198
|
+
});
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
error(err) {
|
|
202
|
+
console.error("[plannotator] Goal setup server error:", err);
|
|
203
|
+
return new Response(
|
|
204
|
+
`Internal Server Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
205
|
+
{ status: 500, headers: { "Content-Type": "text/plain" } }
|
|
206
|
+
);
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
break;
|
|
211
|
+
} catch (err: unknown) {
|
|
212
|
+
const isAddressInUse =
|
|
213
|
+
err instanceof Error && err.message.includes("EADDRINUSE");
|
|
214
|
+
|
|
215
|
+
if (isAddressInUse && attempt < MAX_RETRIES) {
|
|
216
|
+
await Bun.sleep(RETRY_DELAY_MS);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (isAddressInUse) {
|
|
221
|
+
const hint = isRemote
|
|
222
|
+
? " (set PLANNOTATOR_PORT to use different port)"
|
|
223
|
+
: "";
|
|
224
|
+
throw new Error(
|
|
225
|
+
`Port ${configuredPort} in use after ${MAX_RETRIES} retries${hint}`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
throw err;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!server) {
|
|
234
|
+
throw new Error("Failed to start goal setup server");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const port = server.port!;
|
|
238
|
+
const serverUrl = `http://localhost:${port}`;
|
|
239
|
+
onReady?.(serverUrl, isRemote, port);
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
port,
|
|
243
|
+
url: serverUrl,
|
|
244
|
+
isRemote,
|
|
245
|
+
waitForDecision: () => decisionPromise,
|
|
246
|
+
stop: () => server.stop(),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IDE integration — opens plan diffs in external IDEs
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Open two files in VS Code's diff viewer.
|
|
7
|
+
*
|
|
8
|
+
* Returns `{ ok: true }` on success or `{ error: string }` on failure.
|
|
9
|
+
*/
|
|
10
|
+
export async function openEditorDiff(
|
|
11
|
+
oldPath: string,
|
|
12
|
+
newPath: string
|
|
13
|
+
): Promise<{ ok: true } | { error: string }> {
|
|
14
|
+
try {
|
|
15
|
+
const proc = Bun.spawn(["code", "--diff", oldPath, newPath], {
|
|
16
|
+
stdout: "ignore",
|
|
17
|
+
stderr: "pipe",
|
|
18
|
+
});
|
|
19
|
+
const exitCode = await proc.exited;
|
|
20
|
+
|
|
21
|
+
if (exitCode !== 0) {
|
|
22
|
+
const stderr = await new Response(proc.stderr).text();
|
|
23
|
+
if (stderr.includes("not found") || stderr.includes("ENOENT")) {
|
|
24
|
+
return {
|
|
25
|
+
error:
|
|
26
|
+
"VS Code CLI not found. Run 'Shell Command: Install code command in PATH' from the VS Code command palette.",
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return { error: `code --diff exited with ${exitCode}: ${stderr}` };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return { ok: true };
|
|
33
|
+
} catch (err) {
|
|
34
|
+
const msg = err instanceof Error ? err.message : "Failed to open diff";
|
|
35
|
+
if (msg.includes("ENOENT") || msg.includes("not found")) {
|
|
36
|
+
return {
|
|
37
|
+
error:
|
|
38
|
+
"VS Code CLI not found. Run 'Shell Command: Install code command in PATH' from the VS Code command palette.",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return { error: msg };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Validation Tests
|
|
3
|
+
*
|
|
4
|
+
* Run: bun test packages/server/image.test.ts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from "bun:test";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { validateImagePath, validateUploadExtension, UPLOAD_DIR } from "./image";
|
|
10
|
+
|
|
11
|
+
describe("UPLOAD_DIR", () => {
|
|
12
|
+
test("uses os.tmpdir(), not hardcoded /tmp", () => {
|
|
13
|
+
// On macOS tmpdir() returns something like /var/folders/...
|
|
14
|
+
// On Linux it returns /tmp
|
|
15
|
+
// On Windows it returns C:\Users\...\AppData\Local\Temp
|
|
16
|
+
// The key thing: it should NOT be hardcoded to /tmp/plannotator
|
|
17
|
+
expect(UPLOAD_DIR).toContain("plannotator");
|
|
18
|
+
expect(UPLOAD_DIR.startsWith(tmpdir())).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("validateImagePath", () => {
|
|
23
|
+
test("accepts supported extensions", () => {
|
|
24
|
+
for (const ext of ["png", "jpg", "jpeg", "gif", "webp", "svg", "avif"]) {
|
|
25
|
+
const result = validateImagePath(`/tmp/image.${ext}`);
|
|
26
|
+
expect(result.valid).toBe(true);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("rejects unsupported extensions", () => {
|
|
31
|
+
expect(validateImagePath("/tmp/file.txt").valid).toBe(false);
|
|
32
|
+
expect(validateImagePath("/tmp/script.js").valid).toBe(false);
|
|
33
|
+
expect(validateImagePath("/tmp/page.html").valid).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("rejects files with no extension", () => {
|
|
37
|
+
expect(validateImagePath("/tmp/noextension").valid).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("resolves path", () => {
|
|
41
|
+
const result = validateImagePath("relative/image.png");
|
|
42
|
+
expect(result.resolved).toMatch(/^\//); // absolute on POSIX
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("validateUploadExtension", () => {
|
|
47
|
+
test("accepts supported extensions", () => {
|
|
48
|
+
expect(validateUploadExtension("photo.png").valid).toBe(true);
|
|
49
|
+
expect(validateUploadExtension("photo.jpg").valid).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("rejects unsupported extensions", () => {
|
|
53
|
+
const result = validateUploadExtension("file.exe");
|
|
54
|
+
expect(result.valid).toBe(false);
|
|
55
|
+
expect(result.error).toBeDefined();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("defaults to png when no extension", () => {
|
|
59
|
+
const result = validateUploadExtension("noext");
|
|
60
|
+
expect(result.valid).toBe(true);
|
|
61
|
+
expect(result.ext).toBe("png");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { resolve, join } from "path";
|
|
2
|
+
import { tmpdir } from "os";
|
|
3
|
+
|
|
4
|
+
const ALLOWED_IMAGE_EXTENSIONS = new Set([
|
|
5
|
+
"png",
|
|
6
|
+
"jpg",
|
|
7
|
+
"jpeg",
|
|
8
|
+
"gif",
|
|
9
|
+
"webp",
|
|
10
|
+
"svg",
|
|
11
|
+
"bmp",
|
|
12
|
+
"ico",
|
|
13
|
+
"tiff",
|
|
14
|
+
"tif",
|
|
15
|
+
"avif",
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
const UPLOAD_DIR = join(tmpdir(), "plannotator");
|
|
19
|
+
|
|
20
|
+
function getExtension(filePath: string): string {
|
|
21
|
+
const lastDot = filePath.lastIndexOf(".");
|
|
22
|
+
if (lastDot === -1) return "";
|
|
23
|
+
return filePath.slice(lastDot + 1).toLowerCase();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function hasImageExtension(filePath: string): boolean {
|
|
27
|
+
return ALLOWED_IMAGE_EXTENSIONS.has(getExtension(filePath));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function validateImagePath(rawPath: string): {
|
|
31
|
+
valid: boolean;
|
|
32
|
+
resolved: string;
|
|
33
|
+
error?: string;
|
|
34
|
+
} {
|
|
35
|
+
const resolved = resolve(rawPath);
|
|
36
|
+
|
|
37
|
+
if (!hasImageExtension(resolved)) {
|
|
38
|
+
return {
|
|
39
|
+
valid: false,
|
|
40
|
+
resolved,
|
|
41
|
+
error: "Path does not point to a supported image file",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { valid: true, resolved };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function validateUploadExtension(fileName: string): {
|
|
49
|
+
valid: boolean;
|
|
50
|
+
ext: string;
|
|
51
|
+
error?: string;
|
|
52
|
+
} {
|
|
53
|
+
const ext = getExtension(fileName) || "png";
|
|
54
|
+
|
|
55
|
+
if (!ALLOWED_IMAGE_EXTENSIONS.has(ext)) {
|
|
56
|
+
return {
|
|
57
|
+
valid: false,
|
|
58
|
+
ext,
|
|
59
|
+
error: `File extension ".${ext}" is not a supported image type`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { valid: true, ext };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export { UPLOAD_DIR };
|