pi-onedev-toolkit 0.3.0 → 0.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0 — 2026-09-05
4
+
5
+ ### Changed
6
+
7
+ - `issue`, `pull`, and `build` `list` actions with an explicit `project` now work even when the current repository has no OneDev remote: the query runs against a temporary scratch remote that is created and removed for the call.
8
+ - Refreshed development dependencies (pi packages 0.85.1, typebox 1.3.27).
9
+
10
+
3
11
  ## 0.3.0 — 2026-09-05
4
12
 
5
13
  ### Changed
package/README.md CHANGED
@@ -55,6 +55,7 @@ Attachment download is intentionally not exposed: current TOD releases may send
55
55
 
56
56
  - Update TOD regularly from its upstream install script. `get_unit_test_report` reports an actionable upgrade error when the installed TOD predates that command.
57
57
  - Plain references require the current repository to have a OneDev remote from which TOD can infer the project. The toolkit reports `no_project` rather than pretending a qualified reference can bypass TOD's resolver.
58
+ - `issue`, `pull`, and `build` `list` calls with an explicit `project` work from any repository: the toolkit binds TOD to a throwaway Git remote for the query and removes it afterwards.
58
59
  - Tool calls use `snake_case`, for example `for_code_review`, `target_branch`, `report_name`, and `interval_seconds`.
59
60
 
60
61
  ## Skills
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-onedev-toolkit",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Lightweight OneDev integration for Pi via TOD: repo-aware issue, pull request, build, and watch tools",
5
5
  "type": "module",
6
6
  "author": {
@@ -53,12 +53,12 @@
53
53
  "typebox": "*"
54
54
  },
55
55
  "devDependencies": {
56
- "@earendil-works/pi-ai": "0.85.0",
57
- "@earendil-works/pi-server": "0.85.0",
58
- "@earendil-works/pi-coding-agent": "0.85.0",
56
+ "@earendil-works/pi-ai": "0.85.1",
57
+ "@earendil-works/pi-coding-agent": "0.85.1",
58
+ "@earendil-works/pi-server": "0.85.1",
59
59
  "@types/node": "^22.19.0",
60
60
  "@types/proper-lockfile": "4.1.4",
61
- "typebox": "1.3.25",
61
+ "typebox": "1.3.27",
62
62
  "typescript": "^7.0.2",
63
63
  "vitest": "^4.1.10"
64
64
  },
