@gavin-hu/my-pi-agent 0.18.0 → 0.20.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 +66 -52
- package/README.zh-CN.md +56 -46
- package/docs/extensions/README.md +34 -0
- package/docs/extensions/board.md +507 -0
- package/{extensions/bytefly/README.md → docs/extensions/bytefly.md} +774 -574
- package/{extensions/check/README.md → docs/extensions/check.md} +83 -68
- package/{extensions/context/README.md → docs/extensions/context.md} +40 -23
- package/docs/extensions/file-browse.md +561 -0
- package/{extensions/file-open/README.md → docs/extensions/file-open.md} +31 -23
- package/{extensions/file-read/README.md → docs/extensions/file-read.md} +47 -55
- package/{extensions/goal/README.md → docs/extensions/goal.md} +115 -131
- package/{extensions/job/README.md → docs/extensions/job.md} +62 -52
- package/{extensions/memory/README.md → docs/extensions/memory.md} +71 -60
- package/{extensions/plan/README.md → docs/extensions/plan.md} +440 -322
- package/{extensions/question/README.md → docs/extensions/question.md} +54 -31
- package/{extensions/rewind/README.md → docs/extensions/rewind.md} +115 -122
- package/docs/extensions/session-bus.md +298 -0
- package/{extensions/subagent/README.md → docs/extensions/subagent.md} +165 -118
- package/docs/extensions/team.md +254 -0
- package/{extensions/todo/README.md → docs/extensions/todo.md} +85 -88
- package/{extensions/watch/README.md → docs/extensions/watch.md} +54 -36
- package/{extensions/web-access/README.md → docs/extensions/web-access.md} +93 -50
- package/{extensions/wechat/README.md → docs/extensions/wechat.md} +260 -163
- package/{extensions/worktree/README.md → docs/extensions/worktree.md} +137 -79
- package/extensions/board/commands.ts +105 -0
- package/extensions/board/config.ts +36 -0
- package/extensions/board/format.ts +170 -0
- package/extensions/board/index.ts +40 -0
- package/extensions/board/input.ts +54 -0
- package/extensions/board/join.ts +42 -0
- package/extensions/board/page.ts +388 -0
- package/extensions/board/result.ts +108 -0
- package/extensions/board/rows.ts +48 -0
- package/extensions/board/schema.ts +90 -0
- package/extensions/board/seat.ts +28 -0
- package/extensions/board/snapshot.ts +42 -0
- package/extensions/board/tools.ts +444 -0
- package/extensions/board/transport/router.ts +91 -0
- package/extensions/board/transport/server.ts +138 -0
- package/extensions/board/types.ts +121 -0
- package/extensions/board/view/client.ts +51 -0
- package/extensions/board/view/dialog.ts +43 -0
- package/extensions/board/view/keys.ts +125 -0
- package/extensions/board/view/live.ts +231 -0
- package/extensions/board/view/names.ts +91 -0
- package/extensions/board/view/page.ts +118 -0
- package/extensions/board/view/render.ts +519 -0
- package/extensions/board/view/styles.ts +228 -0
- package/extensions/board/view/team.ts +327 -0
- package/extensions/bytefly/footer/budget.ts +6 -5
- package/extensions/bytefly/footer/collect/entries.ts +12 -4
- package/extensions/bytefly/footer/collect/snapshot.ts +8 -17
- package/extensions/bytefly/footer/config.ts +19 -8
- package/extensions/bytefly/footer/price/price.ts +30 -8
- package/extensions/bytefly/footer/register.ts +5 -5
- package/extensions/bytefly/footer/render/format.ts +18 -0
- package/extensions/bytefly/footer/render/lines.ts +23 -7
- package/extensions/bytefly/footer/render/segments.ts +117 -31
- package/extensions/bytefly/footer/types.ts +29 -5
- package/extensions/bytefly/footer/unpriced.ts +42 -17
- package/extensions/bytefly/panel/commands.ts +67 -7
- package/extensions/bytefly/panel/filter.ts +68 -0
- package/extensions/bytefly/panel/format.ts +16 -15
- package/extensions/bytefly/panel/join.ts +56 -13
- package/extensions/bytefly/panel/prices.ts +43 -0
- package/extensions/bytefly/panel/settings-runtime.ts +0 -6
- package/extensions/bytefly/panel/settings.ts +75 -43
- package/extensions/bytefly/panel/tui/entries.ts +26 -8
- package/extensions/bytefly/panel/tui/frame.ts +130 -93
- package/extensions/bytefly/panel/tui/keys.ts +352 -0
- package/extensions/bytefly/panel/tui/list-view.ts +25 -46
- package/extensions/bytefly/panel/tui/panes.ts +76 -0
- package/extensions/bytefly/panel/tui/section-view.ts +126 -0
- package/extensions/bytefly/panel/tui/settings-section.ts +54 -125
- package/extensions/bytefly/panel/tui/settings-view.ts +30 -131
- package/extensions/bytefly/panel/tui/side-pane.ts +224 -0
- package/extensions/bytefly/panel/tui.ts +306 -259
- package/extensions/bytefly/picker/discovery.ts +0 -2
- package/extensions/bytefly/picker/types.ts +0 -2
- package/extensions/bytefly/update/check.ts +39 -0
- package/extensions/bytefly/update/format.ts +15 -0
- package/extensions/bytefly/update/register.ts +73 -34
- package/extensions/bytefly/update/state.ts +6 -3
- package/extensions/check/commands.ts +2 -1
- package/extensions/context/tui.ts +1 -1
- package/extensions/file-browse/{config.ts → config/config.ts} +1 -1
- package/extensions/file-browse/files/classify.ts +136 -0
- package/extensions/file-browse/files/files.ts +12 -0
- package/extensions/file-browse/files/index.ts +90 -0
- package/extensions/file-browse/files/list.ts +261 -0
- package/extensions/file-browse/{paths.ts → files/paths.ts} +1 -1
- package/extensions/file-browse/files/tree.ts +101 -0
- package/extensions/file-browse/{git.ts → git/git.ts} +10 -3
- package/extensions/file-browse/index.ts +4 -4
- package/extensions/file-browse/{live.ts → live/live.ts} +20 -69
- package/extensions/file-browse/{router.ts → transport/router.ts} +21 -17
- package/extensions/file-browse/transport/server.ts +110 -0
- package/extensions/file-browse/{chrome.ts → view/chrome.ts} +4 -4
- package/extensions/file-browse/{client.ts → view/client.ts} +1 -1
- package/extensions/file-browse/{diff-html.ts → view/diff-html.ts} +3 -3
- package/extensions/file-browse/view/emit.ts +51 -0
- package/extensions/file-browse/{html.ts → view/html.ts} +19 -18
- package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -1
- package/extensions/file-browse/{markdown.ts → view/markdown.ts} +18 -20
- package/extensions/file-browse/{raw-html.ts → view/raw-html.ts} +19 -14
- package/extensions/file-browse/{styles.ts → view/styles.ts} +29 -5
- package/extensions/file-browse/{tree.ts → view/tree.ts} +1 -1
- package/extensions/file-browse/{urls.ts → view/urls.ts} +1 -1
- package/extensions/file-read/schema.ts +2 -1
- package/extensions/job/runtime.ts +4 -4
- package/extensions/memory/store.ts +12 -5
- package/extensions/plan/blocks.ts +23 -2
- package/extensions/plan/commands.ts +21 -16
- package/extensions/plan/feedback.ts +185 -52
- package/extensions/plan/format.ts +10 -3
- package/extensions/plan/index.ts +23 -5
- package/extensions/plan/plan-files.ts +88 -0
- package/extensions/plan/plan-index.ts +43 -9
- package/extensions/plan/plan-text.ts +33 -23
- package/extensions/plan/plans.ts +27 -34
- package/extensions/plan/policy.ts +30 -17
- package/extensions/plan/review.ts +3 -27
- package/extensions/plan/runtime.ts +52 -11
- package/extensions/plan/sections.ts +112 -0
- package/extensions/plan/steps.ts +3 -22
- package/extensions/plan/todos.ts +51 -3
- package/extensions/plan/tools/exit.ts +20 -95
- package/extensions/plan/tools/todo-seed.ts +4 -2
- package/extensions/plan/tools/write.ts +12 -1
- package/extensions/plan/tui.ts +5 -34
- package/extensions/plan/types.ts +15 -8
- package/extensions/plan/web-blocks.ts +89 -0
- package/extensions/plan/web-chrome.ts +3 -5
- package/extensions/plan/web-client.ts +179 -330
- package/extensions/plan/web-collab.ts +3 -3
- package/extensions/plan/web-comments.ts +18 -8
- package/extensions/plan/web-composer.ts +80 -0
- package/extensions/plan/web-draft.ts +40 -0
- package/extensions/plan/web-keys.ts +134 -0
- package/extensions/plan/web-outline.ts +192 -0
- package/extensions/plan/web-page.ts +4 -8
- package/extensions/plan/web-plan.ts +150 -0
- package/extensions/plan/web-router.ts +3 -58
- package/extensions/plan/web-settle.ts +194 -0
- package/extensions/plan/web-share-session.ts +24 -30
- package/extensions/plan/web-streams.ts +14 -14
- package/extensions/plan/web-tokens.ts +2 -2
- package/extensions/plan/web.ts +66 -120
- package/extensions/rewind/{commands.ts → commands/commands.ts} +5 -5
- package/extensions/rewind/{review.ts → commands/review.ts} +15 -14
- package/extensions/rewind/{rewind.ts → commands/rewind.ts} +6 -6
- package/extensions/rewind/{config.ts → config/config.ts} +1 -1
- package/extensions/rewind/index.ts +3 -3
- package/extensions/rewind/{policy.ts → state/policy.ts} +4 -4
- package/extensions/rewind/{runtime.ts → state/runtime.ts} +11 -11
- package/extensions/rewind/{snapshot-run.ts → state/snapshot-run.ts} +1 -1
- package/extensions/rewind/{timeline.ts → state/timeline.ts} +2 -2
- package/extensions/rewind/{format.ts → text/format.ts} +3 -3
- package/extensions/rewind/{git.ts → transport/git.ts} +2 -2
- package/extensions/rewind/{restore.ts → transport/restore.ts} +2 -2
- package/extensions/rewind/{snapshot.ts → transport/snapshot.ts} +1 -1
- package/extensions/rewind/{store.ts → transport/store.ts} +1 -1
- package/extensions/rewind/{checklist.ts → view/checklist.ts} +6 -6
- package/extensions/rewind/{hunk-picker.ts → view/hunk-picker.ts} +8 -8
- package/extensions/rewind/{path-picker.ts → view/path-picker.ts} +7 -7
- package/extensions/rewind/{review-tui.ts → view/review-tui.ts} +11 -11
- package/extensions/rewind/{rewind-tui.ts → view/rewind-tui.ts} +17 -11
- package/extensions/rewind/{status.ts → view/status.ts} +4 -4
- package/extensions/session-bus/commands.ts +17 -69
- package/extensions/session-bus/format.ts +22 -45
- package/extensions/session-bus/index.ts +13 -7
- package/extensions/session-bus/join-request.ts +40 -0
- package/extensions/session-bus/mail-watch.ts +134 -0
- package/extensions/session-bus/notice.ts +32 -4
- package/{lib/claims → extensions/session-bus}/overlap.ts +11 -18
- package/extensions/session-bus/protocol.ts +19 -49
- package/extensions/session-bus/resolve.ts +67 -0
- package/extensions/session-bus/result.ts +16 -51
- package/extensions/session-bus/rows.ts +11 -5
- package/extensions/session-bus/runtime.ts +220 -267
- package/extensions/session-bus/store.ts +55 -7
- package/extensions/session-bus/tools.ts +31 -66
- package/extensions/session-bus/tui.ts +5 -5
- package/extensions/session-bus/types.ts +23 -14
- package/extensions/subagent/agents.ts +9 -8
- package/extensions/subagent/commands.ts +2 -1
- package/extensions/subagent/invocation.ts +4 -2
- package/extensions/team/commands.ts +145 -0
- package/extensions/team/dispatch.ts +133 -0
- package/extensions/team/format.ts +119 -0
- package/extensions/team/index.ts +34 -0
- package/extensions/team/schema.ts +29 -0
- package/extensions/team/tools.ts +267 -0
- package/extensions/team/types.ts +65 -0
- package/extensions/todo/index.ts +4 -1
- package/extensions/todo/nudge.ts +8 -2
- package/extensions/web-access/fetch/page.ts +4 -1
- package/extensions/web-access/fetch/structured.ts +447 -0
- package/extensions/web-access/fetch/tool.ts +2 -1
- package/extensions/web-access/search/tool.ts +2 -1
- package/extensions/wechat/bridge.ts +106 -251
- package/extensions/wechat/{commands.ts → commands/commands.ts} +26 -18
- package/extensions/wechat/{doctor.ts → commands/doctor.ts} +5 -5
- package/extensions/wechat/{history.ts → commands/history.ts} +1 -1
- package/extensions/wechat/{login.ts → commands/login.ts} +3 -3
- package/extensions/wechat/{slash.ts → commands/slash.ts} +38 -2
- package/extensions/wechat/dialog/channels.ts +136 -0
- package/extensions/wechat/{interaction.ts → dialog/interaction.ts} +37 -37
- package/extensions/wechat/{prompt.ts → dialog/prompt.ts} +2 -2
- package/extensions/wechat/{remote-ui.ts → dialog/remote-ui.ts} +1 -1
- package/extensions/wechat/index.ts +10 -10
- package/extensions/wechat/intake.ts +245 -0
- package/extensions/wechat/{admission.ts → peer/admission.ts} +2 -2
- package/extensions/wechat/{peer-session.ts → peer/peer-session.ts} +2 -2
- package/extensions/wechat/{peer-worker.ts → peer/peer-worker.ts} +100 -22
- package/extensions/wechat/{presence.ts → peer/presence.ts} +8 -7
- package/extensions/wechat/{reply-pipeline.ts → peer/reply-pipeline.ts} +1 -1
- package/extensions/wechat/{credentials.ts → state/credentials.ts} +2 -2
- package/extensions/wechat/{state.ts → state/state.ts} +14 -2
- package/extensions/wechat/{format.ts → text/format.ts} +3 -3
- package/extensions/wechat/{notices.ts → text/notices.ts} +8 -0
- package/extensions/wechat/tools.ts +29 -12
- package/extensions/wechat/{client.ts → transport/client.ts} +26 -2
- package/extensions/wechat/{deliver.ts → transport/deliver.ts} +72 -28
- package/extensions/wechat/{inbound.ts → transport/inbound.ts} +5 -5
- package/extensions/wechat/{media.ts → transport/media.ts} +11 -5
- package/extensions/wechat/transport/outbound.ts +142 -0
- package/extensions/wechat/{poll.ts → transport/poll.ts} +3 -3
- package/extensions/wechat/types.ts +10 -1
- package/extensions/wechat/{login-screen.ts → view/login-screen.ts} +4 -4
- package/extensions/wechat/{status-screen.ts → view/status-screen.ts} +31 -26
- package/extensions/worktree/commands.ts +24 -17
- package/extensions/worktree/exit.ts +12 -6
- package/extensions/worktree/git.ts +22 -14
- package/extensions/worktree/guard.ts +18 -290
- package/extensions/worktree/handoff.ts +10 -2
- package/extensions/worktree/index.ts +2 -1
- package/extensions/worktree/lifecycle.ts +2 -0
- package/extensions/worktree/registry.ts +20 -11
- package/extensions/worktree/root-tools.ts +13 -3
- package/extensions/worktree/runtime.ts +8 -2
- package/extensions/worktree/screen.ts +15 -8
- package/extensions/worktree/shell.ts +303 -0
- package/extensions/worktree/state.ts +1 -1
- package/extensions/worktree/status.ts +42 -9
- package/extensions/worktree/tools.ts +19 -13
- package/lib/README.md +11 -8
- package/lib/board/card.ts +479 -0
- package/lib/board/links.ts +141 -0
- package/lib/board/page-pointer.ts +310 -0
- package/lib/board/round-text.ts +81 -0
- package/lib/board/round.ts +94 -0
- package/lib/board/rules.ts +437 -0
- package/lib/board/session.ts +105 -0
- package/lib/board/store.ts +497 -0
- package/lib/board/view.ts +234 -0
- package/lib/errors.ts +12 -4
- package/lib/format.ts +35 -0
- package/lib/http/guard.ts +112 -0
- package/lib/http/index.ts +16 -5
- package/lib/http/serve.ts +237 -0
- package/lib/http/server.ts +12 -10
- package/lib/http/sink.ts +135 -0
- package/lib/http/token.ts +38 -0
- package/lib/plan-entry.ts +11 -0
- package/lib/policy.ts +25 -2
- package/lib/requirements.ts +6 -7
- package/lib/session-label.ts +64 -0
- package/lib/session-roster.ts +201 -0
- package/lib/session-tool.ts +184 -0
- package/lib/spawn.ts +37 -3
- package/lib/stray-fields.ts +45 -0
- package/lib/tool-names.ts +38 -2
- package/lib/tui/glyphs.ts +3 -2
- package/lib/web/README.md +57 -4
- package/lib/web/client-runtime.ts +123 -0
- package/lib/web/components.ts +5 -0
- package/lib/web/index.ts +4 -2
- package/lib/web/markup.ts +88 -1
- package/lib/web/names.ts +42 -0
- package/lib/web/runtime.ts +7 -1
- package/lib/web/tokens.ts +25 -2
- package/package.json +26 -14
- package/skills/next/SKILL.md +4 -3
- package/skills/project-docs/SKILL.md +133 -0
- package/skills/project-docs/references/audit.md +131 -0
- package/skills/project-docs/references/changelog.md +105 -0
- package/skills/project-docs/references/discovery.md +127 -0
- package/skills/project-docs/references/readme.md +80 -0
- package/skills/project-docs/references/release.md +84 -0
- package/skills/project-docs/references/roadmap.md +93 -0
- package/skills/project-docs/scripts/state.ts +316 -0
- package/skills/refactor/SKILL.md +100 -125
- package/skills/refactor/references/handoff.md +35 -0
- package/skills/refactor/references/signals.md +54 -0
- package/skills/refactor/scripts/inventory.ts +587 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +0 -40
- package/extensions/bytefly/panel/tui/skills-view.ts +0 -50
- package/extensions/file-browse/README.md +0 -506
- package/extensions/file-browse/files.ts +0 -564
- package/extensions/file-browse/server.ts +0 -141
- package/extensions/job/process.ts +0 -35
- package/extensions/plan/split.ts +0 -87
- package/extensions/session-bus/README.md +0 -247
- package/extensions/session-bus/team.ts +0 -102
- package/extensions/wechat/outbound.ts +0 -100
- package/lib/plans/sections.ts +0 -172
- package/lib/plans/setting.ts +0 -52
- package/lib/plans/split.ts +0 -215
- package/lib/plans/store.ts +0 -19
- package/lib/plans/team.ts +0 -151
- package/skills/changelog/SKILL.md +0 -148
- package/skills/parallel-dev/SKILL.md +0 -150
- /package/extensions/file-browse/{rev.ts → live/rev.ts} +0 -0
- /package/extensions/file-browse/{diff.ts → text/diff.ts} +0 -0
- /package/extensions/file-browse/{fuzzy.ts → text/fuzzy.ts} +0 -0
- /package/extensions/file-browse/{live-client.ts → view/live-client.ts} +0 -0
- /package/extensions/file-browse/{palette.ts → view/palette.ts} +0 -0
- /package/extensions/rewind/{patch.ts → text/patch.ts} +0 -0
- /package/extensions/rewind/{paths.ts → text/paths.ts} +0 -0
- /package/extensions/rewind/{queue.ts → transport/queue.ts} +0 -0
- /package/extensions/rewind/{temp-index.ts → transport/temp-index.ts} +0 -0
- /package/extensions/wechat/{lock.ts → state/lock.ts} +0 -0
- /package/extensions/wechat/{channel.ts → text/channel.ts} +0 -0
|
@@ -46,8 +46,6 @@ export function createSettingsStore(manager: SettingsManager): SettingsStore {
|
|
|
46
46
|
setProjectSkillPaths: (paths) => manager.setProjectSkillPaths(paths),
|
|
47
47
|
setProjectPackages: (packages) => manager.setProjectPackages(packages),
|
|
48
48
|
setQuietStartup: (quiet) => manager.setQuietStartup(quiet),
|
|
49
|
-
setOutputPad: (padding) => manager.setOutputPad(padding),
|
|
50
|
-
setShowHardwareCursor: (enabled) => manager.setShowHardwareCursor(enabled),
|
|
51
49
|
setTheme: (theme) => manager.setTheme(theme),
|
|
52
50
|
isProjectTrusted: () => manager.isProjectTrusted(),
|
|
53
51
|
flush: () => manager.flush(),
|
|
@@ -47,8 +47,6 @@ export interface SettingsStore {
|
|
|
47
47
|
setProjectPackages(packages: PackageSource[]): void;
|
|
48
48
|
/** App preferences are written to the personal settings file. */
|
|
49
49
|
setQuietStartup(quiet: QuietStartup): void;
|
|
50
|
-
setOutputPad(padding: 0 | 1): void;
|
|
51
|
-
setShowHardwareCursor(enabled: boolean): void;
|
|
52
50
|
/** Pi's theme setting is a personal-file preference. */
|
|
53
51
|
setTheme(theme: string): void;
|
|
54
52
|
isProjectTrusted(): boolean;
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { DefaultPackageManager, getAgentDir, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
14
|
+
import type { ProgressEvent } from "@earendil-works/pi-coding-agent";
|
|
14
15
|
import type { PackageUpdate, PackageUpdateCheck } from "./types.ts";
|
|
15
16
|
import { parseVersion } from "./version.ts";
|
|
16
17
|
|
|
@@ -51,6 +52,44 @@ export interface AvailableUpdatesDeps {
|
|
|
51
52
|
checkForAvailableUpdates?: PackageUpdateCheck;
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
/** What applying an update needs: the same manager the check builds, plus a source. */
|
|
56
|
+
export interface ApplyUpdatesDeps {
|
|
57
|
+
/** Session working directory. */
|
|
58
|
+
cwd: string;
|
|
59
|
+
/** Pi agent directory; defaults to `getAgentDir()`. */
|
|
60
|
+
agentDir?: string;
|
|
61
|
+
/** Whether project-local packages may be read. */
|
|
62
|
+
projectTrusted: boolean;
|
|
63
|
+
/** One source to update; every configured package when omitted. */
|
|
64
|
+
source?: string;
|
|
65
|
+
/** Called as Pi's manager reports progress, so a surface can show one line. */
|
|
66
|
+
onProgress?: (message: string) => void;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Apply the update through Pi's own package manager.
|
|
71
|
+
*
|
|
72
|
+
* This is deliberately not a shell: `pi update --extensions` is a command a human
|
|
73
|
+
* types, while `update()` is the manager that owns the install root, the settings
|
|
74
|
+
* that name each package, and the npm command a project configured. It resolves
|
|
75
|
+
* nothing interactively, honors `PI_OFFLINE` by returning early, and skips a
|
|
76
|
+
* pinned npm version — so the caller's job is the confirmation and the report, not
|
|
77
|
+
* the mechanics. It throws when a named source matches no configured package or
|
|
78
|
+
* when the install command fails, and returns nothing on success.
|
|
79
|
+
*/
|
|
80
|
+
export async function applyUpdates(deps: ApplyUpdatesDeps): Promise<void> {
|
|
81
|
+
const agentDir = deps.agentDir ?? getAgentDir();
|
|
82
|
+
const settingsManager = SettingsManager.create(deps.cwd, agentDir, { projectTrusted: deps.projectTrusted });
|
|
83
|
+
const manager = new DefaultPackageManager({ cwd: deps.cwd, agentDir, settingsManager });
|
|
84
|
+
const report = deps.onProgress;
|
|
85
|
+
if (report) {
|
|
86
|
+
manager.setProgressCallback((event: ProgressEvent) => {
|
|
87
|
+
if (event.message) report(event.message);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
await manager.update(deps.source);
|
|
91
|
+
}
|
|
92
|
+
|
|
54
93
|
/**
|
|
55
94
|
* Every package Pi would update, or `undefined` when the check cannot run.
|
|
56
95
|
*
|
|
@@ -174,6 +174,21 @@ export function updateOutcomeText(
|
|
|
174
174
|
};
|
|
175
175
|
case "current":
|
|
176
176
|
return { text: "bytefly: up to date — no configured package has an update.", severity: "info" };
|
|
177
|
+
case "update-failed":
|
|
178
|
+
return {
|
|
179
|
+
text: `bytefly: the update did not finish — ${sanitizeLine(outcome.reason)}`,
|
|
180
|
+
severity: "warning",
|
|
181
|
+
};
|
|
182
|
+
case "updated":
|
|
183
|
+
return outcome.pending
|
|
184
|
+
? {
|
|
185
|
+
text: `bytefly: the update ran, but a package is still behind — run ${UPDATE_COMMAND} or /bytefly update.`,
|
|
186
|
+
severity: "warning",
|
|
187
|
+
}
|
|
188
|
+
: {
|
|
189
|
+
text: "bytefly: every configured package is updated — reload to use the new version.",
|
|
190
|
+
severity: "info",
|
|
191
|
+
};
|
|
177
192
|
case "available": {
|
|
178
193
|
const own = outcome.updates.some(isOwnPackage);
|
|
179
194
|
const release = own ? ownRelease(outcome.ownLatest) : undefined;
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
18
18
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
19
19
|
import pkg from "../../../package.json";
|
|
20
|
+
import { messageOf } from "../../../lib/errors.ts";
|
|
20
21
|
import { isOffline } from "../../../lib/env.ts";
|
|
21
22
|
import {
|
|
22
23
|
isFresh,
|
|
@@ -26,7 +27,7 @@ import {
|
|
|
26
27
|
writeUpdateCache,
|
|
27
28
|
type UpdateCacheEntry,
|
|
28
29
|
} from "./cache.ts";
|
|
29
|
-
import { fetchAvailableUpdates, fetchLatestVersion } from "./check.ts";
|
|
30
|
+
import { applyUpdates, fetchAvailableUpdates, fetchLatestVersion } from "./check.ts";
|
|
30
31
|
import { isOwnPackage, updateNotice } from "./format.ts";
|
|
31
32
|
import { paintLatest, type UpdateOutcome, type UpdateSource } from "./state.ts";
|
|
32
33
|
import { suppressPiPackageUpdates, type UpdateSuppression } from "./suppress.ts";
|
|
@@ -45,6 +46,8 @@ export interface UpdateCache {
|
|
|
45
46
|
export interface UpdateHandle {
|
|
46
47
|
/** Read the package list now, ignoring the cache window. Never throws. */
|
|
47
48
|
recheck(): Promise<UpdateOutcome>;
|
|
49
|
+
/** Apply the update, then re-read it. Never throws. */
|
|
50
|
+
apply(): Promise<UpdateOutcome>;
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
/** Test seams; production uses Pi's package manager, its cache, and the agent dir. */
|
|
@@ -53,6 +56,8 @@ export interface UpdateDeps {
|
|
|
53
56
|
readUpdates?: (ctx: ExtensionContext, check: PackageUpdateCheck) => Promise<PackageUpdate[] | undefined>;
|
|
54
57
|
/** Replace the registry read that names this package's new version. */
|
|
55
58
|
fetchLatest?: () => Promise<string | undefined>;
|
|
59
|
+
/** Replace the package-manager update; defaults to Pi's own manager. */
|
|
60
|
+
applyUpdates?: (ctx: ExtensionContext) => Promise<void>;
|
|
56
61
|
/** Replace the `PI_OFFLINE` check. */
|
|
57
62
|
isOffline?: () => boolean;
|
|
58
63
|
/** Replace the disk cache. */
|
|
@@ -94,6 +99,16 @@ export function registerUpdate(pi: ExtensionAPI, updates: UpdateSource, deps: Up
|
|
|
94
99
|
checkForAvailableUpdates: check,
|
|
95
100
|
}));
|
|
96
101
|
const readLatest = deps.fetchLatest ?? (() => fetchLatestVersion());
|
|
102
|
+
const applyUpdate =
|
|
103
|
+
deps.applyUpdates ??
|
|
104
|
+
((ctx: ExtensionContext) =>
|
|
105
|
+
applyUpdates({
|
|
106
|
+
cwd: ctx.cwd,
|
|
107
|
+
agentDir: getAgentDir(),
|
|
108
|
+
projectTrusted: ctx.isProjectTrusted(),
|
|
109
|
+
// Pi's manager reports one line per source; a slow install is otherwise silent.
|
|
110
|
+
onProgress: (message) => ctx.ui.notify(message, "info"),
|
|
111
|
+
}));
|
|
97
112
|
const offline = deps.isOffline ?? isOffline;
|
|
98
113
|
const cache = deps.cache ?? diskCache();
|
|
99
114
|
const now = deps.now ?? Date.now;
|
|
@@ -155,6 +170,22 @@ export function registerUpdate(pi: ExtensionAPI, updates: UpdateSource, deps: Up
|
|
|
155
170
|
return latest && isNewerVersion(latest, pkg.version) ? latest : undefined;
|
|
156
171
|
};
|
|
157
172
|
|
|
173
|
+
/**
|
|
174
|
+
* One snapshot from a package list: this package's version label, the cache key,
|
|
175
|
+
* and the clock. The background check and the panel's recheck differ only in what
|
|
176
|
+
* they do with a snapshot, so the assembly lives here once instead of being
|
|
177
|
+
* copied wherever something new needs to record a reading.
|
|
178
|
+
*/
|
|
179
|
+
const buildSnapshot = async (ctx: ExtensionContext, list: PackageUpdate[]): Promise<UpdateCacheEntry> => {
|
|
180
|
+
const latest = await resolveOwnLatest(list.find(isOwnPackage));
|
|
181
|
+
return {
|
|
182
|
+
key: keyFor(ctx),
|
|
183
|
+
checkedAt: now(),
|
|
184
|
+
updates: list,
|
|
185
|
+
...(latest ? { latest } : {}),
|
|
186
|
+
};
|
|
187
|
+
};
|
|
188
|
+
|
|
158
189
|
const check = async (ctx: ExtensionContext, token: number): Promise<void> => {
|
|
159
190
|
try {
|
|
160
191
|
const list = await readUpdates(ctx, activeCheck());
|
|
@@ -163,14 +194,8 @@ export function registerUpdate(pi: ExtensionAPI, updates: UpdateSource, deps: Up
|
|
|
163
194
|
// the next session "up to date" for a day and clear a marker that may still
|
|
164
195
|
// be right, so leave both alone and try again next time.
|
|
165
196
|
if (list === undefined) return;
|
|
166
|
-
const
|
|
197
|
+
const entry = await buildSnapshot(ctx, list);
|
|
167
198
|
if (token !== generation) return;
|
|
168
|
-
const entry: UpdateCacheEntry = {
|
|
169
|
-
key: keyFor(ctx),
|
|
170
|
-
checkedAt: now(),
|
|
171
|
-
updates: list,
|
|
172
|
-
...(latest ? { latest } : {}),
|
|
173
|
-
};
|
|
174
199
|
writeCache(entry);
|
|
175
200
|
record(entry);
|
|
176
201
|
} catch {
|
|
@@ -210,31 +235,45 @@ export function registerUpdate(pi: ExtensionAPI, updates: UpdateSource, deps: Up
|
|
|
210
235
|
context = undefined;
|
|
211
236
|
});
|
|
212
237
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// The caller prints the answer, so this path never posts a notice.
|
|
234
|
-
paintLatest(updates, latest, deps.onUpdate);
|
|
235
|
-
return list.length > 0
|
|
236
|
-
? { kind: "available", updates: list, ...(latest ? { ownLatest: latest } : {}) }
|
|
237
|
-
: { kind: "current" };
|
|
238
|
-
},
|
|
238
|
+
/** Re-read the published state on demand; `/bytefly update` prints the answer. */
|
|
239
|
+
const recheck = async (): Promise<UpdateOutcome> => {
|
|
240
|
+
if (offline()) return { kind: "offline" };
|
|
241
|
+
if (!context) return { kind: "failed" };
|
|
242
|
+
let list: PackageUpdate[];
|
|
243
|
+
try {
|
|
244
|
+
const found = await readUpdates(context, activeCheck());
|
|
245
|
+
if (found === undefined) return { kind: "failed" };
|
|
246
|
+
list = found;
|
|
247
|
+
} catch {
|
|
248
|
+
return { kind: "failed" };
|
|
249
|
+
}
|
|
250
|
+
const entry = await buildSnapshot(context, list);
|
|
251
|
+
writeCache(entry);
|
|
252
|
+
// The caller prints the answer, so this path never posts a notice.
|
|
253
|
+
const ownLatest = ownLatestOf(entry);
|
|
254
|
+
paintLatest(updates, ownLatest, deps.onUpdate);
|
|
255
|
+
return list.length > 0
|
|
256
|
+
? { kind: "available", updates: list, ...(ownLatest ? { ownLatest } : {}) }
|
|
257
|
+
: { kind: "current" };
|
|
239
258
|
};
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Apply the update through Pi's manager, then re-read it: the snapshot on disk and
|
|
262
|
+
* on screen described the version that was just replaced, so leaving it would keep
|
|
263
|
+
* a marker for code this session no longer runs. `pending` is the manager reporting
|
|
264
|
+
* success over a package that is still behind, which is worth saying out loud.
|
|
265
|
+
*/
|
|
266
|
+
const apply = async (): Promise<UpdateOutcome> => {
|
|
267
|
+
if (offline()) return { kind: "offline" };
|
|
268
|
+
if (!context) return { kind: "failed" };
|
|
269
|
+
try {
|
|
270
|
+
await applyUpdate(context);
|
|
271
|
+
} catch (error) {
|
|
272
|
+
return { kind: "update-failed", reason: messageOf(error) };
|
|
273
|
+
}
|
|
274
|
+
const after = await recheck();
|
|
275
|
+
return { kind: "updated", pending: after.kind === "available" };
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
return { recheck, apply };
|
|
240
279
|
}
|
|
@@ -20,13 +20,16 @@ export interface UpdateSource {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
* What one explicit
|
|
24
|
-
* asked — so this vocabulary exists only for `/bytefly update
|
|
25
|
-
* a skip is an answer rather
|
|
23
|
+
* What one explicit update reported. The background check reports nothing — nobody
|
|
24
|
+
* asked — so this vocabulary exists only for `/bytefly update` and
|
|
25
|
+
* `/bytefly upgrade`, where a failure, a skip, or a refusal is an answer rather
|
|
26
|
+
* than silence.
|
|
26
27
|
*/
|
|
27
28
|
export type UpdateOutcome =
|
|
28
29
|
| { kind: "offline" }
|
|
29
30
|
| { kind: "failed" }
|
|
31
|
+
| { kind: "update-failed"; reason: string }
|
|
32
|
+
| { kind: "updated"; pending: boolean }
|
|
30
33
|
| { kind: "current" }
|
|
31
34
|
| { kind: "available"; updates: readonly PackageUpdate[]; ownLatest?: string };
|
|
32
35
|
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { resolveEffectiveCwd } from "../../lib/env.ts";
|
|
13
|
+
import { planModeRefusal } from "../../lib/plan-entry.ts";
|
|
13
14
|
import { isPlanModeEnabled } from "../../lib/plan-state.ts";
|
|
14
15
|
import { withRailsSuppressed } from "../../lib/rails.ts";
|
|
15
16
|
import { formatCheckReport, formatCheckSummary } from "./format.ts";
|
|
@@ -107,7 +108,7 @@ export function registerCheckCommand(pi: ExtensionAPI, runtime: CheckRuntime): v
|
|
|
107
108
|
},
|
|
108
109
|
handler: async (args, ctx) => {
|
|
109
110
|
if (isPlanModeEnabled(ctx.sessionManager.getBranch())) {
|
|
110
|
-
ctx.ui.notify("
|
|
111
|
+
ctx.ui.notify(planModeRefusal("running checks"), "warning");
|
|
111
112
|
return;
|
|
112
113
|
}
|
|
113
114
|
const target = parseTarget(args, ctx);
|
|
@@ -203,7 +203,7 @@ export class ContextComponent implements Component {
|
|
|
203
203
|
lines.push("", ` ${paint(this.theme, "label", "Largest messages")}`);
|
|
204
204
|
for (const block of report.largest) {
|
|
205
205
|
const tokens = formatTokens(block.tokens).padStart(6);
|
|
206
|
-
const role = padCell(block.role,
|
|
206
|
+
const role = padCell(block.role, 12);
|
|
207
207
|
const candidate = ` ${paint(this.theme, "meta", tokens)} ${paint(this.theme, "label", role)}${block.label}`;
|
|
208
208
|
lines.push(truncateToWidth(candidate, width));
|
|
209
209
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { randomInt } from "node:crypto";
|
|
10
|
-
import { asBoolean, clampInteger, loadConfigSection } from "
|
|
10
|
+
import { asBoolean, clampInteger, loadConfigSection } from "../../../lib/config.ts";
|
|
11
11
|
|
|
12
12
|
export interface ServeConfig {
|
|
13
13
|
/** TCP port on 127.0.0.1; `0` picks one random port fixed for the session, a positive value pins it. */
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a file's kind is, from its name and — for an extensionless one — a byte sample.
|
|
3
|
+
*
|
|
4
|
+
* Every surface that names a kind (the listing's rows, the tree, a preview's routing) asks
|
|
5
|
+
* here, so "is this an image" has one answer.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { basename, extname } from "node:path";
|
|
9
|
+
|
|
10
|
+
export type EntryKind = "directory" | "text" | "image" | "binary";
|
|
11
|
+
|
|
12
|
+
/** Image extension (no dot) to MIME type; the single source for image classification. */
|
|
13
|
+
const IMAGE_MIME: Record<string, string> = {
|
|
14
|
+
png: "image/png",
|
|
15
|
+
jpg: "image/jpeg",
|
|
16
|
+
jpeg: "image/jpeg",
|
|
17
|
+
gif: "image/gif",
|
|
18
|
+
webp: "image/webp",
|
|
19
|
+
svg: "image/svg+xml",
|
|
20
|
+
bmp: "image/bmp",
|
|
21
|
+
ico: "image/x-icon",
|
|
22
|
+
avif: "image/avif",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Image extensions without a leading dot. */
|
|
26
|
+
export const IMAGE_EXTENSIONS: ReadonlySet<string> = new Set(Object.keys(IMAGE_MIME));
|
|
27
|
+
|
|
28
|
+
const MARKDOWN_EXTS = new Set([".md", ".markdown", ".mdown", ".mkd", ".mkdn"]);
|
|
29
|
+
const TEXT_EXTS = new Set([
|
|
30
|
+
".txt",
|
|
31
|
+
".md",
|
|
32
|
+
".markdown",
|
|
33
|
+
".rst",
|
|
34
|
+
".adoc",
|
|
35
|
+
".org",
|
|
36
|
+
".log",
|
|
37
|
+
".json",
|
|
38
|
+
".jsonc",
|
|
39
|
+
".json5",
|
|
40
|
+
".yaml",
|
|
41
|
+
".yml",
|
|
42
|
+
".toml",
|
|
43
|
+
".ini",
|
|
44
|
+
".cfg",
|
|
45
|
+
".conf",
|
|
46
|
+
".xml",
|
|
47
|
+
".csv",
|
|
48
|
+
".tsv",
|
|
49
|
+
".ts",
|
|
50
|
+
".tsx",
|
|
51
|
+
".js",
|
|
52
|
+
".jsx",
|
|
53
|
+
".mjs",
|
|
54
|
+
".cjs",
|
|
55
|
+
".py",
|
|
56
|
+
".go",
|
|
57
|
+
".rs",
|
|
58
|
+
".java",
|
|
59
|
+
".c",
|
|
60
|
+
".h",
|
|
61
|
+
".cc",
|
|
62
|
+
".cpp",
|
|
63
|
+
".hpp",
|
|
64
|
+
".cs",
|
|
65
|
+
".rb",
|
|
66
|
+
".php",
|
|
67
|
+
".sh",
|
|
68
|
+
".bash",
|
|
69
|
+
".zsh",
|
|
70
|
+
".ps1",
|
|
71
|
+
".swift",
|
|
72
|
+
".kt",
|
|
73
|
+
".kts",
|
|
74
|
+
".scala",
|
|
75
|
+
".lua",
|
|
76
|
+
".r",
|
|
77
|
+
".pl",
|
|
78
|
+
".sql",
|
|
79
|
+
".css",
|
|
80
|
+
".scss",
|
|
81
|
+
".sass",
|
|
82
|
+
".less",
|
|
83
|
+
".html",
|
|
84
|
+
".htm",
|
|
85
|
+
".vue",
|
|
86
|
+
".svelte",
|
|
87
|
+
".astro",
|
|
88
|
+
]);
|
|
89
|
+
const TEXT_NAMES = new Set([
|
|
90
|
+
"license",
|
|
91
|
+
"licence",
|
|
92
|
+
"makefile",
|
|
93
|
+
"dockerfile",
|
|
94
|
+
"readme",
|
|
95
|
+
"changelog",
|
|
96
|
+
"authors",
|
|
97
|
+
"notice",
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
export function isImageName(name: string): boolean {
|
|
101
|
+
return IMAGE_EXTENSIONS.has(extname(name).slice(1).toLowerCase());
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Whether the name has a Markdown extension. */
|
|
105
|
+
export function isMarkdownName(name: string): boolean {
|
|
106
|
+
return MARKDOWN_EXTS.has(extname(name).toLowerCase());
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Extension-based classification for listings (no file contents read). */
|
|
110
|
+
export function classifyByName(name: string): EntryKind {
|
|
111
|
+
const ext = extname(name).toLowerCase();
|
|
112
|
+
if (IMAGE_EXTENSIONS.has(ext.slice(1))) return "image";
|
|
113
|
+
if (TEXT_EXTS.has(ext)) return "text";
|
|
114
|
+
const lower = basename(name).toLowerCase();
|
|
115
|
+
if (TEXT_NAMES.has(lower)) return "text";
|
|
116
|
+
if (!ext && name.startsWith(".")) return "text";
|
|
117
|
+
return "binary";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** True when the sample looks like binary data (NUL or many control bytes). */
|
|
121
|
+
export function isProbablyText(sample: Buffer): boolean {
|
|
122
|
+
if (sample.length === 0) return true;
|
|
123
|
+
if (sample.includes(0)) return false;
|
|
124
|
+
let suspicious = 0;
|
|
125
|
+
for (const byte of sample) {
|
|
126
|
+
if (byte === 9 || byte === 10 || byte === 13 || byte === 12 || byte === 8) continue;
|
|
127
|
+
if (byte < 32 || byte === 127) suspicious++;
|
|
128
|
+
}
|
|
129
|
+
return suspicious / sample.length < 0.3;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** MIME type for a name; text is always served as plain text. */
|
|
133
|
+
export function contentTypeFor(name: string): string {
|
|
134
|
+
const ext = extname(name).slice(1).toLowerCase();
|
|
135
|
+
return IMAGE_MIME[ext] ?? "application/octet-stream";
|
|
136
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem reads for the browse extension: classification, listing, tree, and index.
|
|
3
|
+
*
|
|
4
|
+
* Four jobs that a caller wants as one surface, so they are four modules behind this
|
|
5
|
+
* barrel rather than one 551-line module named after none of them. All paths are absolute
|
|
6
|
+
* and already vetted by `paths.ts`; nothing here touches the raw request URL.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export * from "./classify.ts";
|
|
10
|
+
export * from "./index.ts";
|
|
11
|
+
export * from "./list.ts";
|
|
12
|
+
export * from "./tree.ts";
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The quick-open index: every file path under the root, to `limit`, for the palette.
|
|
3
|
+
*
|
|
4
|
+
* A separate walk from the tree on purpose: the palette wants paths and nothing else, and
|
|
5
|
+
* it reads the whole tree once instead of per folder.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Dirent } from "node:fs";
|
|
9
|
+
import { readdir } from "node:fs/promises";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
12
|
+
/** Directory names the quick-open index never walks. */
|
|
13
|
+
export const INDEX_IGNORED_DIRS = new Set([".git", "node_modules"]);
|
|
14
|
+
|
|
15
|
+
export interface FileIndex {
|
|
16
|
+
files: string[];
|
|
17
|
+
truncated: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Walk `root` and collect file paths for the quick-open palette. Symlinks and
|
|
22
|
+
* the ignored directories are skipped, and the walk stops at `limit` files so a
|
|
23
|
+
* large tree cannot exhaust memory or time. Unreadable directories are skipped.
|
|
24
|
+
*/
|
|
25
|
+
export async function buildFileIndex(root: string, limit: number): Promise<FileIndex> {
|
|
26
|
+
const files: string[] = [];
|
|
27
|
+
let truncated = false;
|
|
28
|
+
|
|
29
|
+
async function walk(absDir: string, relDir: string): Promise<void> {
|
|
30
|
+
let dirents: Dirent[];
|
|
31
|
+
try {
|
|
32
|
+
dirents = await readdir(absDir, { withFileTypes: true });
|
|
33
|
+
} catch {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
dirents.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
|
|
37
|
+
for (const dirent of dirents) {
|
|
38
|
+
if (truncated) return;
|
|
39
|
+
if (dirent.isSymbolicLink()) continue;
|
|
40
|
+
const rel = relDir ? `${relDir}/${dirent.name}` : dirent.name;
|
|
41
|
+
if (dirent.isDirectory()) {
|
|
42
|
+
if (INDEX_IGNORED_DIRS.has(dirent.name)) continue;
|
|
43
|
+
await walk(join(absDir, dirent.name), rel);
|
|
44
|
+
} else if (dirent.isFile()) {
|
|
45
|
+
if (files.length >= limit) {
|
|
46
|
+
truncated = true;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
files.push(rel);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
await walk(root, "");
|
|
55
|
+
files.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()) || a.localeCompare(b));
|
|
56
|
+
return { files, truncated };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type FileIndexProvider = () => Promise<FileIndex>;
|
|
60
|
+
|
|
61
|
+
export interface FileIndexOptions {
|
|
62
|
+
/** How long a built index is reused, in ms (default 10000). */
|
|
63
|
+
ttlMs?: number;
|
|
64
|
+
/** Wall-clock seam; defaults to the real clock. */
|
|
65
|
+
now?: () => number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A cached `buildFileIndex` for `root`; concurrent calls share one walk. */
|
|
69
|
+
export function createFileIndex(root: string, limit: number, options: FileIndexOptions = {}): FileIndexProvider {
|
|
70
|
+
const ttlMs = options.ttlMs ?? 10_000;
|
|
71
|
+
const now = options.now ?? (() => Date.now());
|
|
72
|
+
let cached: FileIndex | undefined;
|
|
73
|
+
let checkedAt = 0;
|
|
74
|
+
let pending: Promise<FileIndex> | undefined;
|
|
75
|
+
return async () => {
|
|
76
|
+
const time = now();
|
|
77
|
+
if (cached && time - checkedAt < ttlMs) return cached;
|
|
78
|
+
if (pending) return pending;
|
|
79
|
+
pending = buildFileIndex(root, limit)
|
|
80
|
+
.then((index) => {
|
|
81
|
+
cached = index;
|
|
82
|
+
checkedAt = now();
|
|
83
|
+
return index;
|
|
84
|
+
})
|
|
85
|
+
.finally(() => {
|
|
86
|
+
pending = undefined;
|
|
87
|
+
});
|
|
88
|
+
return pending;
|
|
89
|
+
};
|
|
90
|
+
}
|