@nseng-ai/branch-context 0.1.1
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/package.json +48 -0
- package/src/api/index.ts +43 -0
- package/src/api/prompt-assets.ts +4 -0
- package/src/core/attach.ts +455 -0
- package/src/core/attached-plan.ts +447 -0
- package/src/core/branch-context-creation.ts +495 -0
- package/src/core/branch-memory.ts +147 -0
- package/src/core/cli.ts +98 -0
- package/src/core/constants.ts +19 -0
- package/src/core/context.ts +45 -0
- package/src/core/existing-branch-reuse.ts +230 -0
- package/src/core/index.ts +47 -0
- package/src/core/operations.ts +590 -0
- package/src/core/plan-content-slug.ts +50 -0
- package/src/core/prompt-assets.ts +14 -0
- package/src/core/prompts/branch-context-impl.md +34 -0
- package/src/core/session-artifact.ts +114 -0
- package/src/ns/command.ts +33 -0
- package/src/ns/commands/attach.ts +19 -0
- package/src/ns/commands/check.ts +18 -0
- package/src/ns/commands/delete.ts +18 -0
- package/src/ns/commands/from-plan.ts +21 -0
- package/src/ns/commands/list.ts +17 -0
- package/src/ns/commands/load.ts +19 -0
- package/src/ns/repo-local-ns-extension.ts +32 -0
- package/src/pi/enriched-plan-save.ts +551 -0
- package/src/pi/extension.ts +154 -0
- package/src/pi/from-plan-commands.ts +1243 -0
- package/src/pi/gt/upstack-impl-launch.ts +154 -0
- package/src/pi/host-types.ts +135 -0
- package/src/pi/index.ts +16 -0
- package/src/pi/options.ts +32 -0
- package/src/pi/surfaces.ts +11 -0
- package/src/testing/index.ts +258 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { type BranchContextEvidence } from "@nseng-ai/branch-context/api";
|
|
2
|
+
import { formatImplBranchContextCommand } from "../surfaces.ts";
|
|
3
|
+
import type { ExecResult } from "@nseng-ai/foundation/command";
|
|
4
|
+
import { setRuntimeStatus } from "@nseng-ai/pi/runtime/status";
|
|
5
|
+
import type { ExtensionAPI, NewSessionOptions, NewSessionResult } from "../host-types.ts";
|
|
6
|
+
|
|
7
|
+
export type BranchContextGtUpstackImplHost = Pick<ExtensionAPI, "exec">;
|
|
8
|
+
|
|
9
|
+
export type BranchContextGtUpstackImplNewSessionOptions = NewSessionOptions;
|
|
10
|
+
export type BranchContextGtUpstackImplNewSessionResult = NewSessionResult;
|
|
11
|
+
|
|
12
|
+
interface LaunchStatusUi {
|
|
13
|
+
setStatus?(key: string, value: string | undefined): void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface BranchContextGtUpstackImplContext {
|
|
17
|
+
cwd: string;
|
|
18
|
+
hasUI: boolean;
|
|
19
|
+
ui: LaunchStatusUi;
|
|
20
|
+
sessionManager?: {
|
|
21
|
+
getSessionFile?(): string | undefined;
|
|
22
|
+
};
|
|
23
|
+
newSession(
|
|
24
|
+
options?: BranchContextGtUpstackImplNewSessionOptions,
|
|
25
|
+
): Promise<BranchContextGtUpstackImplNewSessionResult>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface BranchContextGtUpstackImplLaunchOptions {
|
|
29
|
+
host: BranchContextGtUpstackImplHost;
|
|
30
|
+
ctx: BranchContextGtUpstackImplContext;
|
|
31
|
+
statusKey: string;
|
|
32
|
+
target: Pick<BranchContextEvidence, "branch" | "key">;
|
|
33
|
+
signal?: AbortSignal;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type BranchContextGtUpstackImplLaunchPhase = "checkout" | "new-session";
|
|
37
|
+
|
|
38
|
+
export type BranchContextGtUpstackImplLaunchResult =
|
|
39
|
+
| { type: "launched"; branch: string; key: string; parentSession?: string }
|
|
40
|
+
| { type: "cancelled"; branch: string; key: string; parentSession?: string }
|
|
41
|
+
| {
|
|
42
|
+
type: "failed";
|
|
43
|
+
branch: string;
|
|
44
|
+
key: string;
|
|
45
|
+
phase: BranchContextGtUpstackImplLaunchPhase;
|
|
46
|
+
message: string;
|
|
47
|
+
parentSession?: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const CHECKOUT_TIMEOUT_MS = 30_000;
|
|
51
|
+
|
|
52
|
+
export async function runBranchContextGtUpstackImplLaunch(
|
|
53
|
+
options: BranchContextGtUpstackImplLaunchOptions,
|
|
54
|
+
): Promise<BranchContextGtUpstackImplLaunchResult> {
|
|
55
|
+
const { branch, key } = options.target;
|
|
56
|
+
let isReplacementSessionActive = false;
|
|
57
|
+
let phase: BranchContextGtUpstackImplLaunchPhase = "checkout";
|
|
58
|
+
let parentSession: string | undefined;
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
setRuntimeStatus(options.ctx, options.statusKey, "checking out branch context…");
|
|
62
|
+
const checkout = await checkoutBranchContext({
|
|
63
|
+
host: options.host,
|
|
64
|
+
cwd: options.ctx.cwd,
|
|
65
|
+
targetBranch: branch,
|
|
66
|
+
signal: options.signal,
|
|
67
|
+
});
|
|
68
|
+
if (checkout.type === "failed") {
|
|
69
|
+
return { type: "failed", branch, key, phase: "checkout", message: checkout.message };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
phase = "new-session";
|
|
73
|
+
setRuntimeStatus(options.ctx, options.statusKey, "starting implementation session…");
|
|
74
|
+
parentSession = options.ctx.sessionManager?.getSessionFile?.();
|
|
75
|
+
const parentSessionPart = parentSession === undefined ? {} : { parentSession };
|
|
76
|
+
const newSessionOptions: BranchContextGtUpstackImplNewSessionOptions = {
|
|
77
|
+
withSession: async (newCtx) => {
|
|
78
|
+
isReplacementSessionActive = true;
|
|
79
|
+
await newCtx.sendUserMessage(formatImplBranchContextCommand(key));
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
if (parentSession !== undefined) {
|
|
83
|
+
newSessionOptions.parentSession = parentSession;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const result = await options.ctx.newSession(newSessionOptions);
|
|
87
|
+
if (result.cancelled) {
|
|
88
|
+
return { type: "cancelled", branch, key, ...parentSessionPart };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { type: "launched", branch, key, ...parentSessionPart };
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (isReplacementSessionActive) {
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
type: "failed",
|
|
98
|
+
branch,
|
|
99
|
+
key,
|
|
100
|
+
phase,
|
|
101
|
+
message: error instanceof Error ? error.message : String(error),
|
|
102
|
+
...(parentSession === undefined ? {} : { parentSession }),
|
|
103
|
+
};
|
|
104
|
+
} finally {
|
|
105
|
+
if (!isReplacementSessionActive) {
|
|
106
|
+
setRuntimeStatus(options.ctx, options.statusKey, undefined);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function formatBranchContextGtUpstackImplFollowUpFlow(
|
|
112
|
+
targetBranch: string,
|
|
113
|
+
key: string,
|
|
114
|
+
): string {
|
|
115
|
+
return [`git checkout ${targetBranch}`, "/new", formatImplBranchContextCommand(key)].join("\n");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
type CheckoutResult = { type: "ok" } | { type: "failed"; message: string };
|
|
119
|
+
|
|
120
|
+
interface CheckoutBranchContextOptions {
|
|
121
|
+
host: BranchContextGtUpstackImplHost;
|
|
122
|
+
cwd: string;
|
|
123
|
+
targetBranch: string;
|
|
124
|
+
signal: AbortSignal | undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function checkoutBranchContext(
|
|
128
|
+
options: CheckoutBranchContextOptions,
|
|
129
|
+
): Promise<CheckoutResult> {
|
|
130
|
+
const result = await options.host.exec("git", ["checkout", options.targetBranch], {
|
|
131
|
+
cwd: options.cwd,
|
|
132
|
+
timeout: CHECKOUT_TIMEOUT_MS,
|
|
133
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
134
|
+
});
|
|
135
|
+
if (result.code === 0) {
|
|
136
|
+
return { type: "ok" };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const output = formatCheckoutFailureOutput(result);
|
|
140
|
+
return {
|
|
141
|
+
type: "failed",
|
|
142
|
+
message: `git checkout ${options.targetBranch} failed with exit code ${result.code}: ${output}`,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function formatCheckoutFailureOutput(result: ExecResult): string {
|
|
147
|
+
if (result.stderr.length > 0) {
|
|
148
|
+
return result.stderr;
|
|
149
|
+
}
|
|
150
|
+
if (result.stdout.length > 0) {
|
|
151
|
+
return result.stdout;
|
|
152
|
+
}
|
|
153
|
+
return "(no output)";
|
|
154
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
2
|
+
import type {
|
|
3
|
+
BranchContextContextFactory,
|
|
4
|
+
BranchCreationMethod,
|
|
5
|
+
createBranchContextFromFile,
|
|
6
|
+
loadBranchContextPlan,
|
|
7
|
+
} from "@nseng-ai/branch-context/api";
|
|
8
|
+
import type { ExecOptions, ExecResult } from "@nseng-ai/foundation/command";
|
|
9
|
+
import type {
|
|
10
|
+
SessionReplacementContext,
|
|
11
|
+
SessionReplacementOptions,
|
|
12
|
+
SessionReplacementResult,
|
|
13
|
+
} from "@nseng-ai/pi/sessions/replacement";
|
|
14
|
+
import type { resolveSelectedSavedPlanFile, writeSavedPlanFile } from "@nseng-ai/plans/api";
|
|
15
|
+
import type { SendMessageOptions } from "@nseng-ai/pi/shared/message-delivery";
|
|
16
|
+
|
|
17
|
+
export type NotifyLevel = "info" | "warning" | "error";
|
|
18
|
+
|
|
19
|
+
export interface TextContent {
|
|
20
|
+
type: "text";
|
|
21
|
+
text: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type CustomMessageContent = string | TextContent[];
|
|
25
|
+
|
|
26
|
+
export interface CustomMessage {
|
|
27
|
+
customType: string;
|
|
28
|
+
content: CustomMessageContent;
|
|
29
|
+
display: boolean;
|
|
30
|
+
details?: unknown;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ToolResult {
|
|
34
|
+
content: TextContent[];
|
|
35
|
+
details?: unknown;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type ToolUpdateHandler = (update: Partial<ToolResult>) => void;
|
|
39
|
+
|
|
40
|
+
export type SessionHistoryEntry = unknown;
|
|
41
|
+
|
|
42
|
+
export interface SessionManagerLike {
|
|
43
|
+
getBranch?(): SessionHistoryEntry[];
|
|
44
|
+
getEntries?(): SessionHistoryEntry[];
|
|
45
|
+
getSessionFile?(): string | undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type NewSessionResult = SessionReplacementResult;
|
|
49
|
+
|
|
50
|
+
export interface ReplacedSessionContext extends CommandContext, SessionReplacementContext {
|
|
51
|
+
sendMessage(message: CustomMessage, options?: SendMessageOptions): Promise<void> | void;
|
|
52
|
+
sendUserMessage(content: string): Promise<void> | void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type NewSessionOptions = SessionReplacementOptions<
|
|
56
|
+
ReplacedSessionContext,
|
|
57
|
+
SessionManagerLike
|
|
58
|
+
>;
|
|
59
|
+
|
|
60
|
+
export interface BranchContextOperations {
|
|
61
|
+
loadBranchContextPlan: typeof loadBranchContextPlan;
|
|
62
|
+
createBranchContextFromFile: typeof createBranchContextFromFile;
|
|
63
|
+
writeSavedPlanFile: typeof writeSavedPlanFile;
|
|
64
|
+
resolveSelectedSavedPlanFile: typeof resolveSelectedSavedPlanFile;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface BranchContextExtensionOptions {
|
|
68
|
+
branchContextDefaultCreation?: BranchCreationMethod;
|
|
69
|
+
branchContextPrefix?: string;
|
|
70
|
+
planStoreRoot?: string;
|
|
71
|
+
branchContextOperations?: BranchContextOperations;
|
|
72
|
+
createBranchContextContext?: BranchContextContextFactory<[pi: ExtensionAPI, cwd: string]>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface ToolContext {
|
|
76
|
+
cwd: string;
|
|
77
|
+
hasUI?: boolean;
|
|
78
|
+
ui?: {
|
|
79
|
+
setStatus?(key: string, value: string | undefined): void;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface ToolRenderResultOptions {
|
|
84
|
+
isPartial: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface ToolDefinition {
|
|
88
|
+
name: string;
|
|
89
|
+
label?: string;
|
|
90
|
+
description: string;
|
|
91
|
+
promptSnippet?: string;
|
|
92
|
+
promptGuidelines?: string[];
|
|
93
|
+
parameters: Record<string, unknown>;
|
|
94
|
+
execute(
|
|
95
|
+
toolCallId: string,
|
|
96
|
+
params: unknown,
|
|
97
|
+
signal: AbortSignal | undefined,
|
|
98
|
+
onUpdate: ToolUpdateHandler | undefined,
|
|
99
|
+
ctx: ToolContext,
|
|
100
|
+
): Promise<ToolResult> | ToolResult;
|
|
101
|
+
renderCall?(args: unknown, theme: unknown, context: unknown): Component;
|
|
102
|
+
renderResult?(
|
|
103
|
+
result: ToolResult,
|
|
104
|
+
options: ToolRenderResultOptions,
|
|
105
|
+
theme: unknown,
|
|
106
|
+
context: unknown,
|
|
107
|
+
): Component;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface CommandContext {
|
|
111
|
+
cwd: string;
|
|
112
|
+
hasUI: boolean;
|
|
113
|
+
ui: {
|
|
114
|
+
notify(message: string, level?: NotifyLevel): void;
|
|
115
|
+
setStatus(key: string, value: string | undefined): void;
|
|
116
|
+
confirm?(title: string, message?: string): Promise<boolean>;
|
|
117
|
+
};
|
|
118
|
+
waitForIdle(): Promise<void>;
|
|
119
|
+
newSession(options?: NewSessionOptions): Promise<NewSessionResult>;
|
|
120
|
+
sessionManager?: SessionManagerLike;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface ExtensionAPI {
|
|
124
|
+
registerCommand(
|
|
125
|
+
name: string,
|
|
126
|
+
options: {
|
|
127
|
+
description?: string;
|
|
128
|
+
handler(args: string, ctx: CommandContext): Promise<void> | void;
|
|
129
|
+
},
|
|
130
|
+
): void;
|
|
131
|
+
registerTool(definition: ToolDefinition): void;
|
|
132
|
+
exec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;
|
|
133
|
+
sendMessage?(message: CustomMessage, options?: SendMessageOptions): void;
|
|
134
|
+
sendUserMessage(content: string): void;
|
|
135
|
+
}
|
package/src/pi/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export {
|
|
2
|
+
CREATE_BRANCH_CONTEXT_COMMAND_NAME,
|
|
3
|
+
GT_UPSTACK_IMPL_COMMAND_NAME,
|
|
4
|
+
WRITE_GRILLED_PLAN_COMMAND_NAME,
|
|
5
|
+
WRITE_PLAN_COMMAND_NAME,
|
|
6
|
+
branchContextExtensionParity,
|
|
7
|
+
default,
|
|
8
|
+
} from "./extension.ts";
|
|
9
|
+
export {
|
|
10
|
+
BRANCH_CONTEXT_FROM_PLAN_COMMAND_NAME,
|
|
11
|
+
BRANCH_CONTEXT_UPSTACK_IMPL_FROM_PLAN_COMMAND_NAME,
|
|
12
|
+
IMPL_BRANCH_CONTEXT_COMMAND_NAME,
|
|
13
|
+
IMPL_CURRENT_SAVED_PLAN_COMMAND_NAME,
|
|
14
|
+
formatImplBranchContextCommand,
|
|
15
|
+
} from "./surfaces.ts";
|
|
16
|
+
export type { BranchContextExtensionOptions, ExtensionAPI } from "./extension.ts";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createBranchContextFromFile,
|
|
3
|
+
loadBranchContextPlan,
|
|
4
|
+
type BranchCreationMethod,
|
|
5
|
+
} from "@nseng-ai/branch-context/api";
|
|
6
|
+
import { resolveSelectedSavedPlanFile, writeSavedPlanFile } from "@nseng-ai/plans/api";
|
|
7
|
+
import type { BranchContextExtensionOptions, BranchContextOperations } from "./host-types.ts";
|
|
8
|
+
|
|
9
|
+
const realBranchContextOperations: BranchContextOperations = {
|
|
10
|
+
loadBranchContextPlan,
|
|
11
|
+
createBranchContextFromFile,
|
|
12
|
+
writeSavedPlanFile,
|
|
13
|
+
resolveSelectedSavedPlanFile,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function resolveBranchContextOperations(
|
|
17
|
+
options: BranchContextExtensionOptions,
|
|
18
|
+
): BranchContextOperations {
|
|
19
|
+
return options.branchContextOperations ?? realBranchContextOperations;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resolveBranchContextDefaultCreation(
|
|
23
|
+
options: BranchContextExtensionOptions,
|
|
24
|
+
): BranchCreationMethod {
|
|
25
|
+
return options.branchContextDefaultCreation ?? "plain-git";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function resolvePlanStoreRootOption(
|
|
29
|
+
options: BranchContextExtensionOptions,
|
|
30
|
+
): string | undefined {
|
|
31
|
+
return options.planStoreRoot;
|
|
32
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const BRANCH_CONTEXT_FROM_PLAN_COMMAND_NAME = "ns:branch-context:from-plan";
|
|
2
|
+
export const BRANCH_CONTEXT_UPSTACK_IMPL_FROM_PLAN_COMMAND_NAME =
|
|
3
|
+
"ns:branch-context:upstack-impl-from-plan";
|
|
4
|
+
export const IMPL_BRANCH_CONTEXT_COMMAND_NAME = "ns:branch-context:impl-attached-plan";
|
|
5
|
+
export const WRITE_PLAN_COMMAND_NAME = "ns:plan:save";
|
|
6
|
+
export const WRITE_GRILLED_PLAN_COMMAND_NAME = "ns:plan:grill-and-save";
|
|
7
|
+
export const IMPL_CURRENT_SAVED_PLAN_COMMAND_NAME = "ns:plan:impl-current";
|
|
8
|
+
|
|
9
|
+
export function formatImplBranchContextCommand(key: string): string {
|
|
10
|
+
return `/${IMPL_BRANCH_CONTEXT_COMMAND_NAME} ${key}`;
|
|
11
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FakeBrmemGateway,
|
|
3
|
+
type BrmemGateway,
|
|
4
|
+
type BrmemOptionalResult,
|
|
5
|
+
type BrmemResult,
|
|
6
|
+
type CopyEntriesResult,
|
|
7
|
+
type DeleteEntryResult,
|
|
8
|
+
type EntryContent,
|
|
9
|
+
type EntryDiagnostic,
|
|
10
|
+
type FakeBrmemGatewayOptions,
|
|
11
|
+
type FakeEntrySeed,
|
|
12
|
+
type GitRemoteConfig,
|
|
13
|
+
type ListedEntry,
|
|
14
|
+
type ListedSnapshot,
|
|
15
|
+
type PutEntryResult,
|
|
16
|
+
} from "@nseng-ai/brmem";
|
|
17
|
+
|
|
18
|
+
import { BRANCH_CONTEXT_NAMESPACE } from "../core/constants.ts";
|
|
19
|
+
|
|
20
|
+
export interface InMemoryAttachedPlanState {
|
|
21
|
+
branch: string;
|
|
22
|
+
key: string;
|
|
23
|
+
content?: string;
|
|
24
|
+
refName?: string;
|
|
25
|
+
commit?: string;
|
|
26
|
+
sourceFile?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface InMemoryBrmemGatewayState {
|
|
30
|
+
currentBranch?: FakeBrmemGatewayOptions["currentBranch"];
|
|
31
|
+
entries?: readonly InMemoryAttachedPlanState[];
|
|
32
|
+
presenceFailure?: { code: string; message: string };
|
|
33
|
+
attachFailure?: { code: string; message: string };
|
|
34
|
+
listFailure?: { code: string; message: string };
|
|
35
|
+
getFailure?: { code: string; message: string };
|
|
36
|
+
deleteFailure?: { code: string; message: string };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface BrmemAttachmentCall {
|
|
40
|
+
branch: string;
|
|
41
|
+
key: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface BrmemListCall {
|
|
45
|
+
branch?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface BrmemPutCall {
|
|
49
|
+
namespace: string;
|
|
50
|
+
branch: string;
|
|
51
|
+
key: string;
|
|
52
|
+
content: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface StoredAttachedPlan {
|
|
56
|
+
branch: string;
|
|
57
|
+
key: string;
|
|
58
|
+
content: string;
|
|
59
|
+
refName: string;
|
|
60
|
+
commit: string;
|
|
61
|
+
sourceFile: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class InMemoryBranchMemoryGateway implements BrmemGateway {
|
|
65
|
+
private readonly fake: FakeBrmemGateway;
|
|
66
|
+
private readonly entries = new Map<string, StoredAttachedPlan>();
|
|
67
|
+
readonly checkEntryCalls: BrmemAttachmentCall[] = [];
|
|
68
|
+
readonly putEntryCalls: BrmemPutCall[] = [];
|
|
69
|
+
readonly listEntriesCalls: BrmemListCall[] = [];
|
|
70
|
+
readonly getEntryCalls: BrmemAttachmentCall[] = [];
|
|
71
|
+
readonly deleteEntryCalls: BrmemAttachmentCall[] = [];
|
|
72
|
+
|
|
73
|
+
constructor(state: InMemoryBrmemGatewayState = {}) {
|
|
74
|
+
for (const entry of state.entries ?? [])
|
|
75
|
+
this.entries.set(entryKey(entry.branch, entry.key), normalizeEntry(entry));
|
|
76
|
+
this.fake = new FakeBrmemGateway({
|
|
77
|
+
...(state.currentBranch === undefined ? {} : { currentBranch: state.currentBranch }),
|
|
78
|
+
entries: (state.entries ?? []).map(toFakeEntry),
|
|
79
|
+
operationErrors: {
|
|
80
|
+
...(state.presenceFailure === undefined ? {} : { check: state.presenceFailure }),
|
|
81
|
+
...(state.attachFailure === undefined ? {} : { put: state.attachFailure }),
|
|
82
|
+
...(state.listFailure === undefined ? {} : { list: state.listFailure }),
|
|
83
|
+
...(state.getFailure === undefined ? {} : { get: state.getFailure }),
|
|
84
|
+
...(state.deleteFailure === undefined ? {} : { delete: state.deleteFailure }),
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
get attachmentPresenceCalls(): readonly BrmemAttachmentCall[] {
|
|
90
|
+
return this.checkEntryCalls.map((call) => ({ ...call }));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
get attachPlanCalls(): readonly BrmemPutCall[] {
|
|
94
|
+
return this.putEntryCalls.map((call) => ({ ...call }));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
get listAttachedPlansCalls(): readonly BrmemListCall[] {
|
|
98
|
+
return this.listEntriesCalls.map((call) => ({ ...call }));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
get getAttachedPlanCalls(): readonly BrmemAttachmentCall[] {
|
|
102
|
+
return this.getEntryCalls.map((call) => ({ ...call }));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
get attachedPlans(): readonly InMemoryAttachedPlanState[] {
|
|
106
|
+
return [...this.entries.values()].map((entry) => ({ ...entry }));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async currentBranch(): Promise<BrmemResult<string>> {
|
|
110
|
+
return await this.fake.currentBranch();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async listEntries(options: {
|
|
114
|
+
namespace: string;
|
|
115
|
+
key?: string;
|
|
116
|
+
branch?: string;
|
|
117
|
+
}): Promise<BrmemResult<readonly ListedEntry[]>> {
|
|
118
|
+
this.listEntriesCalls.push(options.branch === undefined ? {} : { branch: options.branch });
|
|
119
|
+
return await this.fake.listEntries(options);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async listAllEntries(options: {
|
|
123
|
+
key?: string;
|
|
124
|
+
branch?: string;
|
|
125
|
+
}): Promise<BrmemResult<readonly ListedEntry[]>> {
|
|
126
|
+
return await this.fake.listAllEntries(options);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async getEntry(options: {
|
|
130
|
+
namespace: string;
|
|
131
|
+
key: string;
|
|
132
|
+
branch: string;
|
|
133
|
+
at?: string;
|
|
134
|
+
}): Promise<BrmemOptionalResult<EntryContent>> {
|
|
135
|
+
this.getEntryCalls.push({ branch: options.branch, key: options.key });
|
|
136
|
+
return await this.fake.getEntry(options);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async checkEntry(options: {
|
|
140
|
+
namespace: string;
|
|
141
|
+
key: string;
|
|
142
|
+
branch: string;
|
|
143
|
+
at?: string;
|
|
144
|
+
}): Promise<BrmemOptionalResult<EntryDiagnostic>> {
|
|
145
|
+
this.checkEntryCalls.push({ branch: options.branch, key: options.key });
|
|
146
|
+
return await this.fake.checkEntry(options);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async putEntry(options: BrmemPutCall): Promise<BrmemResult<PutEntryResult>> {
|
|
150
|
+
this.putEntryCalls.push({ ...options });
|
|
151
|
+
const result = await this.fake.putEntry(options);
|
|
152
|
+
this.recordEntryWriteResult(options, result);
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async createEntry(options: BrmemPutCall): Promise<BrmemResult<PutEntryResult>> {
|
|
157
|
+
this.putEntryCalls.push({ ...options });
|
|
158
|
+
const result = await this.fake.createEntry(options);
|
|
159
|
+
this.recordEntryWriteResult(options, result);
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private recordEntryWriteResult(options: BrmemPutCall, result: BrmemResult<PutEntryResult>): void {
|
|
164
|
+
if (result.type !== "ok" || options.namespace !== BRANCH_CONTEXT_NAMESPACE) return;
|
|
165
|
+
this.entries.set(entryKey(options.branch, options.key), {
|
|
166
|
+
branch: options.branch,
|
|
167
|
+
key: options.key,
|
|
168
|
+
content: options.content,
|
|
169
|
+
refName: result.value.entry.entryLocator,
|
|
170
|
+
commit: result.value.commitSha,
|
|
171
|
+
sourceFile: "",
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async deleteEntry(options: {
|
|
176
|
+
namespace: string;
|
|
177
|
+
key: string;
|
|
178
|
+
branch: string;
|
|
179
|
+
}): Promise<BrmemResult<DeleteEntryResult>> {
|
|
180
|
+
this.deleteEntryCalls.push({ branch: options.branch, key: options.key });
|
|
181
|
+
const result = await this.fake.deleteEntry(options);
|
|
182
|
+
if (result.type === "ok" && options.namespace === BRANCH_CONTEXT_NAMESPACE) {
|
|
183
|
+
this.entries.delete(entryKey(options.branch, options.key));
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async copyEntries(options: {
|
|
189
|
+
namespace: string;
|
|
190
|
+
fromBranch: string;
|
|
191
|
+
toBranch: string;
|
|
192
|
+
shouldOverwrite: boolean;
|
|
193
|
+
keyGlob?: string;
|
|
194
|
+
}): Promise<BrmemResult<CopyEntriesResult>> {
|
|
195
|
+
return await this.fake.copyEntries(options);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async listSnapshots(options: {
|
|
199
|
+
namespace?: string;
|
|
200
|
+
}): Promise<BrmemResult<readonly ListedSnapshot[]>> {
|
|
201
|
+
return await this.fake.listSnapshots(options);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async listLocalBranches(): Promise<BrmemResult<ReadonlySet<string>>> {
|
|
205
|
+
return await this.fake.listLocalBranches();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async localBranchPresence(options: {
|
|
209
|
+
branch: string;
|
|
210
|
+
}): Promise<BrmemResult<"present" | "absent">> {
|
|
211
|
+
return await this.fake.localBranchPresence(options);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async deleteSnapshot(options: { namespace: string; branch: string }): Promise<BrmemResult<void>> {
|
|
215
|
+
return await this.fake.deleteSnapshot(options);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async getRemoteConfig(remote: string): Promise<BrmemOptionalResult<GitRemoteConfig>> {
|
|
219
|
+
return await this.fake.getRemoteConfig(remote);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async addRemoteRefspecs(
|
|
223
|
+
remote: string,
|
|
224
|
+
push: readonly string[],
|
|
225
|
+
fetch: readonly string[],
|
|
226
|
+
): Promise<BrmemResult<void>> {
|
|
227
|
+
return await this.fake.addRemoteRefspecs(remote, push, fetch);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function toFakeEntry(entry: InMemoryAttachedPlanState): FakeEntrySeed {
|
|
232
|
+
return {
|
|
233
|
+
namespace: BRANCH_CONTEXT_NAMESPACE,
|
|
234
|
+
branch: entry.branch,
|
|
235
|
+
key: entry.key,
|
|
236
|
+
content: entry.content ?? "",
|
|
237
|
+
...(entry.commit === undefined ? {} : { headSha: entry.commit }),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function normalizeEntry(entry: InMemoryAttachedPlanState): StoredAttachedPlan {
|
|
242
|
+
return {
|
|
243
|
+
branch: entry.branch,
|
|
244
|
+
key: entry.key,
|
|
245
|
+
content: entry.content ?? "",
|
|
246
|
+
refName: entry.refName ?? defaultRefName(entry.branch, entry.key),
|
|
247
|
+
commit: entry.commit ?? "abc123",
|
|
248
|
+
sourceFile: entry.sourceFile ?? `/tmp/${entry.key}`,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function defaultRefName(branch: string, key: string): string {
|
|
253
|
+
return `refs/brmem/ns/${BRANCH_CONTEXT_NAMESPACE}/${branch.replaceAll("/", "---")}:${key}`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function entryKey(branch: string, key: string): string {
|
|
257
|
+
return `${branch}\0${key}`;
|
|
258
|
+
}
|