@@ -275,6 +275,7 @@ export function registerBuildTool(pi: ExtensionAPI, deps: ToolDeps): void {
275
275
  if (params.action === "list") {
276
276
  output = await runListInContext(deps, args, {
277
277
  signal,
278
+ project: params.project,
278
279
  full: params.full,
279
280
  maxOutputBytes: params.max_bytes,
280
281
  digest: digestBuilds,
@@ -1,3 +1,6 @@
1
+ import { mkdtemp, rm } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
1
4
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
5
  import { Type } from "typebox";
3
6
  import type {
@@ -285,40 +288,57 @@ export function digestProjects(text: string): string | undefined {
285
288
  }
286
289
 
287
290
  /**
288
- * Fetch a whole bounded list payload, then reduce it to a digest unless the
289
- * caller opted into raw output. The internal fetch budget exceeds the
290
- * model-visible cap on purpose: the digest, not the payload, reaches the
291
- * model.
291
+ * Run tod inside a throwaway Git repository whose only remote points at the
292
+ * requested OneDev project. TOD refuses `--project` queries when the working
293
+ * directory has no OneDev remote; the scratch repository supplies one
294
+ * without touching the user's checkout.
292
295
  */
293
- export async function runListInContext(
294
- deps: ToolDeps,
296
+ async function runTodWithScratchRemote(
297
+ exec: CommandExecutor,
295
298
  args: readonly string[],
299
+ serverUrl: string,
300
+ project: string,
301
+ options: { signal?: AbortSignal; maxOutputBytes: number },
302
+ ): Promise<TodOutput> {
303
+ const directory = await mkdtemp(join(tmpdir(), "pi-onedev-scratch-"));
304
+ try {
305
+ for (const gitArgs of [
306
+ ["init", "--quiet", directory],
307
+ [
308
+ "remote",
309
+ "add",
310
+ "origin",
311
+ `${serverUrl.replace(/\/$/, "")}/${project}.git`,
312
+ ],
313
+ ]) {
314
+ const done = await exec("git", gitArgs, { cwd: directory });
315
+ if (done.code !== 0 || done.killed) {
316
+ throw new TodError(
317
+ `could not prepare a temporary OneDev project context: ${(done.stderr || done.stdout).trim() || `git ${gitArgs[0] ?? ""} failed`}`,
318
+ );
319
+ }
320
+ }
321
+ return await runTod(exec, [...args, "--working-dir", directory], {
322
+ signal: options.signal,
323
+ maxOutputBytes: options.maxOutputBytes,
324
+ });
325
+ } finally {
326
+ await rm(directory, { recursive: true, force: true });
327
+ }
328
+ }
329
+
330
+ async function finishListOutput(
331
+ fetch: (maxOutputBytes: number) => Promise<TodOutput>,
296
332
  options: {
297
- signal?: AbortSignal;
298
333
  full?: boolean;
299
334
  maxOutputBytes?: number;
300
335
  digest: (text: string) => string | undefined;
301
336
  },
302
337
  ): Promise<TodOutput> {
303
- const context = deps.context();
304
- if (context.status !== "ready" || !context.project) {
305
- throw new TodError(
306
- context.problem ??
307
- "OneDev tools require a repository whose project can be inferred from a OneDev remote",
308
- );
309
- }
310
338
  if (options.full === true) {
311
- return runTod(deps.exec, args, {
312
- cwd: context.cwd,
313
- signal: options.signal,
314
- maxOutputBytes: modelOutputLimit(options.maxOutputBytes),
315
- });
339
+ return fetch(modelOutputLimit(options.maxOutputBytes));
316
340
  }
317
- const output = await runTod(deps.exec, args, {
318
- cwd: context.cwd,
319
- signal: options.signal,
320
- maxOutputBytes: DIGEST_FETCH_BYTES,
321
- });
341
+ const output = await fetch(DIGEST_FETCH_BYTES);
322
342
  const limit = modelOutputLimit(options.maxOutputBytes);
323
343
  const digested = options.digest(output.text);
324
344
  if (
@@ -341,6 +361,61 @@ export async function runListInContext(
341
361
  };
342
362
  }
343
363
 
364
+ /**
365
+ * Fetch a whole bounded list payload, then reduce it to a digest unless the
366
+ * caller opted into raw output. The internal fetch budget exceeds the
367
+ * model-visible cap on purpose: the digest, not the payload, reaches the
368
+ * model. Without an inferred project, an explicit `project` list query runs
369
+ * against a temporary scratch remote instead of failing.
370
+ */
371
+ export async function runListInContext(
372
+ deps: ToolDeps,
373
+ args: readonly string[],
374
+ options: {
375
+ signal?: AbortSignal;
376
+ full?: boolean;
377
+ maxOutputBytes?: number;
378
+ project?: string;
379
+ digest: (text: string) => string | undefined;
380
+ },
381
+ ): Promise<TodOutput> {
382
+ const context = deps.context();
383
+ if (context.status !== "ready" || !context.project) {
384
+ const scratchServer = context.serverUrl;
385
+ const scratchProject = options.project;
386
+ if (
387
+ context.status !== "no_project" ||
388
+ scratchServer === undefined ||
389
+ scratchProject === undefined
390
+ ) {
391
+ throw new TodError(
392
+ context.problem ??
393
+ "OneDev tools require a repository whose project can be inferred from a OneDev remote",
394
+ );
395
+ }
396
+ return finishListOutput(
397
+ (maxOutputBytes) =>
398
+ runTodWithScratchRemote(
399
+ deps.exec,
400
+ args,
401
+ scratchServer,
402
+ scratchProject,
403
+ { signal: options.signal, maxOutputBytes },
404
+ ),
405
+ options,
406
+ );
407
+ }
408
+ return finishListOutput(
409
+ (maxOutputBytes) =>
410
+ runTod(deps.exec, args, {
411
+ cwd: context.cwd,
412
+ signal: options.signal,
413
+ maxOutputBytes,
414
+ }),
415
+ options,
416
+ );
417
+ }
418
+
344
419
  export function confirmMutation(
345
420
  deps: ToolDeps,
346
421
  ctx: ExtensionContext,
@@ -233,6 +233,7 @@ export function registerIssueTool(pi: ExtensionAPI, deps: ToolDeps): void {
233
233
  params.action === "list"
234
234
  ? await runListInContext(deps, args, {
235
235
  signal,
236
+ project: params.project,
236
237
  full: params.full,
237
238
  maxOutputBytes: params.max_bytes,
238
239
  digest: digestIssues,
package/src/tools/pull.ts CHANGED
@@ -308,6 +308,7 @@ export function registerPullTool(pi: ExtensionAPI, deps: ToolDeps): void {
308
308
  params.action === "list"
309
309
  ? await runListInContext(deps, args, {
310
310
  signal,
311
+ project: params.project,
311
312
  full: params.full,
312
313
  maxOutputBytes: params.max_bytes,
313
314
  digest: digestPulls,