@oh-my-pi/pi-coding-agent 17.0.4 → 17.0.5
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 +86 -43
- package/dist/cli.js +3540 -3521
- package/dist/types/advisor/advise-tool.d.ts +3 -0
- package/dist/types/advisor/runtime.d.ts +7 -1
- package/dist/types/async/job-manager.d.ts +2 -0
- package/dist/types/config/model-registry.d.ts +7 -0
- package/dist/types/config/model-resolver.d.ts +8 -0
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/models-config.d.ts +6 -0
- package/dist/types/dap/client.d.ts +10 -0
- package/dist/types/dap/types.d.ts +6 -5
- package/dist/types/extensibility/extensions/runner.d.ts +4 -2
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +16 -0
- package/dist/types/launch/spawn-options.d.ts +10 -0
- package/dist/types/launch/spawn-options.test.d.ts +1 -0
- package/dist/types/modes/components/status-line/component.d.ts +2 -3
- package/dist/types/modes/components/status-line/types.d.ts +3 -1
- package/dist/types/modes/components/tool-execution.d.ts +2 -0
- package/dist/types/modes/components/transcript-container.d.ts +4 -3
- package/dist/types/modes/components/tree-selector.d.ts +6 -2
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/print-mode.d.ts +4 -0
- package/dist/types/modes/rpc/rpc-input.d.ts +5 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +19 -1
- package/dist/types/session/messages.d.ts +6 -0
- package/dist/types/tools/browser/aria/aria-snapshot.d.ts +17 -7
- package/dist/types/tools/browser/cmux/socket-client.d.ts +2 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +2 -0
- package/dist/types/tools/browser/tab-worker.d.ts +2 -0
- package/dist/types/tools/gh.d.ts +5 -3
- package/dist/types/tools/hub/index.d.ts +1 -1
- package/dist/types/tools/hub/jobs.d.ts +1 -0
- package/dist/types/tools/hub/types.d.ts +2 -0
- package/dist/types/tools/tool-timeouts.d.ts +1 -1
- package/dist/types/utils/git.d.ts +33 -0
- package/dist/types/web/search/providers/codex.d.ts +1 -1
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +150 -0
- package/src/advisor/advise-tool.ts +4 -0
- package/src/advisor/runtime.ts +38 -14
- package/src/async/job-manager.ts +3 -0
- package/src/cli/bench-cli.ts +8 -2
- package/src/cli/dry-balance-cli.ts +1 -0
- package/src/config/model-registry.ts +89 -8
- package/src/config/model-resolver.ts +78 -14
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings.ts +3 -1
- package/src/dap/client.ts +168 -1
- package/src/dap/config.ts +51 -1
- package/src/dap/session.ts +575 -234
- package/src/dap/types.ts +6 -5
- package/src/discovery/agents.ts +2 -2
- package/src/extensibility/extensions/runner.ts +6 -4
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +26 -0
- package/src/launch/broker.ts +34 -31
- package/src/launch/client.ts +7 -1
- package/src/launch/spawn-options.test.ts +31 -0
- package/src/launch/spawn-options.ts +17 -0
- package/src/lsp/types.ts +5 -1
- package/src/main.ts +17 -4
- package/src/mcp/transports/stdio.test.ts +9 -1
- package/src/modes/components/ask-dialog.ts +137 -73
- package/src/modes/components/status-line/component.ts +2 -2
- package/src/modes/components/status-line/segments.ts +20 -3
- package/src/modes/components/status-line/types.ts +3 -1
- package/src/modes/components/tool-execution.ts +5 -0
- package/src/modes/components/transcript-container.ts +23 -122
- package/src/modes/components/tree-selector.ts +10 -4
- package/src/modes/controllers/event-controller.ts +1 -2
- package/src/modes/controllers/input-controller.ts +1 -1
- package/src/modes/controllers/selector-controller.ts +29 -8
- package/src/modes/interactive-mode.ts +40 -8
- package/src/modes/noninteractive-dispose.test.ts +12 -1
- package/src/modes/print-mode.ts +21 -9
- package/src/modes/rpc/rpc-input.ts +38 -0
- package/src/modes/rpc/rpc-mode.ts +7 -2
- package/src/modes/setup-wizard/scenes/sign-in.ts +3 -1
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +3 -2
- package/src/prompts/tools/browser.md +3 -2
- package/src/prompts/tools/debug.md +2 -7
- package/src/prompts/tools/github.md +6 -1
- package/src/prompts/tools/hub.md +4 -2
- package/src/prompts/tools/task-async-contract.md +1 -0
- package/src/prompts/tools/task.md +9 -2
- package/src/sdk.ts +38 -6
- package/src/session/agent-session.ts +395 -162
- package/src/session/messages.test.ts +91 -0
- package/src/session/messages.ts +248 -110
- package/src/slash-commands/builtin-registry.ts +1 -0
- package/src/task/executor.ts +59 -33
- package/src/task/index.ts +46 -9
- package/src/task/worktree.ts +10 -0
- package/src/tools/browser/aria/aria-snapshot.ts +36 -8
- package/src/tools/browser/cmux/cmux-tab.ts +2 -1
- package/src/tools/browser/cmux/socket-client.ts +139 -3
- package/src/tools/browser/run-cancellation.ts +4 -0
- package/src/tools/browser/tab-protocol.ts +2 -0
- package/src/tools/browser/tab-supervisor.ts +21 -11
- package/src/tools/browser/tab-worker.ts +199 -33
- package/src/tools/debug.ts +3 -0
- package/src/tools/gh.ts +40 -2
- package/src/tools/hub/index.ts +4 -1
- package/src/tools/hub/jobs.ts +42 -1
- package/src/tools/hub/types.ts +2 -0
- package/src/tools/tool-timeouts.ts +1 -1
- package/src/utils/git.ts +237 -0
- package/src/web/search/index.ts +9 -5
- package/src/web/search/providers/codex.ts +195 -99
package/src/utils/git.ts
CHANGED
|
@@ -637,6 +637,10 @@ async function readOptionalText(filePath: string): Promise<string | null> {
|
|
|
637
637
|
return retryOnEintr(async () => await Bun.file(filePath).text());
|
|
638
638
|
}
|
|
639
639
|
|
|
640
|
+
async function readOptionalBytes(filePath: string): Promise<Uint8Array | null> {
|
|
641
|
+
return retryOnEintr(async () => await Bun.file(filePath).bytes());
|
|
642
|
+
}
|
|
643
|
+
|
|
640
644
|
function parseGitDirPointer(content: string): string | null {
|
|
641
645
|
const match = /^gitdir:\s*(.+)\s*$/iu.exec(content.trim());
|
|
642
646
|
return match?.[1] ?? null;
|
|
@@ -1379,6 +1383,239 @@ export async function writeTree(cwd: string, options: Pick<CommandOptions, "env"
|
|
|
1379
1383
|
return (await runText(cwd, ["write-tree"], options)).trim();
|
|
1380
1384
|
}
|
|
1381
1385
|
|
|
1386
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
1387
|
+
// API: worktree isolation
|
|
1388
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
1389
|
+
|
|
1390
|
+
/** Outcome of {@link detachGitDir}. */
|
|
1391
|
+
export type DetachGitDirResult =
|
|
1392
|
+
/** `worktreeRoot` had no `.git`; nothing to detach. */
|
|
1393
|
+
| "no-git"
|
|
1394
|
+
/** `.git` already resolves to an independent object DB — left untouched. */
|
|
1395
|
+
| "independent"
|
|
1396
|
+
/** Detached into a standalone repo borrowing `sourceCommonDir`'s objects. */
|
|
1397
|
+
| "detached";
|
|
1398
|
+
|
|
1399
|
+
/**
|
|
1400
|
+
* Sever a copied/mounted working tree from the git metadata it shares with a
|
|
1401
|
+
* source checkout, turning it into a standalone repository that borrows the
|
|
1402
|
+
* source object database through `objects/info/alternates`.
|
|
1403
|
+
*
|
|
1404
|
+
* Isolation backends (reflink/apfs/btrfs/rcopy…) materialise `merged` by
|
|
1405
|
+
* copying `worktreeRoot` byte-for-byte. When `worktreeRoot` is a **linked git
|
|
1406
|
+
* worktree** its `.git` is a pointer file (`gitdir: …/worktrees/<name>`), so
|
|
1407
|
+
* the copy still resolves HEAD/index/refs through the source repo — a task's
|
|
1408
|
+
* `git checkout`/`commit` inside the isolation then mutates the *parent*
|
|
1409
|
+
* checkout. The rcopy `git worktree add` path leaks the other way: task
|
|
1410
|
+
* branches land in the shared ref namespace and stack on each other.
|
|
1411
|
+
*
|
|
1412
|
+
* After detaching, the working tree keeps its files verbatim while:
|
|
1413
|
+
* - HEAD, refs, and the index are frozen to the snapshot at call time;
|
|
1414
|
+
* - all commits/branches the task creates stay private to the isolation;
|
|
1415
|
+
* - objects resolve against `sourceCommonDir` via alternates, so history reads
|
|
1416
|
+
* and later `git fetch <merged>` object transfer keep working;
|
|
1417
|
+
* - the source checkout's HEAD, branch, index, and working tree are untouched.
|
|
1418
|
+
*
|
|
1419
|
+
* A full-copy `.git` (non-worktree source) already owns its object DB and is
|
|
1420
|
+
* returned as `"independent"` without modification. `worktreeRoot` without a
|
|
1421
|
+
* `.git` yields `"no-git"`.
|
|
1422
|
+
*/
|
|
1423
|
+
export async function detachGitDir(worktreeRoot: string, sourceCommonDir: string): Promise<DetachGitDirResult> {
|
|
1424
|
+
ensureAvailable();
|
|
1425
|
+
const gitEntry = path.join(worktreeRoot, ".git");
|
|
1426
|
+
let entryStat: fs.Stats;
|
|
1427
|
+
try {
|
|
1428
|
+
entryStat = await fs.promises.lstat(gitEntry);
|
|
1429
|
+
} catch (err) {
|
|
1430
|
+
if (isEnoent(err)) return "no-git";
|
|
1431
|
+
throw err;
|
|
1432
|
+
}
|
|
1433
|
+
// Canonicalize both sides before comparing: `rev-parse` resolves symlinks
|
|
1434
|
+
// (macOS `/tmp` → `/private/tmp`) while callers derive `sourceCommonDir`
|
|
1435
|
+
// lexically from the session cwd. A lexical mismatch here would silently
|
|
1436
|
+
// classify a shared linked-worktree copy as "independent" and skip the
|
|
1437
|
+
// detach entirely — leaving the parent-mutation leak in place.
|
|
1438
|
+
const parentCommon = await fs.promises.realpath(sourceCommonDir).catch(() => path.resolve(sourceCommonDir));
|
|
1439
|
+
const isoCommonRaw = (
|
|
1440
|
+
await runText(worktreeRoot, ["rev-parse", "--path-format=absolute", "--git-common-dir"], {
|
|
1441
|
+
readOnly: true,
|
|
1442
|
+
})
|
|
1443
|
+
).trim();
|
|
1444
|
+
const isoCommon = await fs.promises.realpath(isoCommonRaw).catch(() => path.resolve(isoCommonRaw));
|
|
1445
|
+
// A full-copy `.git` already resolves to its own object DB — leave it alone.
|
|
1446
|
+
if (isoCommon !== parentCommon) return "independent";
|
|
1447
|
+
|
|
1448
|
+
// Snapshot the state the standalone repo must preserve. HEAD may be a branch
|
|
1449
|
+
// ref (normal checkout), detached, or unborn (a fresh/orphan branch with no
|
|
1450
|
+
// commits — a linked worktree still shares the parent ref namespace, so it
|
|
1451
|
+
// must be severed too). Refs are frozen so `baseSha..branch` ranges and
|
|
1452
|
+
// history reads keep resolving after the source moves on.
|
|
1453
|
+
const headSha = (await tryText(worktreeRoot, ["rev-parse", "HEAD"], { readOnly: true }))?.trim() ?? "";
|
|
1454
|
+
const headRef = (await tryText(worktreeRoot, ["symbolic-ref", "-q", "HEAD"], { readOnly: true }))?.trim() ?? "";
|
|
1455
|
+
const refDump = headSha
|
|
1456
|
+
? (
|
|
1457
|
+
await runText(worktreeRoot, ["for-each-ref", "--format=%(objectname) %(refname)"], {
|
|
1458
|
+
readOnly: true,
|
|
1459
|
+
})
|
|
1460
|
+
).trim()
|
|
1461
|
+
: "";
|
|
1462
|
+
const objectFormat =
|
|
1463
|
+
(await tryText(worktreeRoot, ["rev-parse", "--show-object-format"], { readOnly: true }))?.trim() || "sha1";
|
|
1464
|
+
const userName = await config.get(worktreeRoot, "user.name");
|
|
1465
|
+
const userEmail = await config.get(worktreeRoot, "user.email");
|
|
1466
|
+
|
|
1467
|
+
// Preserve the index verbatim rather than round-tripping through
|
|
1468
|
+
// write-tree/read-tree: the raw index carries skip-worktree bits (sparse
|
|
1469
|
+
// checkout), assume-unchanged flags, and exact stage entries. A rebuilt
|
|
1470
|
+
// index drops skip-worktree, so files intentionally absent from a sparse
|
|
1471
|
+
// working tree would read as deletions and delta capture would apply those
|
|
1472
|
+
// deletions back to the parent. Sparse config + patterns are carried too so
|
|
1473
|
+
// later git operations in the isolation keep honouring the sparse view.
|
|
1474
|
+
const indexPath = (
|
|
1475
|
+
await runText(worktreeRoot, ["rev-parse", "--path-format=absolute", "--git-path", "index"], {
|
|
1476
|
+
readOnly: true,
|
|
1477
|
+
})
|
|
1478
|
+
).trim();
|
|
1479
|
+
const indexBytes = await readOptionalBytes(indexPath);
|
|
1480
|
+
const sparseCheckout = await config.get(worktreeRoot, "core.sparseCheckout");
|
|
1481
|
+
const sparseCone = await config.get(worktreeRoot, "core.sparseCheckoutCone");
|
|
1482
|
+
const sparsePatternPath = (
|
|
1483
|
+
await runText(worktreeRoot, ["rev-parse", "--path-format=absolute", "--git-path", "info/sparse-checkout"], {
|
|
1484
|
+
readOnly: true,
|
|
1485
|
+
})
|
|
1486
|
+
).trim();
|
|
1487
|
+
const sparsePatterns = await readOptionalText(sparsePatternPath);
|
|
1488
|
+
// Status parity with the source: an explicit core.filemode (e.g. false on
|
|
1489
|
+
// mounts ignoring the executable bit) must carry over, or the re-inited
|
|
1490
|
+
// repo's platform default makes clean files read as mode-changed and delta
|
|
1491
|
+
// capture would apply bogus chmod diffs back to the parent.
|
|
1492
|
+
const fileMode = await config.get(worktreeRoot, "core.fileMode");
|
|
1493
|
+
// A split index references sharedindex.* files beside the source index;
|
|
1494
|
+
// restoring the raw index without them makes every git read fail. Carry the
|
|
1495
|
+
// shared files (and the config) alongside the verbatim index bytes.
|
|
1496
|
+
const splitIndex = await config.get(worktreeRoot, "core.splitIndex");
|
|
1497
|
+
const sharedIndexFiles: Array<{ name: string; bytes: Uint8Array }> = [];
|
|
1498
|
+
if (indexBytes) {
|
|
1499
|
+
const indexDir = path.dirname(indexPath);
|
|
1500
|
+
let entries: string[] = [];
|
|
1501
|
+
try {
|
|
1502
|
+
entries = await fs.promises.readdir(indexDir);
|
|
1503
|
+
} catch {}
|
|
1504
|
+
for (const name of entries) {
|
|
1505
|
+
if (!name.startsWith("sharedindex.")) continue;
|
|
1506
|
+
const bytes = await readOptionalBytes(path.join(indexDir, name));
|
|
1507
|
+
if (bytes) sharedIndexFiles.push({ name, bytes });
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
// A shallow source deliberately lacks parents beyond its `shallow` boundary
|
|
1511
|
+
// file; without it, history traversal over the borrowed objects treats the
|
|
1512
|
+
// boundary commit's missing parent as corruption.
|
|
1513
|
+
const shallowBoundary = await readOptionalText(path.join(parentCommon, "shallow"));
|
|
1514
|
+
|
|
1515
|
+
// A pointer `.git` file whose worktree-admin dir back-references this exact
|
|
1516
|
+
// tree is the rcopy `git worktree add` registration. Remove that admin entry
|
|
1517
|
+
// so the source repo's worktree list stops tracking the isolation. A pointer
|
|
1518
|
+
// referencing the *source's* admin (a copied linked-worktree `.git`) is not
|
|
1519
|
+
// ours to delete — only the local pointer file is discarded. Compare via
|
|
1520
|
+
// realpath: git canonicalizes the back-reference (e.g. macOS `/var` →
|
|
1521
|
+
// `/private/var`), so a lexical path comparison would miss the match and
|
|
1522
|
+
// leave a stale registration in the source repo's worktree list.
|
|
1523
|
+
let ownWorktreeAdmin: string | undefined;
|
|
1524
|
+
if (entryStat.isFile()) {
|
|
1525
|
+
const pointer = parseGitDirPointer((await readOptionalText(gitEntry)) ?? "");
|
|
1526
|
+
if (pointer) {
|
|
1527
|
+
const adminDir = path.resolve(path.dirname(gitEntry), pointer);
|
|
1528
|
+
const backRef = (await readOptionalText(path.join(adminDir, "gitdir")))?.trim();
|
|
1529
|
+
if (backRef) {
|
|
1530
|
+
const [realBackRef, realGitEntry] = await Promise.all([
|
|
1531
|
+
fs.promises.realpath(backRef).catch(() => path.resolve(backRef)),
|
|
1532
|
+
fs.promises.realpath(gitEntry).catch(() => path.resolve(gitEntry)),
|
|
1533
|
+
]);
|
|
1534
|
+
if (realBackRef === realGitEntry) ownWorktreeAdmin = adminDir;
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
await fs.promises.rm(gitEntry, { recursive: true, force: true });
|
|
1540
|
+
if (ownWorktreeAdmin) await fs.promises.rm(ownWorktreeAdmin, { recursive: true, force: true });
|
|
1541
|
+
|
|
1542
|
+
// Preserve the checked-out branch name so an unborn HEAD (fresh/orphan
|
|
1543
|
+
// branch with no commits) keeps its symbolic ref after `init` rather than
|
|
1544
|
+
// snapping to the init default; born HEADs get the ref rewritten below anyway.
|
|
1545
|
+
const initArgs = ["init", "--object-format", objectFormat, "-q"];
|
|
1546
|
+
const initialBranch = headRef.startsWith(LOCAL_BRANCH_PREFIX) ? headRef.slice(LOCAL_BRANCH_PREFIX.length) : "";
|
|
1547
|
+
if (initialBranch) initArgs.push("-b", initialBranch);
|
|
1548
|
+
await runEffect(worktreeRoot, initArgs);
|
|
1549
|
+
const objectsInfo = path.join(gitEntry, "objects", "info");
|
|
1550
|
+
await fs.promises.mkdir(objectsInfo, { recursive: true });
|
|
1551
|
+
const alternates = [path.join(parentCommon, "objects")];
|
|
1552
|
+
const chained = await readOptionalText(path.join(parentCommon, "objects", "info", "alternates"));
|
|
1553
|
+
if (chained) {
|
|
1554
|
+
for (const line of chained.split("\n")) {
|
|
1555
|
+
const entry = line.trim();
|
|
1556
|
+
if (!entry) continue;
|
|
1557
|
+
alternates.push(path.isAbsolute(entry) ? entry : path.resolve(parentCommon, "objects", entry));
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
await Bun.write(path.join(objectsInfo, "alternates"), `${alternates.join("\n")}\n`);
|
|
1561
|
+
|
|
1562
|
+
// Freeze refs when HEAD is born. Point HEAD at the raw SHA first so
|
|
1563
|
+
// `update-ref` writes land even for the branch HEAD currently names, then
|
|
1564
|
+
// restore the symbolic HEAD. An unborn HEAD has no refs to freeze; `init -b`
|
|
1565
|
+
// above already set the symbolic HEAD to the unborn branch.
|
|
1566
|
+
if (headSha) {
|
|
1567
|
+
await Bun.write(path.join(gitEntry, "HEAD"), `${headSha}\n`);
|
|
1568
|
+
if (refDump) {
|
|
1569
|
+
const commands = refDump
|
|
1570
|
+
.split("\n")
|
|
1571
|
+
.filter(Boolean)
|
|
1572
|
+
.map(line => {
|
|
1573
|
+
const sep = line.indexOf(" ");
|
|
1574
|
+
return `create ${line.slice(sep + 1)} ${line.slice(0, sep)}`;
|
|
1575
|
+
})
|
|
1576
|
+
.join("\n");
|
|
1577
|
+
await runEffect(worktreeRoot, ["update-ref", "--stdin"], { stdin: `${commands}\n` });
|
|
1578
|
+
}
|
|
1579
|
+
if (headRef) await Bun.write(path.join(gitEntry, "HEAD"), `ref: ${headRef}\n`);
|
|
1580
|
+
} else if (headRef && !initialBranch) {
|
|
1581
|
+
// Unborn detached HEAD (no branch, no commit) — restore the raw ref target.
|
|
1582
|
+
await Bun.write(path.join(gitEntry, "HEAD"), `ref: ${headRef}\n`);
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
// Carry the source identity so isolated commits have an author.
|
|
1586
|
+
if (userName) await config.set(worktreeRoot, "user.name", userName);
|
|
1587
|
+
if (userEmail) await config.set(worktreeRoot, "user.email", userEmail);
|
|
1588
|
+
if (fileMode !== undefined) await config.set(worktreeRoot, "core.fileMode", fileMode);
|
|
1589
|
+
if (splitIndex !== undefined) await config.set(worktreeRoot, "core.splitIndex", splitIndex);
|
|
1590
|
+
// Preserve the shallow boundary so history traversal over the borrowed
|
|
1591
|
+
// object DB stops at the boundary instead of failing on missing parents.
|
|
1592
|
+
if (shallowBoundary !== null) await Bun.write(path.join(gitEntry, "shallow"), shallowBoundary);
|
|
1593
|
+
|
|
1594
|
+
// Restore sparse-checkout state before the index so skip-worktree entries
|
|
1595
|
+
// keep resolving against the carried patterns.
|
|
1596
|
+
if (sparseCheckout) await config.set(worktreeRoot, "core.sparseCheckout", sparseCheckout);
|
|
1597
|
+
if (sparseCone) await config.set(worktreeRoot, "core.sparseCheckoutCone", sparseCone);
|
|
1598
|
+
if (sparsePatterns !== null) {
|
|
1599
|
+
const infoDir = path.join(gitEntry, "info");
|
|
1600
|
+
await fs.promises.mkdir(infoDir, { recursive: true });
|
|
1601
|
+
await Bun.write(path.join(infoDir, "sparse-checkout"), sparsePatterns);
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// Restore the index verbatim (skip-worktree, assume-unchanged, exact stage
|
|
1605
|
+
// entries) so the working tree's dirty set — including sparse-excluded files
|
|
1606
|
+
// — matches the source. Fall back to rebuilding from HEAD only when the
|
|
1607
|
+
// source had no index (a bare-ish/never-staged checkout).
|
|
1608
|
+
if (indexBytes) {
|
|
1609
|
+
for (const shared of sharedIndexFiles) {
|
|
1610
|
+
await Bun.write(path.join(gitEntry, shared.name), shared.bytes);
|
|
1611
|
+
}
|
|
1612
|
+
await Bun.write(path.join(gitEntry, "index"), indexBytes);
|
|
1613
|
+
} else if (headSha) {
|
|
1614
|
+
await readTree(worktreeRoot, headSha);
|
|
1615
|
+
}
|
|
1616
|
+
return "detached";
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1382
1619
|
// ════════════════════════════════════════════════════════════════════════════
|
|
1383
1620
|
// API: show
|
|
1384
1621
|
// ════════════════════════════════════════════════════════════════════════════
|
package/src/web/search/index.ts
CHANGED
|
@@ -134,10 +134,7 @@ async function executeSearch(
|
|
|
134
134
|
const explicitProvider = params.provider;
|
|
135
135
|
let candidates: SearchProviderCandidate[];
|
|
136
136
|
if (explicitProvider && explicitProvider !== "auto") {
|
|
137
|
-
|
|
138
|
-
candidates = (await provider.isExplicitlyAvailable(authStorage))
|
|
139
|
-
? [{ id: explicitProvider, explicit: true }]
|
|
140
|
-
: resolveProviderCandidates("auto");
|
|
137
|
+
candidates = [{ id: explicitProvider, explicit: true }];
|
|
141
138
|
} else if (explicitProvider === "auto") {
|
|
142
139
|
// Explicit `--provider auto` bypasses the configured preferred provider
|
|
143
140
|
// for this invocation; exclusions still apply.
|
|
@@ -175,7 +172,13 @@ async function executeSearch(
|
|
|
175
172
|
const available = candidate.explicit
|
|
176
173
|
? await provider.isExplicitlyAvailable(authStorage)
|
|
177
174
|
: await provider.isAvailable(authStorage);
|
|
178
|
-
if (!available) continue;
|
|
175
|
+
if (!available && !candidate.explicit) continue;
|
|
176
|
+
if (!available && candidate.explicit) {
|
|
177
|
+
throw new SearchProviderError(
|
|
178
|
+
provider.id,
|
|
179
|
+
`${provider.label} web search is unavailable. Configure its credentials or select the automatic provider chain.`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
179
182
|
availableProviderCount++;
|
|
180
183
|
lastProvider = provider;
|
|
181
184
|
|
|
@@ -213,6 +216,7 @@ async function executeSearch(
|
|
|
213
216
|
// summary error), masking the cancellation.
|
|
214
217
|
throwIfAborted(signal);
|
|
215
218
|
failures.push({ provider: provider ?? providerMeta, error });
|
|
219
|
+
if (candidate.explicit) break;
|
|
216
220
|
}
|
|
217
221
|
}
|
|
218
222
|
|