@oisin-ee/pi-todo 0.7.1 → 0.8.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/README.md +4 -4
- package/package.json +1 -1
- package/src/contract-fixture.ts +103 -0
- package/src/display.ts +8 -2
- package/src/gate.ts +79 -0
- package/src/generated/todo_core.d.ts +342 -5
- package/src/generated/todo_core.js +222 -5
- package/src/generated/todo_core_bg.wasm +0 -0
- package/src/generated/todo_core_bg.wasm.d.ts +8 -1
- package/src/guidance.ts +5 -1
- package/src/handoff.ts +84 -0
- package/src/import-fixture.ts +63 -0
- package/src/index.ts +17 -43
- package/src/lease-fixture.ts +95 -0
- package/src/provider-fixture.ts +50 -0
- package/src/provider.ts +289 -0
- package/src/registration-fixture.ts +126 -0
- package/src/repository.ts +31 -0
- package/src/runtime-fixture.ts +209 -0
- package/src/schema.ts +86 -0
- package/src/seed-fixture.ts +35 -0
- package/src/seed-transport.ts +52 -0
- package/src/source.ts +33 -3
- package/src/state.ts +78 -1
- package/src/tool.ts +15 -14
- package/src/wasm.ts +26 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ClaimResult, UnitContract } from "./wasm.ts";
|
|
2
|
+
import { contract, firstId } from "./contract-fixture.ts";
|
|
3
|
+
import type { Result } from "./provider.ts";
|
|
4
|
+
import type { RuntimeFixture } from "./runtime-fixture.ts";
|
|
5
|
+
import { branchSnapshot } from "./state.ts";
|
|
6
|
+
import { runtimeFixture } from "./runtime-fixture.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Requires successful provider output without unchecked assertions.
|
|
10
|
+
* @param {Result<Value>} result Capability response.
|
|
11
|
+
* @returns {Value} Validated success value.
|
|
12
|
+
*/
|
|
13
|
+
const value = <Value>(result: Result<Value>): Value => {
|
|
14
|
+
if (!result.ok) {
|
|
15
|
+
throw new Error(result.error.message);
|
|
16
|
+
}
|
|
17
|
+
return result.value;
|
|
18
|
+
},
|
|
19
|
+
/**
|
|
20
|
+
* Creates and claims one worker through real registration.
|
|
21
|
+
* @param {string} repository Logical repository identity.
|
|
22
|
+
* @returns {Promise<object>} Origin runtime and frozen handoff.
|
|
23
|
+
*/
|
|
24
|
+
worker = async (
|
|
25
|
+
repository = "oisinpi",
|
|
26
|
+
): Promise<{
|
|
27
|
+
test: RuntimeFixture;
|
|
28
|
+
id: string;
|
|
29
|
+
inspected: UnitContract;
|
|
30
|
+
claimed: ClaimResult;
|
|
31
|
+
}> => {
|
|
32
|
+
const test = await runtimeFixture(),
|
|
33
|
+
draft = contract();
|
|
34
|
+
draft.assignment = { kind: "worker", profile: "general" };
|
|
35
|
+
if (draft.scope !== undefined) {
|
|
36
|
+
draft.scope.repositoryId = repository;
|
|
37
|
+
}
|
|
38
|
+
await test.todo({ items: [{ content: "Worker", contract: draft }], op: "init" });
|
|
39
|
+
const id = firstId(branchSnapshot(test.manager.getBranch())),
|
|
40
|
+
inspected = value(test.provider().inspect(id)),
|
|
41
|
+
claimed = value(
|
|
42
|
+
test.provider().claim({
|
|
43
|
+
execution: { executor: "dogsled", ownerId: "owner", requestId: "request" },
|
|
44
|
+
expected: inspected.ref,
|
|
45
|
+
}),
|
|
46
|
+
);
|
|
47
|
+
return { claimed, id, inspected, test };
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export { value, worker };
|
package/src/provider.ts
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ActiveUnit,
|
|
3
|
+
ClaimResult,
|
|
4
|
+
ImportReceipt,
|
|
5
|
+
SettlementResult,
|
|
6
|
+
TodoList,
|
|
7
|
+
UnitContract,
|
|
8
|
+
UnitRef,
|
|
9
|
+
} from "./wasm.ts";
|
|
10
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import { STATE_ENTRY, branchSnapshot } from "./state.ts";
|
|
12
|
+
import type { TodoParams, TodoResult, TodoSession } from "./tool.ts";
|
|
13
|
+
import { active, claim, importUnit, imported, inspect, parse, protect, settle } from "./wasm.ts";
|
|
14
|
+
import { ownsTodo } from "./lifecycle.ts";
|
|
15
|
+
import { repositoryIdentity } from "./repository.ts";
|
|
16
|
+
import { runTodo } from "./tool.ts";
|
|
17
|
+
|
|
18
|
+
/** Stable extension-only result envelope. */
|
|
19
|
+
type Result<Value> =
|
|
20
|
+
| { readonly ok: true; readonly value: Value }
|
|
21
|
+
| { readonly ok: false; readonly error: { readonly code: string; readonly message: string } };
|
|
22
|
+
/** Wire acknowledgements use explicit null for an unsealed import. */
|
|
23
|
+
type ImportAcknowledgement = Omit<ImportReceipt, "seal"> & {
|
|
24
|
+
readonly seal: ImportReceipt["seal"] | null;
|
|
25
|
+
};
|
|
26
|
+
/** Capability acquisition does not expose a model-facing tool. */
|
|
27
|
+
interface TaskProviderV1 {
|
|
28
|
+
readonly inspect: (id: string) => Result<UnitContract>;
|
|
29
|
+
readonly claim: (input: unknown) => Result<ClaimResult>;
|
|
30
|
+
readonly settle: (input: unknown) => Result<SettlementResult>;
|
|
31
|
+
readonly active: () => Result<ActiveUnit | null>;
|
|
32
|
+
readonly imported: () => Result<ImportAcknowledgement | null>;
|
|
33
|
+
}
|
|
34
|
+
interface ProviderHost {
|
|
35
|
+
readonly appendEntry: ExtensionAPI["appendEntry"];
|
|
36
|
+
readonly getAllTools: ExtensionAPI["getAllTools"];
|
|
37
|
+
readonly events: Readonly<ExtensionAPI["events"]>;
|
|
38
|
+
}
|
|
39
|
+
interface Acquisition {
|
|
40
|
+
readonly version: number;
|
|
41
|
+
readonly reply: (provider: TaskProviderV1) => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const version = 1,
|
|
45
|
+
/**
|
|
46
|
+
* Validates an extension capability acquisition.
|
|
47
|
+
* @param {unknown} request Untrusted event payload.
|
|
48
|
+
* @returns {boolean} Whether a versioned callback is present.
|
|
49
|
+
*/
|
|
50
|
+
acquisition = (request: unknown): request is Acquisition =>
|
|
51
|
+
typeof request === "object" &&
|
|
52
|
+
request !== null &&
|
|
53
|
+
"version" in request &&
|
|
54
|
+
request.version === version &&
|
|
55
|
+
"reply" in request &&
|
|
56
|
+
typeof request.reply === "function" &&
|
|
57
|
+
Object.keys(request).every((key) => key === "version" || key === "reply"),
|
|
58
|
+
/**
|
|
59
|
+
* Runs synchronous policy through the capability envelope.
|
|
60
|
+
* @param {Function} operation Core operation.
|
|
61
|
+
* @returns {Result} Output or explicit failure.
|
|
62
|
+
*/
|
|
63
|
+
result = <Value>(operation: () => Value): Result<Value> => {
|
|
64
|
+
try {
|
|
65
|
+
return { ok: true, value: operation() };
|
|
66
|
+
} catch (error: unknown) {
|
|
67
|
+
const message = error instanceof Error ? error.message : String(error),
|
|
68
|
+
[code] = message.split(":");
|
|
69
|
+
return { error: { code: code ?? "CONTRACT_INCOMPLETE", message }, ok: false };
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** One serialization boundary for todo operations and extension mutations. */
|
|
74
|
+
class TaskProvider {
|
|
75
|
+
readonly #host: ProviderHost;
|
|
76
|
+
readonly #leases = new Map<string, UnitRef>();
|
|
77
|
+
#context: ExtensionContext | undefined;
|
|
78
|
+
#mutating = false;
|
|
79
|
+
#generation = Symbol("task-provider");
|
|
80
|
+
|
|
81
|
+
public constructor(host: ProviderHost) {
|
|
82
|
+
this.#host = host;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Registers a versioned synchronous capability only for the winning todo owner. */
|
|
86
|
+
public register(): void {
|
|
87
|
+
this.#host.events.on("oisin.tasks.acquire.v1", (request: unknown): void => {
|
|
88
|
+
if (!ownsTodo(this.#host) || !acquisition(request)) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
request.reply(this.capability());
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Refreshes the context at the actual call boundary.
|
|
97
|
+
* @param {ExtensionContext} ctx Current runtime context.
|
|
98
|
+
*/
|
|
99
|
+
public context(ctx: ExtensionContext): void {
|
|
100
|
+
this.#context = ctx;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Removes runtime-only leases on extension shutdown. */
|
|
104
|
+
public stop(): void {
|
|
105
|
+
this.#generation = Symbol("task-provider");
|
|
106
|
+
this.#leases.clear();
|
|
107
|
+
this.#context = undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Holds a revision until this invocation ends.
|
|
112
|
+
* @param {string} id Runtime invocation ID.
|
|
113
|
+
* @param {Readonly<UnitRef>} reference Executing revision.
|
|
114
|
+
*/
|
|
115
|
+
public lease(id: string, reference: Readonly<UnitRef>): void {
|
|
116
|
+
this.#leases.set(id, structuredClone(reference));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Releases only the matching invocation.
|
|
121
|
+
* @param {string} id Ended invocation ID.
|
|
122
|
+
*/
|
|
123
|
+
public release(id: string): void {
|
|
124
|
+
this.#leases.delete(id);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Serializes normal todo operations with extension mutations.
|
|
129
|
+
* @param {TodoParams} params Model-authored request.
|
|
130
|
+
* @param {ExtensionContext} ctx Current context.
|
|
131
|
+
* @returns {TodoResult} Persisted result.
|
|
132
|
+
*/
|
|
133
|
+
public run(params: TodoParams, ctx: ExtensionContext): TodoResult {
|
|
134
|
+
this.context(ctx);
|
|
135
|
+
return this.#mutation(() => runTodo(params, this.#session()));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Imports startup transport through the same atomic mutation boundary.
|
|
140
|
+
* @param {string} source Untrusted seed JSON.
|
|
141
|
+
* @param {string} origin Checkout origin URL.
|
|
142
|
+
* @param {ExtensionContext} ctx Current runtime authority.
|
|
143
|
+
*/
|
|
144
|
+
public import(source: string, origin: string, ctx: ExtensionContext): void {
|
|
145
|
+
this.context(ctx);
|
|
146
|
+
this.#mutation(() => {
|
|
147
|
+
const session = this.#session(),
|
|
148
|
+
change = importUnit(
|
|
149
|
+
branchSnapshot(session.branch()),
|
|
150
|
+
source,
|
|
151
|
+
session.sessionId,
|
|
152
|
+
repositoryIdentity(origin),
|
|
153
|
+
);
|
|
154
|
+
if (change.persist) {
|
|
155
|
+
session.append(STATE_ENTRY, change.list);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Creates methods that recheck ownership and branch state.
|
|
162
|
+
* @returns {TaskProviderV1} Extension-only capability.
|
|
163
|
+
*/
|
|
164
|
+
public capability(): TaskProviderV1 {
|
|
165
|
+
const generation = this.#generation,
|
|
166
|
+
respond = <Value>(operation: () => Value): Result<Value> =>
|
|
167
|
+
this.#respond(generation, operation);
|
|
168
|
+
return {
|
|
169
|
+
active: () =>
|
|
170
|
+
respond(() => {
|
|
171
|
+
const session = this.#session();
|
|
172
|
+
return active(branchSnapshot(session.branch()), session.sessionId) ?? null;
|
|
173
|
+
}),
|
|
174
|
+
claim: (input) =>
|
|
175
|
+
respond(() =>
|
|
176
|
+
this.#mutation(() => {
|
|
177
|
+
const session = this.#session(),
|
|
178
|
+
change = claim(
|
|
179
|
+
branchSnapshot(session.branch()),
|
|
180
|
+
JSON.stringify(input),
|
|
181
|
+
session.sessionId,
|
|
182
|
+
);
|
|
183
|
+
if (change.persist) {
|
|
184
|
+
session.append(STATE_ENTRY, change.list);
|
|
185
|
+
}
|
|
186
|
+
return change.value;
|
|
187
|
+
}),
|
|
188
|
+
),
|
|
189
|
+
imported: () => respond(() => this.#imported()),
|
|
190
|
+
inspect: (id) => respond(() => inspect(branchSnapshot(this.#session().branch()), id)),
|
|
191
|
+
settle: (input) =>
|
|
192
|
+
respond(() =>
|
|
193
|
+
this.#mutation(() => {
|
|
194
|
+
const session = this.#session(),
|
|
195
|
+
change = settle(
|
|
196
|
+
branchSnapshot(session.branch()),
|
|
197
|
+
JSON.stringify(input),
|
|
198
|
+
session.sessionId,
|
|
199
|
+
);
|
|
200
|
+
if (change.persist) {
|
|
201
|
+
session.append(STATE_ENTRY, change.list);
|
|
202
|
+
}
|
|
203
|
+
return change.value;
|
|
204
|
+
}),
|
|
205
|
+
),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
#imported(): ImportAcknowledgement | null {
|
|
210
|
+
const session = this.#session(),
|
|
211
|
+
receipt = imported(branchSnapshot(session.branch()), session.sessionId);
|
|
212
|
+
return receipt === undefined
|
|
213
|
+
? null
|
|
214
|
+
: Object.assign({}, receipt, { seal: receipt.seal ?? null });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
#respond<Value>(generation: symbol, operation: () => Value): Result<Value> {
|
|
218
|
+
return result(() => {
|
|
219
|
+
if (generation !== this.#generation) {
|
|
220
|
+
throw new Error("PROVIDER_UNAVAILABLE: capability belongs to a closed runtime");
|
|
221
|
+
}
|
|
222
|
+
return operation();
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
#mutation<Value>(operation: () => Value): Value {
|
|
227
|
+
if (this.#mutating) {
|
|
228
|
+
throw new Error("UNIT_ALREADY_BOUND: a task mutation is already in progress");
|
|
229
|
+
}
|
|
230
|
+
this.#mutating = true;
|
|
231
|
+
try {
|
|
232
|
+
return operation();
|
|
233
|
+
} finally {
|
|
234
|
+
this.#mutating = false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Rejects branch rewind around an immutable seed or seal.
|
|
240
|
+
* @param {ExtensionContext} ctx Runtime session.
|
|
241
|
+
* @param {TodoList} branch Current branch snapshot.
|
|
242
|
+
*/
|
|
243
|
+
static #importScope(ctx: ExtensionContext, branch: TodoList): void {
|
|
244
|
+
const sessionId = ctx.sessionManager.getSessionId(),
|
|
245
|
+
receipt = imported(branch, sessionId);
|
|
246
|
+
for (const entry of ctx.sessionManager.getEntries()) {
|
|
247
|
+
if (entry.type === "custom" && entry.customType === STATE_ENTRY) {
|
|
248
|
+
const recorded = imported(parse(entry.data), sessionId);
|
|
249
|
+
if (
|
|
250
|
+
recorded !== undefined &&
|
|
251
|
+
(receipt === undefined ||
|
|
252
|
+
JSON.stringify(recorded.execution) !== JSON.stringify(receipt.execution) ||
|
|
253
|
+
recorded.digest !== receipt.digest ||
|
|
254
|
+
(recorded.seal !== undefined && receipt.seal === undefined))
|
|
255
|
+
) {
|
|
256
|
+
throw new Error("SEEDED_SCOPE_LOCKED: branch cannot discard imported scope or its seal");
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
#session(): TodoSession {
|
|
263
|
+
const ctx = this.#context;
|
|
264
|
+
if (ctx === undefined || !ownsTodo(this.#host)) {
|
|
265
|
+
throw new Error("PROVIDER_UNAVAILABLE: pi-todo does not own an active session");
|
|
266
|
+
}
|
|
267
|
+
const sessionId = ctx.sessionManager.getSessionId(),
|
|
268
|
+
leaf = ctx.sessionManager.getLeafId(),
|
|
269
|
+
before = branchSnapshot(ctx.sessionManager.getBranch());
|
|
270
|
+
TaskProvider.#importScope(ctx, before);
|
|
271
|
+
return {
|
|
272
|
+
append: (customType: string, list: TodoList): void => {
|
|
273
|
+
if (
|
|
274
|
+
ctx.sessionManager.getSessionId() !== sessionId ||
|
|
275
|
+
ctx.sessionManager.getLeafId() !== leaf
|
|
276
|
+
) {
|
|
277
|
+
throw new Error("STALE_ORIGIN: session branch changed during task mutation");
|
|
278
|
+
}
|
|
279
|
+
protect(before, list, [...this.#leases.values()]);
|
|
280
|
+
this.#host.appendEntry(customType, list);
|
|
281
|
+
},
|
|
282
|
+
branch: () => ctx.sessionManager.getBranch(),
|
|
283
|
+
sessionId,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export { TaskProvider };
|
|
289
|
+
export type { Result, TaskProviderV1 };
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionToolContext,
|
|
4
|
+
ToolDefinition,
|
|
5
|
+
ToolInfo,
|
|
6
|
+
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import type { TSchema } from "typebox";
|
|
9
|
+
import type { TodoParams } from "./tool.ts";
|
|
10
|
+
import { Parse as parse } from "typebox/value";
|
|
11
|
+
|
|
12
|
+
/** The API subset the extension uses, fully typed without loading the SDK. */
|
|
13
|
+
type TodoAPI = Readonly<
|
|
14
|
+
Pick<
|
|
15
|
+
ExtensionAPI,
|
|
16
|
+
| "appendEntry"
|
|
17
|
+
| "events"
|
|
18
|
+
| "getAllTools"
|
|
19
|
+
| "exec"
|
|
20
|
+
| "on"
|
|
21
|
+
| "registerCommand"
|
|
22
|
+
| "registerShortcut"
|
|
23
|
+
| "registerTool"
|
|
24
|
+
| "sendMessage"
|
|
25
|
+
>
|
|
26
|
+
>;
|
|
27
|
+
interface Registered {
|
|
28
|
+
readonly name: string;
|
|
29
|
+
readonly parameters: TSchema;
|
|
30
|
+
readonly execute: (params: TodoParams, ctx: ExtensionToolContext) => Promise<unknown>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface Registration {
|
|
34
|
+
readonly api: TodoAPI;
|
|
35
|
+
readonly commands: readonly string[];
|
|
36
|
+
readonly events: readonly string[];
|
|
37
|
+
readonly shortcuts: readonly string[];
|
|
38
|
+
readonly tools: readonly Registered[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Captures pi registration facts without loading the SDK.
|
|
43
|
+
* @param {Readonly<Pick<SessionManager, "appendCustomEntry">>} session Pi session append owner.
|
|
44
|
+
* @returns {Registration} Typed registration methods and captured facts.
|
|
45
|
+
*/
|
|
46
|
+
const silent = (): void => {
|
|
47
|
+
/* Registration has no transport side effects. */
|
|
48
|
+
},
|
|
49
|
+
/**
|
|
50
|
+
* Projects registered test tools to real ownership metadata.
|
|
51
|
+
* @param {readonly Registered[]} tools Captured definitions.
|
|
52
|
+
* @returns {ToolInfo[]} Runtime metadata.
|
|
53
|
+
*/
|
|
54
|
+
metadata = (tools: readonly Registered[]): ToolInfo[] =>
|
|
55
|
+
tools.map((tool) => ({
|
|
56
|
+
description: "Registered test tool",
|
|
57
|
+
exposure: "direct",
|
|
58
|
+
name: tool.name,
|
|
59
|
+
parameters: tool.parameters,
|
|
60
|
+
sourceInfo: {
|
|
61
|
+
origin: "top-level",
|
|
62
|
+
path: new URL("index.ts", import.meta.url).pathname,
|
|
63
|
+
scope: "project",
|
|
64
|
+
source: "extension",
|
|
65
|
+
},
|
|
66
|
+
})),
|
|
67
|
+
/**
|
|
68
|
+
* Converts the registered execution surface to a typed test caller.
|
|
69
|
+
* @param {Readonly<object>} tool Registered tool definition.
|
|
70
|
+
* @returns {Registered} Captured registration.
|
|
71
|
+
*/
|
|
72
|
+
registeredTool = <Params extends TSchema, Details, State>(
|
|
73
|
+
tool: Readonly<Pick<ToolDefinition<Params, Details, State>, "execute" | "name" | "parameters">>,
|
|
74
|
+
): Registered => ({
|
|
75
|
+
execute: async (params, ctx) =>
|
|
76
|
+
tool.execute("test", parse(tool.parameters, params), undefined, undefined, ctx),
|
|
77
|
+
name: tool.name,
|
|
78
|
+
parameters: tool.parameters,
|
|
79
|
+
}),
|
|
80
|
+
registration = (
|
|
81
|
+
session: Readonly<Pick<SessionManager, "appendCustomEntry">> = SessionManager.inMemory(),
|
|
82
|
+
): Registration => {
|
|
83
|
+
const commands: string[] = [],
|
|
84
|
+
events: string[] = [],
|
|
85
|
+
shortcuts: string[] = [],
|
|
86
|
+
tools: Registered[] = [];
|
|
87
|
+
return {
|
|
88
|
+
api: {
|
|
89
|
+
appendEntry: (customType, data): void => {
|
|
90
|
+
session.appendCustomEntry(customType, data);
|
|
91
|
+
},
|
|
92
|
+
events: {
|
|
93
|
+
emit: silent,
|
|
94
|
+
on: (): (() => void) => silent,
|
|
95
|
+
},
|
|
96
|
+
exec: async () => {
|
|
97
|
+
throw new Error("Unexpected startup execution");
|
|
98
|
+
},
|
|
99
|
+
getAllTools: () => metadata(tools),
|
|
100
|
+
on: (event): (() => void) => {
|
|
101
|
+
events.push(event);
|
|
102
|
+
return silent;
|
|
103
|
+
},
|
|
104
|
+
registerCommand: (name): void => {
|
|
105
|
+
commands.push(name);
|
|
106
|
+
},
|
|
107
|
+
registerShortcut: (key): void => {
|
|
108
|
+
shortcuts.push(key);
|
|
109
|
+
},
|
|
110
|
+
registerTool: <Params extends TSchema, Details, State>(
|
|
111
|
+
tool: Readonly<
|
|
112
|
+
Pick<ToolDefinition<Params, Details, State>, "execute" | "name" | "parameters">
|
|
113
|
+
>,
|
|
114
|
+
): void => {
|
|
115
|
+
tools.push(registeredTool(tool));
|
|
116
|
+
},
|
|
117
|
+
sendMessage: silent,
|
|
118
|
+
},
|
|
119
|
+
commands,
|
|
120
|
+
events,
|
|
121
|
+
shortcuts,
|
|
122
|
+
tools,
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export { registration };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical origin identity, independent of clone transport and checkout path.
|
|
3
|
+
* @param {string} origin Git's origin URL.
|
|
4
|
+
* @returns {string} Lowercase host and case-preserved repository path.
|
|
5
|
+
*/
|
|
6
|
+
const repositoryIdentity = (origin: string): string => {
|
|
7
|
+
const value = origin.trim(),
|
|
8
|
+
scp = value.includes("://")
|
|
9
|
+
? null
|
|
10
|
+
: /^(?:[^/@:\s]+@)?(?<host>[^/:\s]+):(?<path>[^\s]+)$/u.exec(value),
|
|
11
|
+
url = new URL(
|
|
12
|
+
scp?.groups === undefined ? value : `ssh://${scp.groups["host"]}/${scp.groups["path"]}`,
|
|
13
|
+
),
|
|
14
|
+
path = url.pathname
|
|
15
|
+
.replace(/^\//u, "")
|
|
16
|
+
.replace(/\/$/u, "")
|
|
17
|
+
.replace(/\.git$/u, "");
|
|
18
|
+
if (
|
|
19
|
+
!["ssh:", "https:", "http:", "git:"].includes(url.protocol) ||
|
|
20
|
+
url.hostname === "" ||
|
|
21
|
+
url.search !== "" ||
|
|
22
|
+
url.hash !== "" ||
|
|
23
|
+
path === "" ||
|
|
24
|
+
path.split("/").some((part) => part === "" || part === "." || part === "..")
|
|
25
|
+
) {
|
|
26
|
+
throw new Error("IMPORT_FAILED: origin must identify a remote repository host/path");
|
|
27
|
+
}
|
|
28
|
+
return `${url.hostname.toLowerCase()}/${path}`;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export { repositoryIdentity };
|