tokentracker-cli 0.87.7 → 0.87.8
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 +3 -2
- package/README.zh-CN.md +3 -2
- package/dashboard/dist/assets/{AchievementBadge-4SSFyzNE.js → AchievementBadge-CpxX02OK.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-4ASCXja2.js → AchievementsPage-CSms8IjI.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-CyaMYMol.js → AchievementsSection-Sm43tnkh.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-CUWTCEru.js → BadgeDetailModal-cSNqMLsK.js} +1 -1
- package/dashboard/dist/assets/{Card-C79yXijL.js → Card-wNwNRVSb.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-Cw6J-x_d.js → ClawdAnimated-S8sIS2Ca.js} +1 -1
- package/dashboard/dist/assets/{CommandPalette-C1RQ58De.js → CommandPalette-DVeiwbPM.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-CycaCGsd.js → CommunityStatsModal-g5P4j3um.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-hPuA2aup.js → ConfirmModal-2P2D5iIp.js} +1 -1
- package/dashboard/dist/assets/{Controls-DjunxsRS.js → Controls-CCaotoQP.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-C5H5_EZN.js → DashboardPage-DxPGTJpe.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-a1TT-0Bd.js → DevicePage-_nLwNash.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-CCRfnIbO.js → DialogClose-D0giFwO3.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-8veCvaUb.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-2Qa5pApU.js → DialogTitle-JS_04eUC.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-C3mpVy83.js → FadeIn-tFzTWDkC.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-Dn_F7ByX.js → HeaderGithubStar-B2EUJUTH.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-DaywO0Zz.js → HoverTooltip-BJzHksPT.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-oqGfnWb7.js → IpCheckPage-B8mA0mut.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-CX7dYsEX.js → LandingPage-DmJZDKr9.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardAvatar-Bl-OMQ2x.js → LeaderboardAvatar-CDeRQUCg.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-yeWcgiVo.js → LeaderboardPage-Dkgm89kY.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-CDmZtQ_m.js → LeaderboardProfileModal-B6_kuR1z.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-BqJYxoN1.js → LeaderboardProfilePage-BRs8sJBV.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-D8NFgXbZ.js → LimitsPage-DZ1s5NWV.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-DjdhDEiq.js → LocalOnlyNotice-Cu_hSnux.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-jQXAmSQb.js → LoginCard-yDZbGsHR.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-q4G6EN7b.js → LoginModal-DEvzCUfQ.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-BklWSXzs.js → LoginPage-DBySF9do.js} +1 -1
- package/dashboard/dist/assets/{PetPage-DyWuMUTA.js → PetPage-DnzH9t-r.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-Dbxed_FL.js → PopoverPopup-DgFsEMax.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-ChYuSSfj.js → ProviderIcon-BEixe-xP.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-DJww1dxo.js → ResetPasswordPage-excKqsCL.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-Dr73_QVm.js → SegmentedControl-DDFkH64o.js} +1 -1
- package/dashboard/dist/assets/{Select-BMCy_8oY.js → Select-ChEPIuJf.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-BRU7mlBI.js → SelectItemText--b7ISnJm.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-DDao-oTH.js → ServiceStatusPage-DtAY-Qnn.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-BjngkrsY.js → SessionsPage-CKFglvuj.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-Dsftjg5j.js → SettingsPage-s0605qWg.js} +1 -1
- package/dashboard/dist/assets/{SkillsPage-igML0DOZ.js → SkillsPage-DTsbCucq.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-BBETSYe7.js → TokenGalaxy-C_M7q8S4.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-CzEAxfqj.js → ToolbarRootContext-CXQwpea-.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-JV6lkw-s.js → TrendMonitor-1Yhlplf0.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-GpUmv0hI.js → WidgetsPage-DC6pmtld.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-SP2r4wf1.js → WrappedPage-DixoqR13.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-AQgdBxou.js → __vite-browser-external-5aLUeBkM.js} +4 -3
- package/dashboard/dist/assets/{agent-logos-DxwSc05k.js → agent-logos-DILTbPLg.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-CDW5zn0Q.js → arrow-up-right-DgIDgjVW.js} +1 -1
- package/dashboard/dist/assets/{check-CQfDkdqM.js → check-Bz6l09VH.js} +1 -1
- package/dashboard/dist/assets/{chevron-right-DDFSY4w-.js → chevron-right-EvGQXbI3.js} +1 -1
- package/dashboard/dist/assets/{copy-hxnyMdBa.js → copy-DbsB0OVy.js} +1 -1
- package/dashboard/dist/assets/{download-DOUBKxcf.js → download-R8nUvXYh.js} +1 -1
- package/dashboard/dist/assets/{external-link-CVipCZOa.js → external-link-x0oNzGeq.js} +1 -1
- package/dashboard/dist/assets/{flame-C-QU6mPf.js → flame-Bo4IdG6k.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-DRX15id1.js → format-tokens-BSGC1-Gz.js} +1 -1
- package/dashboard/dist/assets/{icons-ajxtn_0O.js → icons-D0Okncmo.js} +1 -1
- package/dashboard/dist/assets/{index-OJC-yROZ.js → index-BbOkC6wf.js} +1 -1
- package/dashboard/dist/assets/{index-Bq5VZMkS.js → index-Bvvmei-I.js} +1 -1
- package/dashboard/dist/assets/{index-DnVXRsqb.js → index-D6F8eNr_.js} +1 -1
- package/dashboard/dist/assets/{info-eITfPd17.js → info-DXXKNpLG.js} +1 -1
- package/dashboard/dist/assets/{limits-providers-TMq74bRy.js → limits-providers-DYa4fdsE.js} +1 -1
- package/dashboard/dist/assets/{link-2-D778MOmL.js → link-2-C9mxEnT6.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-uN5rBNIF.js → loader-circle-Y8V8Mq9L.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-C4FBbMpN.js → maximize-2-QuP69Xcg.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-DLeiQDTT.js → modifiers.esm-B3SleRKs.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-C-obu2-u.js → monitor-smartphone-B2LslYi7.js} +1 -1
- package/dashboard/dist/assets/{provider-display-Bjl8tmYu.js → provider-display-DzBwrDZp.js} +2 -2
- package/dashboard/dist/assets/{react-Diah7uAT.js → react-BooKG84W.js} +1 -1
- package/dashboard/dist/assets/{search-B0bPOquo.js → search-zdYuHmL-.js} +1 -1
- package/dashboard/dist/assets/{skills-api-6otf2Sdl.js → skills-api-C2IkKfwE.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-DcrJaOfF.js → sortable.esm-BHvl5vG5.js} +1 -1
- package/dashboard/dist/assets/{store-DOs4omJM.js → store-BBP4jx0R.js} +1 -1
- package/dashboard/dist/assets/{terminal-CpMZjaGk.js → terminal-C0nhFNQ0.js} +1 -1
- package/dashboard/dist/assets/{trash-2-BQYnn3zf.js → trash-2-DNVSqbxA.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-CdL4zXXV.js → use-community-stats-BK070d1s.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-DUh4f8f4.js → use-limits-display-prefs-BXOUxblO.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-DVs0Ik3O.js → use-ordered-list-0SVHZHaW.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-abZbtoVS.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-CGggPI6Y.js → use-session-efficiency-pref-eaUpwuLK.js} +1 -1
- package/dashboard/dist/assets/{use-transform-DMj91FEL.js → use-transform-Bhar2U4w.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-xJOXzuZG.js → use-usage-limits-BGGCMVzi.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-BQRBx-KO.js → useCurrency-gIJVmq3w.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-DuTPOBl8.js → useScrollLock-BhFzeRtR.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-Dd95ph6d.js → useTokenFormat-BZ8WT-A6.js} +1 -1
- package/dashboard/dist/assets/{zap-DJMdRI6y.js → zap-Dvc_Jt1t.js} +1 -1
- package/dashboard/dist/brand-logos/omp.svg +33 -0
- package/dashboard/dist/brand-logos/pi.svg +16 -0
- package/dashboard/dist/index.html +1 -1
- package/dashboard/dist/ip-check.html +1 -1
- package/dashboard/dist/leaderboard.html +1 -1
- package/dashboard/dist/share.html +1 -1
- package/package.json +1 -1
- package/src/commands/init.js +49 -8
- package/src/commands/status.js +12 -5
- package/src/commands/uninstall.js +36 -1
- package/src/lib/omp-hook.js +589 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/dashboard/dist/assets/DialogDescription-C99d5K4r.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion--_Yc7AWk.js +0 -1
package/src/commands/status.js
CHANGED
|
@@ -84,6 +84,7 @@ const wsl = require("../lib/wsl-probe");
|
|
|
84
84
|
const { getWslMode, isInvalidWslMode, shouldProbeWsl, discoverWslHome } = wsl;
|
|
85
85
|
const { resolveInstallPaths } = require("../lib/install-resolver");
|
|
86
86
|
const { probeGrokHookState, resolveGrokHome } = require("../lib/grok-hook");
|
|
87
|
+
const { probeOmpHookState } = require("../lib/omp-hook");
|
|
87
88
|
|
|
88
89
|
function formatResolvedPaths(paths, filename) {
|
|
89
90
|
const active = [];
|
|
@@ -359,10 +360,11 @@ async function cmdStatus(argv = []) {
|
|
|
359
360
|
? fssync.existsSync(path.join(workbuddyHome, "workbuddy.db"))
|
|
360
361
|
: false;
|
|
361
362
|
|
|
362
|
-
// oh-my-pi — passive scan
|
|
363
|
+
// oh-my-pi — passive scan + optional notify extension.
|
|
363
364
|
const ompAgentDir = resolveOmpAgentDir(process.env);
|
|
364
365
|
const ompInstalled = Boolean(ompAgentDir) && fssync.existsSync(path.join(ompAgentDir, "sessions"));
|
|
365
366
|
const ompFiles = ompInstalled ? resolveOmpSessionFiles(process.env) : [];
|
|
367
|
+
const ompHookState = await probeOmpHookState({ home, trackerDir, env: process.env });
|
|
366
368
|
|
|
367
369
|
// pi (@mariozechner/pi-coding-agent) — passive scan only (no hooks).
|
|
368
370
|
// Skip when its agent dir collides with omp's; sync would dedupe anyway.
|
|
@@ -771,8 +773,13 @@ async function cmdStatus(argv = []) {
|
|
|
771
773
|
workbuddy: workbuddyInstalled
|
|
772
774
|
? { installed: true, files: workbuddyFiles.length }
|
|
773
775
|
: { installed: false },
|
|
774
|
-
omp: ompInstalled
|
|
775
|
-
? {
|
|
776
|
+
omp: ompInstalled || ompHookState.ompPresent
|
|
777
|
+
? {
|
|
778
|
+
installed: true,
|
|
779
|
+
files: ompFiles.length,
|
|
780
|
+
notify_extension: Boolean(ompHookState.configured),
|
|
781
|
+
notify_extension_path: ompHookState.extensionPath || null,
|
|
782
|
+
}
|
|
776
783
|
: { installed: false },
|
|
777
784
|
pi: piInstalled
|
|
778
785
|
? { installed: true, files: piFiles.length }
|
|
@@ -895,8 +902,8 @@ async function cmdStatus(argv = []) {
|
|
|
895
902
|
workbuddyInstalled
|
|
896
903
|
? `- WorkBuddy hooks: ${workbuddyHookConfigured ? "set" : "unset"} (${workbuddyFiles.length} session jsonl file${workbuddyFiles.length !== 1 ? "s" : ""} found, SQLite DB ${workbuddyDbExists ? "found" : "not found"})`
|
|
897
904
|
: null,
|
|
898
|
-
ompInstalled
|
|
899
|
-
? `- oh-my-pi: passive reader (${ompFiles.length} session jsonl file${ompFiles.length !== 1 ? "s" : ""} found)`
|
|
905
|
+
ompInstalled || ompHookState.ompPresent
|
|
906
|
+
? `- oh-my-pi: passive reader (${ompFiles.length} session jsonl file${ompFiles.length !== 1 ? "s" : ""} found${ompHookState.configured ? ", notify extension: yes" : ", notify extension: no"})`
|
|
900
907
|
: null,
|
|
901
908
|
piInstalled
|
|
902
909
|
? `- pi: passive reader (${piFiles.length} session jsonl file${piFiles.length !== 1 ? "s" : ""} found)`
|
|
@@ -24,6 +24,7 @@ const { resolveOpencodeConfigDir, removeOpencodePlugin } = require("../lib/openc
|
|
|
24
24
|
const { removeOpenclawHookConfig } = require("../lib/openclaw-hook");
|
|
25
25
|
const { removeOpenclawSessionPluginConfig } = require("../lib/openclaw-session-plugin");
|
|
26
26
|
const { removeGrokHook } = require("../lib/grok-hook");
|
|
27
|
+
const { removeOmpHook } = require("../lib/omp-hook");
|
|
27
28
|
const { resolveTrackerPaths } = require("../lib/tracker-paths");
|
|
28
29
|
|
|
29
30
|
async function cmdUninstall(argv) {
|
|
@@ -104,6 +105,7 @@ async function cmdUninstall(argv) {
|
|
|
104
105
|
});
|
|
105
106
|
const openclawHookRemove = await removeOpenclawHookConfig({ home, trackerDir, env: process.env });
|
|
106
107
|
const grokHookRemove = await removeGrokHook({ home, trackerDir, env: process.env });
|
|
108
|
+
const ompHookRemove = await removeOmpHook({ home, trackerDir, env: process.env });
|
|
107
109
|
|
|
108
110
|
// Remove installed notify handler.
|
|
109
111
|
await fs.unlink(notifyPath).catch(() => {});
|
|
@@ -195,6 +197,7 @@ async function cmdUninstall(argv) {
|
|
|
195
197
|
grokHookRemove?.removed
|
|
196
198
|
? `- Grok Build hook removed: ${grokHookRemove.hookPath}`
|
|
197
199
|
: "- Grok Build hook: no change",
|
|
200
|
+
formatOmpHookRemoveLine(ompHookRemove),
|
|
198
201
|
opts.purge ? `- Purged: ${path.join(home, ".tokentracker")}` : "- Purge: skipped (use --purge)",
|
|
199
202
|
...(machineIdSeedKept
|
|
200
203
|
? [`- Kept: ${machineIdSeedPath} (cloud device identity — a reinstall reuses the same device; delete it to fully reset)`]
|
|
@@ -204,6 +207,38 @@ async function cmdUninstall(argv) {
|
|
|
204
207
|
);
|
|
205
208
|
}
|
|
206
209
|
|
|
210
|
+
function formatOmpHookRemoveLine(ompHookRemove) {
|
|
211
|
+
if (ompHookRemove?.removed) {
|
|
212
|
+
return `- oh-my-pi notify extension removed: ${ompHookRemove.extensionPath}`;
|
|
213
|
+
}
|
|
214
|
+
const reason = ompHookRemove?.skippedReason;
|
|
215
|
+
const residualPath = ompHookRemove?.stagedPath || ompHookRemove?.extensionPath;
|
|
216
|
+
const residual = residualPath
|
|
217
|
+
? ` (left in place: ${residualPath})`
|
|
218
|
+
: "";
|
|
219
|
+
if (reason === "unmanaged") {
|
|
220
|
+
return `- oh-my-pi notify extension: skipped (unmanaged file)${residual}`;
|
|
221
|
+
}
|
|
222
|
+
if (reason === "unlink-failed") {
|
|
223
|
+
const detail = ompHookRemove?.error ? `: ${ompHookRemove.error}` : "";
|
|
224
|
+
return `- oh-my-pi notify extension: failed to remove${detail}${residual}`;
|
|
225
|
+
}
|
|
226
|
+
if (reason === "extension-read-failed") {
|
|
227
|
+
const detail = ompHookRemove?.error ? `: ${ompHookRemove.error}` : "";
|
|
228
|
+
return `- oh-my-pi notify extension: failed to read${detail}${residual}`;
|
|
229
|
+
}
|
|
230
|
+
if (reason === "identity-changed") {
|
|
231
|
+
return `- oh-my-pi notify extension: skipped (file changed during uninstall)${residual}`;
|
|
232
|
+
}
|
|
233
|
+
if (reason === "omp-agent-dir-unresolved") {
|
|
234
|
+
return "- oh-my-pi notify extension: skipped (omp agent dir unresolved)";
|
|
235
|
+
}
|
|
236
|
+
if (reason === "missing") {
|
|
237
|
+
return "- oh-my-pi notify extension: no change";
|
|
238
|
+
}
|
|
239
|
+
return "- oh-my-pi notify extension: no change";
|
|
240
|
+
}
|
|
241
|
+
|
|
207
242
|
function parseArgs(argv) {
|
|
208
243
|
const out = { purge: false };
|
|
209
244
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -214,7 +249,7 @@ function parseArgs(argv) {
|
|
|
214
249
|
return out;
|
|
215
250
|
}
|
|
216
251
|
|
|
217
|
-
module.exports = { cmdUninstall };
|
|
252
|
+
module.exports = { cmdUninstall, formatOmpHookRemoveLine };
|
|
218
253
|
|
|
219
254
|
async function isFile(p) {
|
|
220
255
|
try {
|
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const os = require("node:os");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const fs = require("node:fs/promises");
|
|
6
|
+
const fssync = require("node:fs");
|
|
7
|
+
|
|
8
|
+
// Reuse the passive scanner's path resolvers so init/status/uninstall target
|
|
9
|
+
// the same OMP installation that sync scans (HOME overrides, PI_CONFIG_DIR,
|
|
10
|
+
// PI_CODING_AGENT_DIR ownership, ~ expansion, Windows/WSL paths).
|
|
11
|
+
const {
|
|
12
|
+
resolveOmpHome,
|
|
13
|
+
resolveOmpAgentDir,
|
|
14
|
+
} = require("./rollout");
|
|
15
|
+
|
|
16
|
+
const EXTENSION_FILENAME = "tokentracker-notify.ts";
|
|
17
|
+
const MARKER = "// @tokentracker-managed-omp-extension";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Test-only injection points for deterministic race repros.
|
|
21
|
+
* Production code never sets these; tests may assign async functions:
|
|
22
|
+
* - beforeExclusiveCreate(extensionPath)
|
|
23
|
+
* - beforeManagedWrite(extensionPath, identity)
|
|
24
|
+
* - beforeUnlink(extensionPath, identity)
|
|
25
|
+
* - afterStagingRename(extensionPath, stagingPath)
|
|
26
|
+
*/
|
|
27
|
+
const _testHooks = {
|
|
28
|
+
beforeExclusiveCreate: null,
|
|
29
|
+
beforeManagedWrite: null,
|
|
30
|
+
beforeUnlink: null,
|
|
31
|
+
afterStagingRename: null,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function resolveOmpExtensionsDir(env = process.env) {
|
|
35
|
+
const agentDir = resolveOmpAgentDir(env);
|
|
36
|
+
if (!agentDir) return null;
|
|
37
|
+
return path.join(agentDir, "extensions");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function resolveTrackerBinDir(trackerDir) {
|
|
41
|
+
if (!trackerDir) throw new Error("trackerDir is required");
|
|
42
|
+
return path.basename(trackerDir) === "tracker"
|
|
43
|
+
? path.join(path.dirname(trackerDir), "bin")
|
|
44
|
+
: path.join(trackerDir, "bin");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Extension source written into ~/.omp/agent/extensions/tokentracker-notify.ts.
|
|
49
|
+
* Uses require()-style node builtins so omp's extension rewriter can parse it.
|
|
50
|
+
* Spawns notify.cjs --source=omp (already in AUTO_SYNC_SOURCES) after turns.
|
|
51
|
+
*/
|
|
52
|
+
function buildOmpNotifyExtensionSource({ notifyPath }) {
|
|
53
|
+
const notifyLiteral = JSON.stringify(notifyPath);
|
|
54
|
+
return `${MARKER}
|
|
55
|
+
// TokenTracker notify bridge for oh-my-pi (omp).
|
|
56
|
+
// Passive session scanning still works without this file; the extension only
|
|
57
|
+
// makes local dashboard refresh near-real-time after each agent turn.
|
|
58
|
+
// Managed by TokenTracker init/uninstall — do not hand-edit the marker line.
|
|
59
|
+
|
|
60
|
+
const { spawn } = require("node:child_process");
|
|
61
|
+
const fs = require("node:fs");
|
|
62
|
+
const os = require("node:os");
|
|
63
|
+
const path = require("node:path");
|
|
64
|
+
|
|
65
|
+
const SOURCE = "omp";
|
|
66
|
+
const MIN_INTERVAL_MS = 12_000;
|
|
67
|
+
const MANAGED_NOTIFY = ${notifyLiteral};
|
|
68
|
+
|
|
69
|
+
let lastSpawnAt = 0;
|
|
70
|
+
let pendingTimer = null;
|
|
71
|
+
let warnedMissing = false;
|
|
72
|
+
|
|
73
|
+
function resolveNotifyPath() {
|
|
74
|
+
const candidates = [];
|
|
75
|
+
if (process.env.TOKENTRACKER_NOTIFY) candidates.push(process.env.TOKENTRACKER_NOTIFY);
|
|
76
|
+
if (MANAGED_NOTIFY) candidates.push(MANAGED_NOTIFY);
|
|
77
|
+
candidates.push(path.join(os.homedir(), ".tokentracker", "bin", "notify.cjs"));
|
|
78
|
+
for (const candidate of candidates) {
|
|
79
|
+
try {
|
|
80
|
+
if (candidate && fs.existsSync(candidate)) return candidate;
|
|
81
|
+
} catch (_) {}
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function spawnNotify() {
|
|
87
|
+
const notifyPath = resolveNotifyPath();
|
|
88
|
+
if (!notifyPath) {
|
|
89
|
+
if (!warnedMissing) {
|
|
90
|
+
warnedMissing = true;
|
|
91
|
+
console.warn(
|
|
92
|
+
"[tokentracker-notify] notify.cjs not found; expected ~/.tokentracker/bin/notify.cjs",
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const now = Date.now();
|
|
99
|
+
if (now - lastSpawnAt < MIN_INTERVAL_MS) {
|
|
100
|
+
if (pendingTimer == null) {
|
|
101
|
+
const wait = Math.max(MIN_INTERVAL_MS - (now - lastSpawnAt), 50);
|
|
102
|
+
pendingTimer = setTimeout(() => {
|
|
103
|
+
pendingTimer = null;
|
|
104
|
+
spawnNotify();
|
|
105
|
+
}, wait);
|
|
106
|
+
if (typeof pendingTimer.unref === "function") pendingTimer.unref();
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
lastSpawnAt = now;
|
|
111
|
+
if (pendingTimer != null) {
|
|
112
|
+
clearTimeout(pendingTimer);
|
|
113
|
+
pendingTimer = null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const child = spawn(process.execPath, [notifyPath, "--source=" + SOURCE], {
|
|
118
|
+
detached: true,
|
|
119
|
+
stdio: "ignore",
|
|
120
|
+
env: process.env,
|
|
121
|
+
});
|
|
122
|
+
child.unref();
|
|
123
|
+
} catch (err) {
|
|
124
|
+
if (!warnedMissing) {
|
|
125
|
+
warnedMissing = true;
|
|
126
|
+
console.warn(
|
|
127
|
+
"[tokentracker-notify] failed to spawn notify:",
|
|
128
|
+
err && err.message ? err.message : String(err),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export default function (pi) {
|
|
135
|
+
pi.on("turn_end", () => {
|
|
136
|
+
spawnNotify();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
pi.on("agent_end", () => {
|
|
140
|
+
spawnNotify();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
pi.on("session_shutdown", () => {
|
|
144
|
+
lastSpawnAt = 0;
|
|
145
|
+
if (pendingTimer != null) {
|
|
146
|
+
clearTimeout(pendingTimer);
|
|
147
|
+
pendingTimer = null;
|
|
148
|
+
}
|
|
149
|
+
spawnNotify();
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function isManagedOmpExtension(content) {
|
|
156
|
+
return typeof content === "string" && content.includes(MARKER);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function fileIdentity(stat) {
|
|
160
|
+
if (!stat) return null;
|
|
161
|
+
return { dev: stat.dev, ino: stat.ino };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function sameFileIdentity(a, b) {
|
|
165
|
+
return Boolean(a && b && a.dev === b.dev && a.ino === b.ino);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function pathIdentity(filePath) {
|
|
169
|
+
try {
|
|
170
|
+
return fileIdentity(await fs.stat(filePath));
|
|
171
|
+
} catch (err) {
|
|
172
|
+
if (err && err.code === "ENOENT") return null;
|
|
173
|
+
throw err;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function probeOmpHookState({ home = os.homedir(), trackerDir, env = process.env } = {}) {
|
|
178
|
+
const agentDir = resolveOmpAgentDir(env);
|
|
179
|
+
const extensionsDir = resolveOmpExtensionsDir(env);
|
|
180
|
+
const extensionPath = extensionsDir
|
|
181
|
+
? path.join(extensionsDir, EXTENSION_FILENAME)
|
|
182
|
+
: null;
|
|
183
|
+
const sessionsDir = agentDir ? path.join(agentDir, "sessions") : null;
|
|
184
|
+
const ompPresent = Boolean(
|
|
185
|
+
(agentDir && fssync.existsSync(agentDir)) ||
|
|
186
|
+
(sessionsDir && fssync.existsSync(sessionsDir)),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
let configured = false;
|
|
190
|
+
let managed = false;
|
|
191
|
+
let exists = false;
|
|
192
|
+
if (extensionPath && fssync.existsSync(extensionPath)) {
|
|
193
|
+
exists = true;
|
|
194
|
+
try {
|
|
195
|
+
const content = await fs.readFile(extensionPath, "utf8");
|
|
196
|
+
managed = isManagedOmpExtension(content);
|
|
197
|
+
// Treat any tokentracker-notify extension as configured for status UX.
|
|
198
|
+
// Ownership decisions (overwrite/remove) still require the exact MARKER.
|
|
199
|
+
configured = managed || /notify\.cjs|--source=omp|tokentracker/i.test(content);
|
|
200
|
+
} catch {
|
|
201
|
+
configured = false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
ompPresent,
|
|
207
|
+
agentDir,
|
|
208
|
+
extensionsDir,
|
|
209
|
+
extensionPath,
|
|
210
|
+
exists,
|
|
211
|
+
configured,
|
|
212
|
+
managed,
|
|
213
|
+
sessionsDir,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Write managed source through an already-opened handle, then confirm the
|
|
219
|
+
* directory entry still refers to that same inode (so a path replacement
|
|
220
|
+
* during the write cannot be reported as success against a user file).
|
|
221
|
+
*/
|
|
222
|
+
async function writeManagedThroughHandle(handle, extensionPath, source, openedIdentity) {
|
|
223
|
+
if (typeof _testHooks.beforeManagedWrite === "function") {
|
|
224
|
+
await _testHooks.beforeManagedWrite(extensionPath, openedIdentity);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let pathIdBefore;
|
|
228
|
+
try {
|
|
229
|
+
pathIdBefore = await pathIdentity(extensionPath);
|
|
230
|
+
} catch (err) {
|
|
231
|
+
return {
|
|
232
|
+
written: false,
|
|
233
|
+
skippedReason: "identity-check-failed",
|
|
234
|
+
error: String(err?.message || err),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
if (!sameFileIdentity(pathIdBefore, openedIdentity)) {
|
|
238
|
+
return { written: false, skippedReason: "identity-changed" };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
await handle.truncate(0);
|
|
243
|
+
const sourceBytes = Buffer.from(source, "utf8");
|
|
244
|
+
let offset = 0;
|
|
245
|
+
while (offset < sourceBytes.length) {
|
|
246
|
+
const { bytesWritten } = await handle.write(
|
|
247
|
+
sourceBytes,
|
|
248
|
+
offset,
|
|
249
|
+
sourceBytes.length - offset,
|
|
250
|
+
offset,
|
|
251
|
+
);
|
|
252
|
+
if (bytesWritten <= 0) {
|
|
253
|
+
throw new Error("Failed to write managed oh-my-pi extension");
|
|
254
|
+
}
|
|
255
|
+
offset += bytesWritten;
|
|
256
|
+
}
|
|
257
|
+
} catch (err) {
|
|
258
|
+
return {
|
|
259
|
+
written: false,
|
|
260
|
+
skippedReason: "extension-write-failed",
|
|
261
|
+
error: String(err?.message || err),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
let pathIdAfter;
|
|
266
|
+
try {
|
|
267
|
+
pathIdAfter = await pathIdentity(extensionPath);
|
|
268
|
+
} catch (err) {
|
|
269
|
+
return {
|
|
270
|
+
written: false,
|
|
271
|
+
skippedReason: "identity-check-failed",
|
|
272
|
+
error: String(err?.message || err),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
if (!sameFileIdentity(pathIdAfter, openedIdentity)) {
|
|
276
|
+
// Path now points elsewhere (or is gone). The handle write hit the
|
|
277
|
+
// original managed inode only; do not claim ownership of the path.
|
|
278
|
+
return { written: false, skippedReason: "identity-changed" };
|
|
279
|
+
}
|
|
280
|
+
return { written: true };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function upsertOmpHook({ home = os.homedir(), trackerDir, env = process.env } = {}) {
|
|
284
|
+
if (!trackerDir) throw new Error("trackerDir is required");
|
|
285
|
+
const binDir = resolveTrackerBinDir(trackerDir);
|
|
286
|
+
const notifyPath = path.join(binDir, "notify.cjs");
|
|
287
|
+
const extensionsDir = resolveOmpExtensionsDir(env);
|
|
288
|
+
if (!extensionsDir) {
|
|
289
|
+
return {
|
|
290
|
+
written: false,
|
|
291
|
+
skippedReason: "omp-agent-dir-unresolved",
|
|
292
|
+
extensionPath: null,
|
|
293
|
+
notifyPath,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
const extensionPath = path.join(extensionsDir, EXTENSION_FILENAME);
|
|
297
|
+
const source = buildOmpNotifyExtensionSource({ notifyPath });
|
|
298
|
+
|
|
299
|
+
await fs.mkdir(extensionsDir, { recursive: true });
|
|
300
|
+
|
|
301
|
+
if (typeof _testHooks.beforeExclusiveCreate === "function") {
|
|
302
|
+
await _testHooks.beforeExclusiveCreate(extensionPath);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Create path: exclusive open so a concurrent user create cannot be clobbered.
|
|
306
|
+
try {
|
|
307
|
+
const createHandle = await fs.open(extensionPath, "wx");
|
|
308
|
+
try {
|
|
309
|
+
await createHandle.writeFile(source, "utf8");
|
|
310
|
+
} finally {
|
|
311
|
+
await createHandle.close();
|
|
312
|
+
}
|
|
313
|
+
const state = await probeOmpHookState({ home, trackerDir, env });
|
|
314
|
+
return {
|
|
315
|
+
written: true,
|
|
316
|
+
extensionPath,
|
|
317
|
+
notifyPath,
|
|
318
|
+
state,
|
|
319
|
+
};
|
|
320
|
+
} catch (err) {
|
|
321
|
+
if (!err || err.code !== "EEXIST") {
|
|
322
|
+
return {
|
|
323
|
+
written: false,
|
|
324
|
+
skippedReason: "extension-write-failed",
|
|
325
|
+
error: String(err?.message || err),
|
|
326
|
+
extensionPath,
|
|
327
|
+
notifyPath,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
// Fall through: path exists — only rewrite if still our managed inode.
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Update path: open by path, verify marker on the opened inode, mutate only
|
|
334
|
+
// that inode, and refuse if the directory entry changes identity mid-flight.
|
|
335
|
+
let handle;
|
|
336
|
+
try {
|
|
337
|
+
handle = await fs.open(extensionPath, "r+");
|
|
338
|
+
} catch (err) {
|
|
339
|
+
if (err && err.code === "ENOENT") {
|
|
340
|
+
// Lost the race to a delete; retry exclusive create once without the
|
|
341
|
+
// create-hook (hook already ran for the first attempt).
|
|
342
|
+
try {
|
|
343
|
+
const createHandle = await fs.open(extensionPath, "wx");
|
|
344
|
+
try {
|
|
345
|
+
await createHandle.writeFile(source, "utf8");
|
|
346
|
+
} finally {
|
|
347
|
+
await createHandle.close();
|
|
348
|
+
}
|
|
349
|
+
const state = await probeOmpHookState({ home, trackerDir, env });
|
|
350
|
+
return { written: true, extensionPath, notifyPath, state };
|
|
351
|
+
} catch (retryErr) {
|
|
352
|
+
if (retryErr && retryErr.code === "EEXIST") {
|
|
353
|
+
// Concurrent create won; re-enter ownership check below.
|
|
354
|
+
try {
|
|
355
|
+
handle = await fs.open(extensionPath, "r+");
|
|
356
|
+
} catch (openErr) {
|
|
357
|
+
return {
|
|
358
|
+
written: false,
|
|
359
|
+
skippedReason: "extension-read-failed",
|
|
360
|
+
error: String(openErr?.message || openErr),
|
|
361
|
+
extensionPath,
|
|
362
|
+
notifyPath,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
} else {
|
|
366
|
+
return {
|
|
367
|
+
written: false,
|
|
368
|
+
skippedReason: "extension-write-failed",
|
|
369
|
+
error: String(retryErr?.message || retryErr),
|
|
370
|
+
extensionPath,
|
|
371
|
+
notifyPath,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
} else {
|
|
376
|
+
return {
|
|
377
|
+
written: false,
|
|
378
|
+
skippedReason: "extension-read-failed",
|
|
379
|
+
error: String(err?.message || err),
|
|
380
|
+
extensionPath,
|
|
381
|
+
notifyPath,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
const existing = await handle.readFile("utf8");
|
|
388
|
+
if (!isManagedOmpExtension(existing)) {
|
|
389
|
+
return {
|
|
390
|
+
written: false,
|
|
391
|
+
skippedReason: "unmanaged-extension-present",
|
|
392
|
+
extensionPath,
|
|
393
|
+
notifyPath,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const openedIdentity = fileIdentity(await handle.stat());
|
|
398
|
+
const writeResult = await writeManagedThroughHandle(
|
|
399
|
+
handle,
|
|
400
|
+
extensionPath,
|
|
401
|
+
source,
|
|
402
|
+
openedIdentity,
|
|
403
|
+
);
|
|
404
|
+
if (!writeResult.written) {
|
|
405
|
+
return {
|
|
406
|
+
written: false,
|
|
407
|
+
skippedReason: writeResult.skippedReason,
|
|
408
|
+
...(writeResult.error ? { error: writeResult.error } : {}),
|
|
409
|
+
extensionPath,
|
|
410
|
+
notifyPath,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const state = await probeOmpHookState({ home, trackerDir, env });
|
|
415
|
+
return {
|
|
416
|
+
written: true,
|
|
417
|
+
extensionPath,
|
|
418
|
+
notifyPath,
|
|
419
|
+
state,
|
|
420
|
+
};
|
|
421
|
+
} finally {
|
|
422
|
+
await handle.close().catch(() => {});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Restore without replacing a concurrent destination; keep staging on failure.
|
|
427
|
+
async function restoreStagedExtension(stagingPath, extensionPath) {
|
|
428
|
+
try {
|
|
429
|
+
await fs.copyFile(stagingPath, extensionPath, fssync.constants.COPYFILE_EXCL);
|
|
430
|
+
} catch (error) {
|
|
431
|
+
return { restored: false, error };
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
try {
|
|
435
|
+
await fs.unlink(stagingPath);
|
|
436
|
+
} catch (error) {
|
|
437
|
+
return { restored: true, staleStaging: true, error };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return { restored: true };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async function removeOmpHook({ home = os.homedir(), trackerDir, env = process.env } = {}) {
|
|
444
|
+
const extensionsDir = resolveOmpExtensionsDir(env);
|
|
445
|
+
if (!extensionsDir) {
|
|
446
|
+
return { removed: false, skippedReason: "omp-agent-dir-unresolved", extensionPath: null };
|
|
447
|
+
}
|
|
448
|
+
const extensionPath = path.join(extensionsDir, EXTENSION_FILENAME);
|
|
449
|
+
|
|
450
|
+
// Peek first so the common unmanaged/missing cases do not thrash the path.
|
|
451
|
+
try {
|
|
452
|
+
const peek = await fs.readFile(extensionPath, "utf8");
|
|
453
|
+
if (!isManagedOmpExtension(peek)) {
|
|
454
|
+
return { removed: false, skippedReason: "unmanaged", extensionPath };
|
|
455
|
+
}
|
|
456
|
+
} catch (err) {
|
|
457
|
+
if (err && err.code === "ENOENT") {
|
|
458
|
+
return { removed: false, skippedReason: "missing", extensionPath };
|
|
459
|
+
}
|
|
460
|
+
return {
|
|
461
|
+
removed: false,
|
|
462
|
+
skippedReason: "extension-read-failed",
|
|
463
|
+
error: String(err?.message || err),
|
|
464
|
+
extensionPath,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (typeof _testHooks.beforeUnlink === "function") {
|
|
469
|
+
await _testHooks.beforeUnlink(extensionPath, null);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Atomically claim the directory entry via rename. The staged inode remains
|
|
473
|
+
// private until restoreStagedExtension copies it back without replacing a
|
|
474
|
+
// concurrent replacement at the original path.
|
|
475
|
+
const stagingPath = path.join(
|
|
476
|
+
extensionsDir,
|
|
477
|
+
`.${EXTENSION_FILENAME}.tokentracker-removing-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
try {
|
|
481
|
+
await fs.rename(extensionPath, stagingPath);
|
|
482
|
+
} catch (err) {
|
|
483
|
+
if (err && err.code === "ENOENT") {
|
|
484
|
+
return { removed: false, skippedReason: "missing", extensionPath };
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
removed: false,
|
|
488
|
+
skippedReason: "unlink-failed",
|
|
489
|
+
error: String(err?.message || err),
|
|
490
|
+
extensionPath,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (typeof _testHooks.afterStagingRename === "function") {
|
|
495
|
+
await _testHooks.afterStagingRename(extensionPath, stagingPath);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
let stagedContent;
|
|
499
|
+
try {
|
|
500
|
+
stagedContent = await fs.readFile(stagingPath, "utf8");
|
|
501
|
+
} catch (err) {
|
|
502
|
+
// Best-effort restore if we cannot inspect what we claimed.
|
|
503
|
+
const restoreResult = await restoreStagedExtension(stagingPath, extensionPath);
|
|
504
|
+
return {
|
|
505
|
+
removed: false,
|
|
506
|
+
skippedReason: "extension-read-failed",
|
|
507
|
+
error: String(err?.message || err),
|
|
508
|
+
extensionPath,
|
|
509
|
+
...(restoreResult.staleStaging
|
|
510
|
+
? {
|
|
511
|
+
staleStaging: true,
|
|
512
|
+
stagedPath: stagingPath,
|
|
513
|
+
stagingError: String(restoreResult.error?.message || restoreResult.error),
|
|
514
|
+
}
|
|
515
|
+
: restoreResult.restored
|
|
516
|
+
? {}
|
|
517
|
+
: { stagedPath: stagingPath }),
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (!isManagedOmpExtension(stagedContent)) {
|
|
522
|
+
// Restored user/replacement file under the original path.
|
|
523
|
+
const restoreResult = await restoreStagedExtension(stagingPath, extensionPath);
|
|
524
|
+
if (!restoreResult.restored) {
|
|
525
|
+
// If the original path was recreated, leave the staged file for the user.
|
|
526
|
+
return {
|
|
527
|
+
removed: false,
|
|
528
|
+
skippedReason: "identity-changed",
|
|
529
|
+
error: String(restoreResult.error?.message || restoreResult.error),
|
|
530
|
+
extensionPath,
|
|
531
|
+
stagedPath: stagingPath,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
return {
|
|
535
|
+
removed: false,
|
|
536
|
+
skippedReason: "identity-changed",
|
|
537
|
+
extensionPath,
|
|
538
|
+
...(restoreResult.staleStaging
|
|
539
|
+
? {
|
|
540
|
+
staleStaging: true,
|
|
541
|
+
stagedPath: stagingPath,
|
|
542
|
+
stagingError: String(restoreResult.error?.message || restoreResult.error),
|
|
543
|
+
}
|
|
544
|
+
: {}),
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
try {
|
|
549
|
+
await fs.unlink(stagingPath);
|
|
550
|
+
} catch (err) {
|
|
551
|
+
if (err?.code === "ENOENT") {
|
|
552
|
+
return { removed: true, extensionPath };
|
|
553
|
+
}
|
|
554
|
+
// Restore the managed file so uninstall does not leave a silent orphan
|
|
555
|
+
// under a temp name when the final delete is blocked.
|
|
556
|
+
const restoreResult = await restoreStagedExtension(stagingPath, extensionPath);
|
|
557
|
+
return {
|
|
558
|
+
removed: false,
|
|
559
|
+
skippedReason: "unlink-failed",
|
|
560
|
+
error: String(err?.message || err),
|
|
561
|
+
extensionPath,
|
|
562
|
+
...(restoreResult.staleStaging
|
|
563
|
+
? {
|
|
564
|
+
staleStaging: true,
|
|
565
|
+
stagedPath: stagingPath,
|
|
566
|
+
stagingError: String(restoreResult.error?.message || restoreResult.error),
|
|
567
|
+
}
|
|
568
|
+
: restoreResult.restored
|
|
569
|
+
? {}
|
|
570
|
+
: { stagedPath: stagingPath }),
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return { removed: true, extensionPath };
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
module.exports = {
|
|
578
|
+
EXTENSION_FILENAME,
|
|
579
|
+
MARKER,
|
|
580
|
+
resolveOmpHome,
|
|
581
|
+
resolveOmpAgentDir,
|
|
582
|
+
resolveOmpExtensionsDir,
|
|
583
|
+
buildOmpNotifyExtensionSource,
|
|
584
|
+
isManagedOmpExtension,
|
|
585
|
+
probeOmpHookState,
|
|
586
|
+
upsertOmpHook,
|
|
587
|
+
removeOmpHook,
|
|
588
|
+
_testHooks,
|
|
589
|
+
};
|