@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,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Registry
|
|
3
|
+
*
|
|
4
|
+
* Tracks active Plannotator server sessions in ~/.plannotator/sessions/
|
|
5
|
+
* so users can discover and reopen closed browser tabs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import {
|
|
10
|
+
mkdirSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
readFileSync,
|
|
13
|
+
readdirSync,
|
|
14
|
+
unlinkSync,
|
|
15
|
+
existsSync,
|
|
16
|
+
} from "fs";
|
|
17
|
+
import { getPlannotatorDataDir } from "@plannotator/shared/data-dir";
|
|
18
|
+
|
|
19
|
+
export interface SessionInfo {
|
|
20
|
+
pid: number;
|
|
21
|
+
port: number;
|
|
22
|
+
url: string;
|
|
23
|
+
mode: "plan" | "review" | "annotate" | "archive" | "goal-setup";
|
|
24
|
+
project: string;
|
|
25
|
+
startedAt: string;
|
|
26
|
+
label: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getSessionsDir(): string {
|
|
30
|
+
const dir = join(getPlannotatorDataDir(), "sessions");
|
|
31
|
+
mkdirSync(dir, { recursive: true });
|
|
32
|
+
return dir;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function sessionPath(pid: number): string {
|
|
36
|
+
return join(getSessionsDir(), `${pid}.json`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if a process is still alive.
|
|
41
|
+
*/
|
|
42
|
+
function isAlive(pid: number): boolean {
|
|
43
|
+
try {
|
|
44
|
+
process.kill(pid, 0);
|
|
45
|
+
return true;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Register the current server session.
|
|
53
|
+
*/
|
|
54
|
+
export function registerSession(info: SessionInfo): void {
|
|
55
|
+
writeFileSync(sessionPath(info.pid), JSON.stringify(info, null, 2), "utf-8");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Unregister the current process's session. No-op if not found.
|
|
60
|
+
*/
|
|
61
|
+
export function unregisterSession(pid: number = process.pid): void {
|
|
62
|
+
const filePath = sessionPath(pid);
|
|
63
|
+
try {
|
|
64
|
+
if (existsSync(filePath)) unlinkSync(filePath);
|
|
65
|
+
} catch {
|
|
66
|
+
// Ignore delete failures
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* List all active sessions. Automatically removes stale entries.
|
|
72
|
+
*/
|
|
73
|
+
export function listSessions(): SessionInfo[] {
|
|
74
|
+
const dir = getSessionsDir();
|
|
75
|
+
const active: SessionInfo[] = [];
|
|
76
|
+
|
|
77
|
+
let entries: string[];
|
|
78
|
+
try {
|
|
79
|
+
entries = readdirSync(dir);
|
|
80
|
+
} catch {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const entry of entries) {
|
|
85
|
+
if (!entry.endsWith(".json")) continue;
|
|
86
|
+
|
|
87
|
+
const filePath = join(dir, entry);
|
|
88
|
+
try {
|
|
89
|
+
const data: SessionInfo = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
90
|
+
|
|
91
|
+
if (isAlive(data.pid)) {
|
|
92
|
+
active.push(data);
|
|
93
|
+
} else {
|
|
94
|
+
// Stale session — clean up
|
|
95
|
+
try {
|
|
96
|
+
unlinkSync(filePath);
|
|
97
|
+
} catch {}
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
// Corrupt file — remove it
|
|
101
|
+
try {
|
|
102
|
+
unlinkSync(filePath);
|
|
103
|
+
} catch {}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Sort by most recent first
|
|
108
|
+
return active.sort(
|
|
109
|
+
(a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime()
|
|
110
|
+
);
|
|
111
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side share URL generation for remote sessions
|
|
3
|
+
*
|
|
4
|
+
* Generates a share.plannotator.ai URL from plan content so remote users
|
|
5
|
+
* can open the review in their local browser without port forwarding.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { compress } from "@plannotator/shared/compress";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_SHARE_BASE = "https://share.plannotator.ai";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generate a share URL from plan markdown content.
|
|
14
|
+
*
|
|
15
|
+
* Returns the full hash-based URL. For remote sessions, this lets the
|
|
16
|
+
* user open the plan in their local browser without any backend needed.
|
|
17
|
+
*/
|
|
18
|
+
export async function generateRemoteShareUrl(
|
|
19
|
+
plan: string,
|
|
20
|
+
shareBaseUrl?: string
|
|
21
|
+
): Promise<string> {
|
|
22
|
+
const base = shareBaseUrl || DEFAULT_SHARE_BASE;
|
|
23
|
+
const hash = await compress({ p: plan, a: [] });
|
|
24
|
+
return `${base}/#${hash}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Format byte size as human-readable string
|
|
29
|
+
*/
|
|
30
|
+
export function formatSize(bytes: number): string {
|
|
31
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
32
|
+
const kb = bytes / 1024;
|
|
33
|
+
return kb < 100 ? `${kb.toFixed(1)} KB` : `${Math.round(kb)} KB`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Generate a remote share URL and write it to stderr for the user.
|
|
38
|
+
* Silently does nothing on failure.
|
|
39
|
+
*/
|
|
40
|
+
export async function writeRemoteShareLink(
|
|
41
|
+
content: string,
|
|
42
|
+
shareBaseUrl: string | undefined,
|
|
43
|
+
verb: string,
|
|
44
|
+
noun: string
|
|
45
|
+
): Promise<void> {
|
|
46
|
+
const shareUrl = await generateRemoteShareUrl(content, shareBaseUrl);
|
|
47
|
+
const size = formatSize(new TextEncoder().encode(shareUrl).length);
|
|
48
|
+
process.stderr.write(
|
|
49
|
+
`\n Open this link on your local machine to ${verb}:\n` +
|
|
50
|
+
` ${shareUrl}\n\n` +
|
|
51
|
+
` (${size} — ${noun}, annotations added in browser)\n\n`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { handleServerReady, writeServerReadyMetadata } from "./shared-handlers";
|
|
6
|
+
|
|
7
|
+
describe("writeServerReadyMetadata", () => {
|
|
8
|
+
test("writes host-plugin ready metadata", () => {
|
|
9
|
+
const dir = mkdtempSync(join(tmpdir(), "plannotator-ready-"));
|
|
10
|
+
const readyFile = join(dir, "nested", "ready.jsonl");
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
writeServerReadyMetadata(readyFile, {
|
|
14
|
+
url: "http://localhost:12345",
|
|
15
|
+
isRemote: false,
|
|
16
|
+
port: 12345,
|
|
17
|
+
});
|
|
18
|
+
const [line] = readFileSync(readyFile, "utf8").trim().split(/\r?\n/);
|
|
19
|
+
expect(JSON.parse(line)).toEqual({
|
|
20
|
+
url: "http://localhost:12345",
|
|
21
|
+
isRemote: false,
|
|
22
|
+
port: 12345,
|
|
23
|
+
});
|
|
24
|
+
} finally {
|
|
25
|
+
rmSync(dir, { recursive: true, force: true });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("handleServerReady", () => {
|
|
31
|
+
test("does not open a browser when host-plugin mode handles it", async () => {
|
|
32
|
+
let opened = false;
|
|
33
|
+
|
|
34
|
+
await handleServerReady("http://localhost:12345", false, 12345, {
|
|
35
|
+
skipBrowserOpen: true,
|
|
36
|
+
openBrowser: async () => {
|
|
37
|
+
opened = true;
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
expect(opened).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared route handlers used by plan, review, and annotate servers.
|
|
3
|
+
*
|
|
4
|
+
* Eliminates duplication of /api/image, /api/upload, /api/draft, and the
|
|
5
|
+
* server-ready handler across all three server files. Also shares /api/agents
|
|
6
|
+
* for plan + review.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
10
|
+
import { dirname } from "node:path";
|
|
11
|
+
import { openBrowser as openBrowserImpl } from "./browser";
|
|
12
|
+
import { validateImagePath, validateUploadExtension, UPLOAD_DIR } from "./image";
|
|
13
|
+
import { saveDraft, loadDraft, deleteDraft } from "./draft";
|
|
14
|
+
import { FAVICON_SVG } from "@plannotator/shared/favicon";
|
|
15
|
+
|
|
16
|
+
/** Serve images from local paths or temp uploads. Used by all 3 servers. */
|
|
17
|
+
export async function handleImage(req: Request): Promise<Response> {
|
|
18
|
+
const url = new URL(req.url);
|
|
19
|
+
const imagePath = url.searchParams.get("path");
|
|
20
|
+
if (!imagePath) {
|
|
21
|
+
return new Response("Missing path parameter", { status: 400 });
|
|
22
|
+
}
|
|
23
|
+
const validation = validateImagePath(imagePath);
|
|
24
|
+
if (!validation.valid) {
|
|
25
|
+
return new Response(validation.error!, { status: 403 });
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const file = Bun.file(validation.resolved);
|
|
29
|
+
if (await file.exists()) {
|
|
30
|
+
return new Response(file);
|
|
31
|
+
}
|
|
32
|
+
// If not found and a base directory is provided, try resolving relative to it
|
|
33
|
+
const base = url.searchParams.get("base");
|
|
34
|
+
if (base && !imagePath.startsWith("/")) {
|
|
35
|
+
const { resolve: resolvePath } = await import("path");
|
|
36
|
+
const fromBase = resolvePath(base, imagePath);
|
|
37
|
+
const baseValidation = validateImagePath(fromBase);
|
|
38
|
+
if (baseValidation.valid) {
|
|
39
|
+
const baseFile = Bun.file(baseValidation.resolved);
|
|
40
|
+
if (await baseFile.exists()) {
|
|
41
|
+
return new Response(baseFile);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return new Response("File not found", { status: 404 });
|
|
46
|
+
} catch {
|
|
47
|
+
return new Response("Failed to read file", { status: 500 });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Upload image to temp dir, return path. Used by all 3 servers. */
|
|
52
|
+
export async function handleUpload(req: Request): Promise<Response> {
|
|
53
|
+
try {
|
|
54
|
+
const formData = await req.formData();
|
|
55
|
+
const file = formData.get("file") as File;
|
|
56
|
+
if (!file) {
|
|
57
|
+
return new Response("No file provided", { status: 400 });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const extResult = validateUploadExtension(file.name);
|
|
61
|
+
if (!extResult.valid) {
|
|
62
|
+
return Response.json({ error: extResult.error }, { status: 400 });
|
|
63
|
+
}
|
|
64
|
+
mkdirSync(UPLOAD_DIR, { recursive: true });
|
|
65
|
+
const tempPath = `${UPLOAD_DIR}/${crypto.randomUUID()}.${extResult.ext}`;
|
|
66
|
+
|
|
67
|
+
await Bun.write(tempPath, file);
|
|
68
|
+
return Response.json({ path: tempPath, originalName: file.name });
|
|
69
|
+
} catch (err) {
|
|
70
|
+
const message = err instanceof Error ? err.message : "Upload failed";
|
|
71
|
+
return Response.json({ error: message }, { status: 500 });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** OpenCode agent client interface (subset of OpenCode SDK) */
|
|
76
|
+
export interface OpencodeClient {
|
|
77
|
+
app: {
|
|
78
|
+
agents: (options?: object) => Promise<{
|
|
79
|
+
data?: Array<{ name: string; description?: string; mode: string; hidden?: boolean }>;
|
|
80
|
+
}>;
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** List available agents. Used by plan + review servers (OpenCode only). */
|
|
85
|
+
export async function handleAgents(opencodeClient?: OpencodeClient): Promise<Response> {
|
|
86
|
+
if (!opencodeClient) {
|
|
87
|
+
return Response.json({ agents: [] });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const result = await opencodeClient.app.agents({});
|
|
92
|
+
const agents = (result.data ?? [])
|
|
93
|
+
.filter((a) => a.mode === "primary" && !a.hidden)
|
|
94
|
+
.map((a) => ({ id: a.name, name: a.name, description: a.description }));
|
|
95
|
+
|
|
96
|
+
return Response.json({ agents });
|
|
97
|
+
} catch {
|
|
98
|
+
return Response.json({ agents: [], error: "Failed to fetch agents" });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Save annotation draft. Used by all 3 servers. */
|
|
103
|
+
export async function handleDraftSave(req: Request, contentKey: string): Promise<Response> {
|
|
104
|
+
try {
|
|
105
|
+
const body = await req.json();
|
|
106
|
+
saveDraft(contentKey, body);
|
|
107
|
+
return Response.json({ ok: true });
|
|
108
|
+
} catch (err) {
|
|
109
|
+
const message = err instanceof Error ? err.message : "Failed to save draft";
|
|
110
|
+
console.error(`[draft] save failed: ${message}`);
|
|
111
|
+
return Response.json({ error: message }, { status: 500 });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Load annotation draft. Used by all 3 servers. */
|
|
116
|
+
export function handleDraftLoad(contentKey: string): Response {
|
|
117
|
+
const draft = loadDraft(contentKey);
|
|
118
|
+
if (!draft) {
|
|
119
|
+
return Response.json({ found: false }, { status: 404 });
|
|
120
|
+
}
|
|
121
|
+
return Response.json(draft);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Delete annotation draft. Used by all 3 servers. */
|
|
125
|
+
export function handleDraftDelete(contentKey: string): Response {
|
|
126
|
+
deleteDraft(contentKey);
|
|
127
|
+
return Response.json({ ok: true });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
/** Serve the app favicon. Used by all 3 servers. */
|
|
133
|
+
export function handleFavicon(): Response {
|
|
134
|
+
return new Response(FAVICON_SVG, {
|
|
135
|
+
headers: { "Content-Type": "image/svg+xml", "Cache-Control": "public, max-age=86400" },
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
interface ServerReadyOptions {
|
|
140
|
+
readyFile?: string;
|
|
141
|
+
skipBrowserOpen?: boolean;
|
|
142
|
+
openBrowser?: typeof openBrowserImpl;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface ServerReadyMetadata {
|
|
146
|
+
url: string;
|
|
147
|
+
isRemote: boolean;
|
|
148
|
+
port: number;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function writeServerReadyMetadata(readyFile: string, metadata: ServerReadyMetadata): void {
|
|
152
|
+
mkdirSync(dirname(readyFile), { recursive: true });
|
|
153
|
+
appendFileSync(readyFile, `${JSON.stringify(metadata)}\n`, "utf8");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Attempt to open the browser for the session URL. */
|
|
157
|
+
export async function handleServerReady(
|
|
158
|
+
url: string,
|
|
159
|
+
isRemote: boolean,
|
|
160
|
+
port: number,
|
|
161
|
+
options: ServerReadyOptions = {},
|
|
162
|
+
): Promise<void> {
|
|
163
|
+
const readyFile = options.readyFile ?? process.env.PLANNOTATOR_READY_FILE;
|
|
164
|
+
if (readyFile) {
|
|
165
|
+
try {
|
|
166
|
+
writeServerReadyMetadata(readyFile, { url, isRemote, port });
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (options.readyFile) throw error;
|
|
169
|
+
// Best effort: host plugins use this side channel to open the browser.
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const skipBrowserOpen = options.skipBrowserOpen ?? process.env.PLANNOTATOR_SKIP_BROWSER_OPEN === "1";
|
|
174
|
+
if (skipBrowserOpen) return;
|
|
175
|
+
|
|
176
|
+
await (options.openBrowser ?? openBrowserImpl)(url, { isRemote, useGlimpse: true });
|
|
177
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan Storage Tests
|
|
3
|
+
*
|
|
4
|
+
* Run: bun test packages/server/storage.test.ts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
8
|
+
import { mkdtempSync, rmSync, readFileSync, readdirSync } from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { generateSlug, getPlanDir, savePlan, saveToHistory, getPlanVersion, getVersionCount, listVersions } from "./storage";
|
|
12
|
+
|
|
13
|
+
const tempDirs: string[] = [];
|
|
14
|
+
|
|
15
|
+
function makeTempDir(): string {
|
|
16
|
+
const dir = mkdtempSync(join(tmpdir(), "plannotator-storage-test-"));
|
|
17
|
+
tempDirs.push(dir);
|
|
18
|
+
return dir;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
for (const dir of tempDirs.splice(0)) {
|
|
23
|
+
rmSync(dir, { recursive: true, force: true });
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("generateSlug", () => {
|
|
28
|
+
test("uses first heading and date", () => {
|
|
29
|
+
const slug = generateSlug("# My Plan\n\nSome content");
|
|
30
|
+
const date = new Date().toISOString().split("T")[0];
|
|
31
|
+
expect(slug).toMatch(/^my-plan-\d{4}-\d{2}-\d{2}$/);
|
|
32
|
+
expect(slug).toEndWith(date);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("falls back to 'plan' when no heading", () => {
|
|
36
|
+
const slug = generateSlug("No heading here");
|
|
37
|
+
expect(slug).toMatch(/^plan-\d{4}-\d{2}-\d{2}$/);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("same heading on same day produces same slug", () => {
|
|
41
|
+
const a = generateSlug("# Deploy Strategy\nVersion A");
|
|
42
|
+
const b = generateSlug("# Deploy Strategy\nVersion B");
|
|
43
|
+
expect(a).toBe(b);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("different headings produce different slugs", () => {
|
|
47
|
+
const a = generateSlug("# Plan A");
|
|
48
|
+
const b = generateSlug("# Plan B");
|
|
49
|
+
expect(a).not.toBe(b);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("getPlanDir", () => {
|
|
54
|
+
test("creates directory at custom path", () => {
|
|
55
|
+
const dir = makeTempDir();
|
|
56
|
+
const customPath = join(dir, "custom", "plans");
|
|
57
|
+
const result = getPlanDir(customPath);
|
|
58
|
+
expect(result).toBe(customPath);
|
|
59
|
+
// Directory should exist
|
|
60
|
+
expect(readdirSync(customPath)).toBeDefined();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("expands tilde in custom path", () => {
|
|
64
|
+
const result = getPlanDir("~/.plannotator/test-plans");
|
|
65
|
+
expect(result).not.toContain("~");
|
|
66
|
+
expect(result).toMatch(/\.plannotator\/test-plans$/);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("uses default when no custom path", () => {
|
|
70
|
+
const result = getPlanDir();
|
|
71
|
+
expect(result).toMatch(/plans$/);
|
|
72
|
+
expect(result).toBe(getPlanDir(null));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("uses default for null", () => {
|
|
76
|
+
const result = getPlanDir(null);
|
|
77
|
+
expect(result).toMatch(/plans$/);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("uses default for whitespace-only custom path", () => {
|
|
81
|
+
const result = getPlanDir(" ");
|
|
82
|
+
expect(result).toMatch(/plans$/);
|
|
83
|
+
expect(result).not.toBe(process.cwd());
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("savePlan", () => {
|
|
88
|
+
test("writes markdown file to disk", () => {
|
|
89
|
+
const dir = makeTempDir();
|
|
90
|
+
const path = savePlan("test-slug", "# Content", dir);
|
|
91
|
+
expect(path).toBe(join(dir, "test-slug.md"));
|
|
92
|
+
expect(readFileSync(path, "utf-8")).toBe("# Content");
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("saveToHistory", () => {
|
|
97
|
+
test("creates first version as 001.md", () => {
|
|
98
|
+
const slug = `first-version-${Date.now()}`;
|
|
99
|
+
const result = saveToHistory("test-project", slug, "# V1");
|
|
100
|
+
expect(result.version).toBe(1);
|
|
101
|
+
expect(result.path).toEndWith("001.md");
|
|
102
|
+
expect(result.isNew).toBe(true);
|
|
103
|
+
expect(readFileSync(result.path, "utf-8")).toBe("# V1");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("increments version number", () => {
|
|
107
|
+
const slug = `inc-test-${Date.now()}`;
|
|
108
|
+
const v1 = saveToHistory("test-project", slug, "# V1");
|
|
109
|
+
const v2 = saveToHistory("test-project", slug, "# V2");
|
|
110
|
+
expect(v1.version).toBe(1);
|
|
111
|
+
expect(v2.version).toBe(2);
|
|
112
|
+
expect(v2.path).toEndWith("002.md");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("deduplicates identical content", () => {
|
|
116
|
+
const slug = `dedup-test-${Date.now()}`;
|
|
117
|
+
const v1 = saveToHistory("test-project", slug, "# Same");
|
|
118
|
+
const v2 = saveToHistory("test-project", slug, "# Same");
|
|
119
|
+
expect(v1.version).toBe(1);
|
|
120
|
+
expect(v2.version).toBe(1);
|
|
121
|
+
expect(v2.isNew).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("saves when content differs", () => {
|
|
125
|
+
const slug = `diff-test-${Date.now()}`;
|
|
126
|
+
const v1 = saveToHistory("test-project", slug, "# V1");
|
|
127
|
+
const v2 = saveToHistory("test-project", slug, "# V2");
|
|
128
|
+
expect(v2.isNew).toBe(true);
|
|
129
|
+
expect(v2.version).toBe(2);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("getPlanVersion", () => {
|
|
134
|
+
test("reads saved version content", () => {
|
|
135
|
+
const slug = `read-test-${Date.now()}`;
|
|
136
|
+
saveToHistory("test-project", slug, "# Read Me");
|
|
137
|
+
const content = getPlanVersion("test-project", slug, 1);
|
|
138
|
+
expect(content).toBe("# Read Me");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("returns null for nonexistent version", () => {
|
|
142
|
+
const content = getPlanVersion("test-project", "nonexistent", 99);
|
|
143
|
+
expect(content).toBeNull();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("getVersionCount", () => {
|
|
148
|
+
test("returns 0 for nonexistent project", () => {
|
|
149
|
+
expect(getVersionCount("nope", "nope")).toBe(0);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("counts versions correctly", () => {
|
|
153
|
+
const slug = `count-test-${Date.now()}`;
|
|
154
|
+
saveToHistory("test-project", slug, "# V1");
|
|
155
|
+
saveToHistory("test-project", slug, "# V2");
|
|
156
|
+
saveToHistory("test-project", slug, "# V3");
|
|
157
|
+
expect(getVersionCount("test-project", slug)).toBe(3);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe("listVersions", () => {
|
|
162
|
+
test("returns empty for nonexistent project", () => {
|
|
163
|
+
expect(listVersions("nope", "nope")).toEqual([]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("lists versions in ascending order", () => {
|
|
167
|
+
const slug = `list-test-${Date.now()}`;
|
|
168
|
+
saveToHistory("test-project", slug, "# V1");
|
|
169
|
+
saveToHistory("test-project", slug, "# V2");
|
|
170
|
+
const versions = listVersions("test-project", slug);
|
|
171
|
+
expect(versions).toHaveLength(2);
|
|
172
|
+
expect(versions[0].version).toBe(1);
|
|
173
|
+
expect(versions[1].version).toBe(2);
|
|
174
|
+
expect(versions[0].timestamp).toBeTruthy();
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export {
|
|
2
|
+
getPlanDir,
|
|
3
|
+
generateSlug,
|
|
4
|
+
savePlan,
|
|
5
|
+
saveAnnotations,
|
|
6
|
+
saveFinalSnapshot,
|
|
7
|
+
getHistoryDir,
|
|
8
|
+
saveToHistory,
|
|
9
|
+
getPlanVersion,
|
|
10
|
+
getPlanVersionPath,
|
|
11
|
+
getVersionCount,
|
|
12
|
+
listVersions,
|
|
13
|
+
parseArchiveFilename,
|
|
14
|
+
listArchivedPlans,
|
|
15
|
+
readArchivedPlan,
|
|
16
|
+
type ArchivedPlan,
|
|
17
|
+
} from "@plannotator/shared/storage";
|