pushary 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1943 -0
- package/LICENSE +21 -0
- package/README.md +330 -0
- package/data/SKILL.md +591 -0
- package/data/cowork/SKILL.md +77 -0
- package/data/cursor-plugin/.cursor-plugin/plugin.json +25 -0
- package/data/cursor-plugin/CHANGELOG.md +70 -0
- package/data/cursor-plugin/CONTRIBUTING.md +40 -0
- package/data/cursor-plugin/LICENSE +21 -0
- package/data/cursor-plugin/README.md +113 -0
- package/data/cursor-plugin/SECURITY.md +33 -0
- package/data/cursor-plugin/assets/logo.png +0 -0
- package/data/cursor-plugin/commands/notify-when-done.md +14 -0
- package/data/cursor-plugin/commands/pushary-test.md +13 -0
- package/data/cursor-plugin/hooks/hooks.json +75 -0
- package/data/cursor-plugin/mcp.json +11 -0
- package/data/cursor-plugin/rules/pushary.mdc +40 -0
- package/data/cursor-plugin/scripts/pushary-gate.mjs +724 -0
- package/data/cursor-plugin/scripts/pushary-gate.test.mjs +137 -0
- package/data/cursor-plugin/scripts/redaction.mjs +51 -0
- package/data/cursor-plugin/skills/pushary/SKILL.md +584 -0
- package/data/vscode-plugin/.claude-plugin/plugin.json +28 -0
- package/data/vscode-plugin/.mcp.json +11 -0
- package/data/vscode-plugin/CHANGELOG.md +49 -0
- package/data/vscode-plugin/CONTRIBUTING.md +40 -0
- package/data/vscode-plugin/LICENSE +21 -0
- package/data/vscode-plugin/README.md +128 -0
- package/data/vscode-plugin/SECURITY.md +56 -0
- package/data/vscode-plugin/assets/logo.png +0 -0
- package/data/vscode-plugin/commands/notify-when-done.md +14 -0
- package/data/vscode-plugin/commands/pushary-test.md +13 -0
- package/data/vscode-plugin/hooks/hooks.json +60 -0
- package/data/vscode-plugin/scripts/pushary-gate.mjs +762 -0
- package/data/vscode-plugin/scripts/redaction.mjs +51 -0
- package/data/vscode-plugin/skills/pushary/SKILL.md +584 -0
- package/dist/bin/pushary-bell-hook.d.ts +1 -0
- package/dist/bin/pushary-bell-hook.js +46 -0
- package/dist/bin/pushary-bell.d.ts +1 -0
- package/dist/bin/pushary-bell.js +146 -0
- package/dist/bin/pushary-claude.d.ts +1 -0
- package/dist/bin/pushary-claude.js +1916 -0
- package/dist/bin/pushary-clean.d.ts +1 -0
- package/dist/bin/pushary-clean.js +1300 -0
- package/dist/bin/pushary-codex-bridge.d.ts +1 -0
- package/dist/bin/pushary-codex-bridge.js +599 -0
- package/dist/bin/pushary-codex-hook.d.ts +1 -0
- package/dist/bin/pushary-codex-hook.js +886 -0
- package/dist/bin/pushary-codex.d.ts +1 -0
- package/dist/bin/pushary-codex.js +125 -0
- package/dist/bin/pushary-connect.d.ts +1 -0
- package/dist/bin/pushary-connect.js +100 -0
- package/dist/bin/pushary-cowork.d.ts +1 -0
- package/dist/bin/pushary-cowork.js +36 -0
- package/dist/bin/pushary-daemon-supervisor.d.ts +1 -0
- package/dist/bin/pushary-daemon-supervisor.js +27 -0
- package/dist/bin/pushary-daemon.d.ts +1 -0
- package/dist/bin/pushary-daemon.js +878 -0
- package/dist/bin/pushary-disconnect.d.ts +1 -0
- package/dist/bin/pushary-disconnect.js +264 -0
- package/dist/bin/pushary-doctor.d.ts +1 -0
- package/dist/bin/pushary-doctor.js +1433 -0
- package/dist/bin/pushary-elicitation-hook.d.ts +1 -0
- package/dist/bin/pushary-elicitation-hook.js +149 -0
- package/dist/bin/pushary-gemini-bridge.d.ts +1 -0
- package/dist/bin/pushary-gemini-bridge.js +339 -0
- package/dist/bin/pushary-gemini-hook.d.ts +1 -0
- package/dist/bin/pushary-gemini-hook.js +381 -0
- package/dist/bin/pushary-hook.d.ts +1 -0
- package/dist/bin/pushary-hook.js +95 -0
- package/dist/bin/pushary-login.d.ts +1 -0
- package/dist/bin/pushary-login.js +198 -0
- package/dist/bin/pushary-logout.d.ts +1 -0
- package/dist/bin/pushary-logout.js +147 -0
- package/dist/bin/pushary-mcp.d.ts +1 -0
- package/dist/bin/pushary-mcp.js +114 -0
- package/dist/bin/pushary-mode.d.ts +1 -0
- package/dist/bin/pushary-mode.js +130 -0
- package/dist/bin/pushary-notification-hook.d.ts +1 -0
- package/dist/bin/pushary-notification-hook.js +56 -0
- package/dist/bin/pushary-opencode-hook.d.ts +1 -0
- package/dist/bin/pushary-opencode-hook.js +338 -0
- package/dist/bin/pushary-permission-denied-hook.d.ts +1 -0
- package/dist/bin/pushary-permission-denied-hook.js +60 -0
- package/dist/bin/pushary-permission-hook.d.ts +1 -0
- package/dist/bin/pushary-permission-hook.js +65 -0
- package/dist/bin/pushary-post-hook.d.ts +1 -0
- package/dist/bin/pushary-post-hook.js +70 -0
- package/dist/bin/pushary-prompt-hook.d.ts +1 -0
- package/dist/bin/pushary-prompt-hook.js +61 -0
- package/dist/bin/pushary-session-end-hook.d.ts +1 -0
- package/dist/bin/pushary-session-end-hook.js +59 -0
- package/dist/bin/pushary-session-start-hook.d.ts +1 -0
- package/dist/bin/pushary-session-start-hook.js +72 -0
- package/dist/bin/pushary-setup.d.ts +1 -0
- package/dist/bin/pushary-setup.js +2660 -0
- package/dist/bin/pushary-stats.d.ts +1 -0
- package/dist/bin/pushary-stats.js +44 -0
- package/dist/bin/pushary-status.d.ts +1 -0
- package/dist/bin/pushary-status.js +262 -0
- package/dist/bin/pushary-stop-hook.d.ts +1 -0
- package/dist/bin/pushary-stop-hook.js +63 -0
- package/dist/bin/pushary-stopfailure-hook.d.ts +1 -0
- package/dist/bin/pushary-stopfailure-hook.js +56 -0
- package/dist/bin/pushary-suggestions.d.ts +1 -0
- package/dist/bin/pushary-suggestions.js +100 -0
- package/dist/bin/pushary-transcript-register.d.ts +1 -0
- package/dist/bin/pushary-transcript-register.js +40 -0
- package/dist/bin/pushary-transcripts.d.ts +1 -0
- package/dist/bin/pushary-transcripts.js +77 -0
- package/dist/bin/pushary-upgrade.d.ts +1 -0
- package/dist/bin/pushary-upgrade.js +599 -0
- package/dist/bin/pushary-wait.d.ts +1 -0
- package/dist/bin/pushary-wait.js +122 -0
- package/dist/bin/pushary.d.ts +1 -0
- package/dist/bin/pushary.js +74 -0
- package/dist/chunk-2ABTSGFT.js +173 -0
- package/dist/chunk-2OY3ZZ5N.js +124 -0
- package/dist/chunk-3EGEA4KH.js +44 -0
- package/dist/chunk-3EVPNIBE.js +185 -0
- package/dist/chunk-5OP5MQI7.js +45 -0
- package/dist/chunk-6J2JC2RT.js +149 -0
- package/dist/chunk-7QLSKOSU.js +19 -0
- package/dist/chunk-7Z7Q4GGS.js +88 -0
- package/dist/chunk-A2A2YTMG.js +835 -0
- package/dist/chunk-AHV4LNB5.js +377 -0
- package/dist/chunk-AXPCESYO.js +197 -0
- package/dist/chunk-C2WCPBF7.js +86 -0
- package/dist/chunk-CJVVKOSR.js +73 -0
- package/dist/chunk-CULUZTWQ.js +2117 -0
- package/dist/chunk-CVQYGMSR.js +142 -0
- package/dist/chunk-CVWG2N3Y.js +16 -0
- package/dist/chunk-D2FX3EPW.js +15 -0
- package/dist/chunk-DB3ONZB4.js +44 -0
- package/dist/chunk-DE4Y6TCC.js +160 -0
- package/dist/chunk-E4FJTUH4.js +249 -0
- package/dist/chunk-EPCXUP3P.js +41 -0
- package/dist/chunk-EX3GAPA4.js +44 -0
- package/dist/chunk-EYK3GSRH.js +143 -0
- package/dist/chunk-GMXKITVA.js +70 -0
- package/dist/chunk-GPEGOGRJ.js +49 -0
- package/dist/chunk-GU3EJCVV.js +43 -0
- package/dist/chunk-HQ6G3B5R.js +59 -0
- package/dist/chunk-HZVVNGEW.js +22 -0
- package/dist/chunk-IPVKH2ST.js +231 -0
- package/dist/chunk-J5TIV3J6.js +238 -0
- package/dist/chunk-JU6W3XAU.js +64 -0
- package/dist/chunk-JYG4B33G.js +227 -0
- package/dist/chunk-JZTHGAPA.js +503 -0
- package/dist/chunk-KGDWDSNL.js +102 -0
- package/dist/chunk-KVQBWI5T.js +99 -0
- package/dist/chunk-KZERVKTD.js +16 -0
- package/dist/chunk-LXQ5FBBD.js +47 -0
- package/dist/chunk-M5ICYGHT.js +138 -0
- package/dist/chunk-M5JDZ45W.js +943 -0
- package/dist/chunk-MDEPU45E.js +1226 -0
- package/dist/chunk-MPWXGONV.js +157 -0
- package/dist/chunk-N6IOSZOE.js +185 -0
- package/dist/chunk-NGDD6TXY.js +192 -0
- package/dist/chunk-NQTHFF4W.js +2830 -0
- package/dist/chunk-OKXS7WDZ.js +68 -0
- package/dist/chunk-OLQRRFBX.js +21 -0
- package/dist/chunk-P7WQEVWA.js +307 -0
- package/dist/chunk-POFBE6ZY.js +12 -0
- package/dist/chunk-PQ4K74WL.js +30 -0
- package/dist/chunk-PUZZELE6.js +37 -0
- package/dist/chunk-Q7E4I6UA.js +767 -0
- package/dist/chunk-QCGNGEGE.js +15 -0
- package/dist/chunk-R7437YI3.js +151 -0
- package/dist/chunk-RHR6NARM.js +305 -0
- package/dist/chunk-SAXBC4AZ.js +60 -0
- package/dist/chunk-SGZJ5LYH.js +76 -0
- package/dist/chunk-SIFVFP6T.js +70 -0
- package/dist/chunk-SLFABHW3.js +150 -0
- package/dist/chunk-T7E75EKA.js +67 -0
- package/dist/chunk-TY4JT7W3.js +38 -0
- package/dist/chunk-U4TMAANR.js +111 -0
- package/dist/chunk-UI36QSBI.js +177 -0
- package/dist/chunk-VQKRG3AC.js +339 -0
- package/dist/chunk-WE2J62NC.js +1131 -0
- package/dist/chunk-XHKBHWLX.js +14 -0
- package/dist/chunk-ZSZSSJCV.js +275 -0
- package/dist/chunk-ZVSKMPBZ.js +135 -0
- package/dist/reapply-CW3MA66V.js +34 -0
- package/dist/src/index.d.ts +204 -0
- package/dist/src/index.js +64 -0
- package/package.json +108 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HOOK_BUDGETS,
|
|
3
|
+
OPENCODE_APPROVAL_EVENT
|
|
4
|
+
} from "./chunk-NQTHFF4W.js";
|
|
5
|
+
|
|
6
|
+
// src/opencode-plugin.ts
|
|
7
|
+
var OPENCODE_PLUGIN_MARKER = "pushary-opencode-plugin w1";
|
|
8
|
+
var OPENCODE_PLUGIN_PLACEHOLDER = "__PUSHARY_COMMAND__";
|
|
9
|
+
var OPENCODE_PLUGIN_FILE = "pushary.js";
|
|
10
|
+
var OPENCODE_PLUGIN_DIR = "plugins";
|
|
11
|
+
var OPENCODE_PLUGIN_SIBLING = "package.json";
|
|
12
|
+
var OPENCODE_PLUGIN_SIBLING_CONTENTS = `${JSON.stringify({ type: "module" }, null, 2)}
|
|
13
|
+
`;
|
|
14
|
+
var OPENCODE_ASKED_EVENTS = ["permission.asked", "permission.updated"];
|
|
15
|
+
var OPENCODE_REPLIED_EVENT = "permission.replied";
|
|
16
|
+
var OPENCODE_IDLE_EVENT = "session.idle";
|
|
17
|
+
var approvalTimeoutMs = () => (HOOK_BUDGETS.opencode.budgetSeconds - HOOK_BUDGETS.opencode.guardSeconds) * 1e3;
|
|
18
|
+
var openCodePluginSource = () => `// Pushary for OpenCode. Generated by @pushary/agent-hooks \u2014 do not edit.
|
|
19
|
+
// ${OPENCODE_PLUGIN_MARKER}
|
|
20
|
+
//
|
|
21
|
+
// OpenCode finds a plugin by globbing {plugin,plugins}/*.{ts,js} under its config
|
|
22
|
+
// directory and importing it, so this file is the whole install.
|
|
23
|
+
//
|
|
24
|
+
// It decides nothing. Each hook builds the payload OpenCode handed it, writes it
|
|
25
|
+
// to the Pushary hook command on stdin, and reads a verdict back. The policy
|
|
26
|
+
// engine, the phone, the wait and the settlement all live behind that command.
|
|
27
|
+
//
|
|
28
|
+
// Approvals ride the event stream rather than the "permission.ask" hook, which is
|
|
29
|
+
// declared in @opencode-ai/plugin and called by nothing. Permission.ask publishes
|
|
30
|
+
// "permission.asked" and blocks on a Deferred that only a reply resolves, so this
|
|
31
|
+
// answers that reply on the person's behalf when Pushary has a verdict.
|
|
32
|
+
//
|
|
33
|
+
// Failure is silence. A missing command, a crash, a timeout, unreadable bytes and
|
|
34
|
+
// an unparseable reply all leave OpenCode's own prompt exactly as it was. The
|
|
35
|
+
// worst this file may do to somebody is nothing at all.
|
|
36
|
+
|
|
37
|
+
import { spawn } from "node:child_process"
|
|
38
|
+
|
|
39
|
+
const COMMAND = ${OPENCODE_PLUGIN_PLACEHOLDER}
|
|
40
|
+
const APPROVAL_TIMEOUT_MS = ${approvalTimeoutMs()}
|
|
41
|
+
const TELEMETRY_TIMEOUT_MS = 10000
|
|
42
|
+
const MAX_REPLY_BYTES = 65536
|
|
43
|
+
// One entry per call in flight. Bounded because a long session must not grow them
|
|
44
|
+
// without limit, and evicted oldest-first because whatever is still waiting on an
|
|
45
|
+
// answer is the newest thing in here, never the oldest.
|
|
46
|
+
const MAX_TRACKED = 256
|
|
47
|
+
|
|
48
|
+
const text = (value) => (typeof value === "string" && value.length > 0 ? value : undefined)
|
|
49
|
+
|
|
50
|
+
const remember = (map, key, value) => {
|
|
51
|
+
if (!key || value === undefined) return
|
|
52
|
+
if (map.size >= MAX_TRACKED) map.delete(map.keys().next().value)
|
|
53
|
+
map.set(key, value)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const run = (payload, timeoutMs, onSpawn) =>
|
|
57
|
+
new Promise((resolve) => {
|
|
58
|
+
let settled = false
|
|
59
|
+
const finish = (value) => {
|
|
60
|
+
if (settled) return
|
|
61
|
+
settled = true
|
|
62
|
+
clearTimeout(timer)
|
|
63
|
+
resolve(value)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let child
|
|
67
|
+
try {
|
|
68
|
+
child = spawn(COMMAND[0], COMMAND.slice(1), { stdio: ["pipe", "pipe", "ignore"] })
|
|
69
|
+
} catch {
|
|
70
|
+
return finish(null)
|
|
71
|
+
}
|
|
72
|
+
if (onSpawn) onSpawn(child)
|
|
73
|
+
|
|
74
|
+
const timer = setTimeout(() => {
|
|
75
|
+
try {
|
|
76
|
+
child.kill("SIGKILL")
|
|
77
|
+
} catch {}
|
|
78
|
+
finish(null)
|
|
79
|
+
}, timeoutMs)
|
|
80
|
+
if (typeof timer.unref === "function") timer.unref()
|
|
81
|
+
|
|
82
|
+
let out = ""
|
|
83
|
+
child.stdout.on("data", (chunk) => {
|
|
84
|
+
if (out.length < MAX_REPLY_BYTES) out += chunk
|
|
85
|
+
})
|
|
86
|
+
child.on("error", () => finish(null))
|
|
87
|
+
child.on("close", () => {
|
|
88
|
+
const body = out.trim()
|
|
89
|
+
if (!body) return finish(null)
|
|
90
|
+
try {
|
|
91
|
+
finish(JSON.parse(body))
|
|
92
|
+
} catch {
|
|
93
|
+
finish(null)
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
child.stdin.on("error", () => {})
|
|
99
|
+
child.stdin.end(JSON.stringify(payload))
|
|
100
|
+
} catch {
|
|
101
|
+
finish(null)
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
// Telemetry is fire and forget: the agent has already acted and is only being
|
|
106
|
+
// observed, so nothing here may hold up the next turn.
|
|
107
|
+
const tell = (payload) => {
|
|
108
|
+
void run(payload, TELEMETRY_TIMEOUT_MS)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// What OpenCode gates on, and what it hands over with it. Verified against its own
|
|
112
|
+
// tools: bash asks with { command }, write and edit both ask under "edit" with
|
|
113
|
+
// { filepath, diff }, webfetch with { url }, and a shell command reaching outside
|
|
114
|
+
// the worktree asks a second time under "external_directory".
|
|
115
|
+
const inputFrom = (permission, metadata) => {
|
|
116
|
+
const meta = metadata && typeof metadata === "object" ? metadata : {}
|
|
117
|
+
const command = text(meta.command)
|
|
118
|
+
if (command) return { command }
|
|
119
|
+
const filepath = text(meta.filepath) ?? text(meta.file_path)
|
|
120
|
+
// The diff rides along, because it is what the person is actually approving.
|
|
121
|
+
// OpenCode computes it before raising the permission and hands it over; without
|
|
122
|
+
// it the phone shows a filename and nothing to judge.
|
|
123
|
+
if (filepath) return { file_path: filepath, ...(text(meta.diff) ? { diff: meta.diff } : {}) }
|
|
124
|
+
const url = text(meta.url)
|
|
125
|
+
if (url) return { url }
|
|
126
|
+
return { permission }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Two spellings of one request: the runtime publishes PermissionV1.Request
|
|
130
|
+
// ({ permission, patterns }), the generated SDK types say Permission ({ type,
|
|
131
|
+
// pattern }). Whichever arrives, the id and the session are what we need.
|
|
132
|
+
const askedFrom = (properties) => {
|
|
133
|
+
if (!properties || typeof properties !== "object") return null
|
|
134
|
+
const id = text(properties.id)
|
|
135
|
+
const sessionID = text(properties.sessionID)
|
|
136
|
+
if (!id || !sessionID) return null
|
|
137
|
+
return {
|
|
138
|
+
id,
|
|
139
|
+
sessionID,
|
|
140
|
+
permission: text(properties.permission) ?? text(properties.type) ?? "unknown",
|
|
141
|
+
metadata: properties.metadata,
|
|
142
|
+
title: text(properties.title),
|
|
143
|
+
callID: text(properties.callID) ?? text(properties.tool && properties.tool.callID),
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const repliedID = (properties) => {
|
|
148
|
+
if (!properties || typeof properties !== "object") return undefined
|
|
149
|
+
return text(properties.requestID) ?? text(properties.permissionID) ?? text(properties.id)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export const PusharyPlugin = async ({ client, directory, worktree }) => {
|
|
153
|
+
const cwd = worktree || directory || process.cwd()
|
|
154
|
+
// OpenCode raises a permission under a coarse key \u2014 "edit" covers both write and
|
|
155
|
+
// edit \u2014 and names the actual tool only on tool.execute.before. Keeping the
|
|
156
|
+
// pairing is what lets a rule written for Write mean Write here too.
|
|
157
|
+
const tools = new Map()
|
|
158
|
+
// Every gate still waiting. Held so a person answering in the terminal can end
|
|
159
|
+
// ours: whoever answers first wins, and the loser stops asking.
|
|
160
|
+
const waiting = new Map()
|
|
161
|
+
|
|
162
|
+
const base = (sessionID) => ({ session_id: sessionID ?? "", cwd })
|
|
163
|
+
|
|
164
|
+
// Replying is the whole approval. Two shapes, because the client handed to a
|
|
165
|
+
// plugin is generated: v1 exposes postSessionIdPermissionsPermissionId against
|
|
166
|
+
// /session/{id}/permissions/{permissionID}, v2 exposes permission.reply against
|
|
167
|
+
// /permission/{requestID}/reply. Both routes are live in the server today.
|
|
168
|
+
const respond = async (asked, allow) => {
|
|
169
|
+
const legacy = client && typeof client.postSessionIdPermissionsPermissionId === "function"
|
|
170
|
+
? () => client.postSessionIdPermissionsPermissionId({
|
|
171
|
+
path: { id: asked.sessionID, permissionID: asked.id },
|
|
172
|
+
body: { response: allow ? "once" : "reject" },
|
|
173
|
+
})
|
|
174
|
+
: null
|
|
175
|
+
const current = client && client.permission && typeof client.permission.reply === "function"
|
|
176
|
+
? () => client.permission.reply({
|
|
177
|
+
// Current v2 uses flat parameters; older generated clients read path/body.
|
|
178
|
+
requestID: asked.id, reply: allow ? "once" : "reject",
|
|
179
|
+
path: { requestID: asked.id },
|
|
180
|
+
body: { reply: allow ? "once" : "reject" },
|
|
181
|
+
}, { throwOnError: true })
|
|
182
|
+
: null
|
|
183
|
+
|
|
184
|
+
for (const attempt of [legacy, current]) {
|
|
185
|
+
if (!attempt) continue
|
|
186
|
+
try {
|
|
187
|
+
const result = await attempt()
|
|
188
|
+
if (result && result.error) continue
|
|
189
|
+
return
|
|
190
|
+
} catch {
|
|
191
|
+
// The person answered first, or this client does not serve that route.
|
|
192
|
+
// Either way the next shape is the only thing left to try.
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const gate = async (asked) => {
|
|
198
|
+
const reply = await run({
|
|
199
|
+
...base(asked.sessionID),
|
|
200
|
+
hook_event_name: "${OPENCODE_APPROVAL_EVENT}",
|
|
201
|
+
tool_name: tools.get(asked.callID) ?? asked.permission,
|
|
202
|
+
tool_input: inputFrom(asked.permission, asked.metadata),
|
|
203
|
+
permission_id: asked.id,
|
|
204
|
+
permission_type: asked.permission,
|
|
205
|
+
title: asked.title,
|
|
206
|
+
}, APPROVAL_TIMEOUT_MS, (child) => remember(waiting, asked.id, child))
|
|
207
|
+
|
|
208
|
+
// Gone means the terminal already answered while we were asking. Replying now
|
|
209
|
+
// would be answering a question nobody is holding open any more.
|
|
210
|
+
if (!waiting.delete(asked.id)) return
|
|
211
|
+
|
|
212
|
+
const status = reply && typeof reply === "object" ? reply.status : undefined
|
|
213
|
+
if (status !== "allow" && status !== "deny") return
|
|
214
|
+
await respond(asked, status === "allow")
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const withdraw = (id) => {
|
|
218
|
+
const child = id && waiting.get(id)
|
|
219
|
+
if (!child) return
|
|
220
|
+
waiting.delete(id)
|
|
221
|
+
// SIGTERM rather than SIGKILL: the hook cancels its outstanding question on
|
|
222
|
+
// the way out, so the phone stops showing a decision the terminal has taken.
|
|
223
|
+
try {
|
|
224
|
+
child.kill("SIGTERM")
|
|
225
|
+
} catch {}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
"tool.execute.before": async (input) => {
|
|
230
|
+
remember(tools, input && input.callID, input && input.tool)
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
"tool.execute.after": async (input, output) => {
|
|
234
|
+
const call = input && input.callID
|
|
235
|
+
if (call) tools.delete(call)
|
|
236
|
+
tell({
|
|
237
|
+
...base(input && input.sessionID),
|
|
238
|
+
hook_event_name: "tool.execute.after",
|
|
239
|
+
tool_name: text(input && input.tool) ?? "unknown",
|
|
240
|
+
tool_input: input && input.args && typeof input.args === "object" ? input.args : {},
|
|
241
|
+
tool_result: { title: text(output && output.title), output: text(output && output.output) },
|
|
242
|
+
})
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
"chat.message": async (input, output) => {
|
|
246
|
+
const parts = output && Array.isArray(output.parts) ? output.parts : []
|
|
247
|
+
const prompt = parts
|
|
248
|
+
.filter((part) => part && part.type === "text" && typeof part.text === "string")
|
|
249
|
+
.map((part) => part.text)
|
|
250
|
+
.join("\\n")
|
|
251
|
+
.trim()
|
|
252
|
+
if (!prompt) return
|
|
253
|
+
tell({ ...base(input && input.sessionID), hook_event_name: "chat.message", prompt })
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
event: async ({ event }) => {
|
|
257
|
+
// Never allowed to throw: the host calls this hook without awaiting it, so a
|
|
258
|
+
// rejection here is an unhandled rejection in somebody's editor.
|
|
259
|
+
try {
|
|
260
|
+
const type = event && event.type
|
|
261
|
+
const properties = event && event.properties
|
|
262
|
+
if (${JSON.stringify(OPENCODE_ASKED_EVENTS)}.includes(type)) {
|
|
263
|
+
const asked = askedFrom(properties)
|
|
264
|
+
if (asked) void gate(asked)
|
|
265
|
+
return
|
|
266
|
+
}
|
|
267
|
+
if (type === ${JSON.stringify(OPENCODE_REPLIED_EVENT)}) {
|
|
268
|
+
withdraw(repliedID(properties))
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
// session.idle is the turn boundary \u2014 OpenCode's equivalent of Claude's
|
|
272
|
+
// Stop, not of its SessionEnd. Nothing else on the stream is relayed: the
|
|
273
|
+
// receipts already arrive through the hooks above, and reporting the same
|
|
274
|
+
// moment twice would double every row on the account.
|
|
275
|
+
if (type === ${JSON.stringify(OPENCODE_IDLE_EVENT)}) {
|
|
276
|
+
tell({
|
|
277
|
+
...base(properties && properties.sessionID),
|
|
278
|
+
hook_event_name: ${JSON.stringify(OPENCODE_IDLE_EVENT)},
|
|
279
|
+
})
|
|
280
|
+
}
|
|
281
|
+
} catch {}
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
dispose: async () => {
|
|
285
|
+
for (const id of Array.from(waiting.keys())) withdraw(id)
|
|
286
|
+
tools.clear()
|
|
287
|
+
},
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
`;
|
|
291
|
+
var openCodePluginModule = () => ({
|
|
292
|
+
marker: OPENCODE_PLUGIN_MARKER,
|
|
293
|
+
source: openCodePluginSource(),
|
|
294
|
+
placeholder: OPENCODE_PLUGIN_PLACEHOLDER,
|
|
295
|
+
sibling: {
|
|
296
|
+
path: OPENCODE_PLUGIN_SIBLING,
|
|
297
|
+
contents: OPENCODE_PLUGIN_SIBLING_CONTENTS
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
var renderOpenCodePlugin = (module, argv) => module.source.replace(module.placeholder, JSON.stringify(argv));
|
|
301
|
+
var isOpenCodePluginOurs = (contents) => contents.includes(OPENCODE_PLUGIN_MARKER);
|
|
302
|
+
|
|
303
|
+
// src/opencode-config.ts
|
|
304
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
305
|
+
import { homedir } from "os";
|
|
306
|
+
import { join } from "path";
|
|
307
|
+
var OPENCODE_HOOK_BINARY = "pushary-opencode-hook";
|
|
308
|
+
var openCodeConfigDirFrom = (home, env = process.env) => env.OPENCODE_CONFIG_DIR?.trim() || join(env.XDG_CONFIG_HOME?.trim() || join(home, ".config"), "opencode");
|
|
309
|
+
var openCodeConfigDir = () => openCodeConfigDirFrom(homedir());
|
|
310
|
+
var OPENCODE_PLUGIN_PATH = `${OPENCODE_PLUGIN_DIR}/${OPENCODE_PLUGIN_FILE}`;
|
|
311
|
+
var openCodePluginDir = () => join(openCodeConfigDir(), OPENCODE_PLUGIN_DIR);
|
|
312
|
+
var openCodePluginFile = () => join(openCodePluginDir(), OPENCODE_PLUGIN_FILE);
|
|
313
|
+
var OPENCODE_HOOK_EVENTS = [
|
|
314
|
+
{ event: OPENCODE_APPROVAL_EVENT, delivery: "approval", timeout: HOOK_BUDGETS.opencode.budgetSeconds },
|
|
315
|
+
{ event: "tool.execute.after", delivery: "telemetry", timeout: 10 },
|
|
316
|
+
{ event: "chat.message", delivery: "telemetry", timeout: 10 },
|
|
317
|
+
{ event: OPENCODE_IDLE_EVENT, delivery: "telemetry", timeout: 10 }
|
|
318
|
+
];
|
|
319
|
+
var hasForeignPlugins = (entries) => entries.some((entry) => entry !== OPENCODE_PLUGIN_FILE && /\.(ts|js)$/.test(entry));
|
|
320
|
+
var installOpenCodePlugin = (argv, dir = openCodePluginDir()) => {
|
|
321
|
+
mkdirSync(dir, { recursive: true });
|
|
322
|
+
const plugin = join(dir, OPENCODE_PLUGIN_FILE);
|
|
323
|
+
writeFileSync(plugin, renderOpenCodePlugin(openCodePluginModule(), argv), "utf-8");
|
|
324
|
+
const sibling = join(dir, OPENCODE_PLUGIN_SIBLING);
|
|
325
|
+
if (existsSync(sibling)) return { plugin, wroteSibling: false };
|
|
326
|
+
writeFileSync(sibling, OPENCODE_PLUGIN_SIBLING_CONTENTS, "utf-8");
|
|
327
|
+
return { plugin, wroteSibling: true };
|
|
328
|
+
};
|
|
329
|
+
var openCodeWired = (dir = openCodePluginDir()) => {
|
|
330
|
+
try {
|
|
331
|
+
return isOpenCodePluginOurs(readFileSync(join(dir, OPENCODE_PLUGIN_FILE), "utf-8"));
|
|
332
|
+
} catch {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
var OPENCODE_MCP_URL = "https://pushary.com/api/mcp/mcp";
|
|
337
|
+
var OPENCODE_CONFIG_FILE = "opencode.json";
|
|
338
|
+
var openCodeConfigFile = () => join(openCodeConfigDir(), OPENCODE_CONFIG_FILE);
|
|
339
|
+
var OPENCODE_INSTRUCTIONS_FILE = "AGENTS.md";
|
|
340
|
+
var openCodeInstructionsFile = () => join(openCodeConfigDir(), OPENCODE_INSTRUCTIONS_FILE);
|
|
341
|
+
var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
342
|
+
var addOpenCodeMcpServer = (config, apiKey) => {
|
|
343
|
+
const servers = asRecord(config.mcp) ?? {};
|
|
344
|
+
servers.pushary = {
|
|
345
|
+
type: "remote",
|
|
346
|
+
url: OPENCODE_MCP_URL,
|
|
347
|
+
enabled: true,
|
|
348
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
349
|
+
};
|
|
350
|
+
config.mcp = servers;
|
|
351
|
+
};
|
|
352
|
+
var removeOpenCodeMcpServer = (config) => {
|
|
353
|
+
const servers = asRecord(config.mcp);
|
|
354
|
+
if (!servers || !("pushary" in servers)) return false;
|
|
355
|
+
delete servers.pushary;
|
|
356
|
+
if (Object.keys(servers).length === 0) delete config.mcp;
|
|
357
|
+
return true;
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
export {
|
|
361
|
+
OPENCODE_PLUGIN_PLACEHOLDER,
|
|
362
|
+
OPENCODE_PLUGIN_SIBLING,
|
|
363
|
+
OPENCODE_PLUGIN_SIBLING_CONTENTS,
|
|
364
|
+
isOpenCodePluginOurs,
|
|
365
|
+
OPENCODE_HOOK_BINARY,
|
|
366
|
+
openCodeConfigDirFrom,
|
|
367
|
+
openCodeConfigDir,
|
|
368
|
+
openCodePluginDir,
|
|
369
|
+
openCodePluginFile,
|
|
370
|
+
hasForeignPlugins,
|
|
371
|
+
installOpenCodePlugin,
|
|
372
|
+
openCodeWired,
|
|
373
|
+
openCodeConfigFile,
|
|
374
|
+
openCodeInstructionsFile,
|
|
375
|
+
addOpenCodeMcpServer,
|
|
376
|
+
removeOpenCodeMcpServer
|
|
377
|
+
};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CLAUDE_HOOK_EVENTS,
|
|
3
|
+
supportedClaudeEvents
|
|
4
|
+
} from "./chunk-3EVPNIBE.js";
|
|
5
|
+
import {
|
|
6
|
+
guardBinary,
|
|
7
|
+
holdBudgetOf,
|
|
8
|
+
isProtectedByScope,
|
|
9
|
+
registerHookEntry
|
|
10
|
+
} from "./chunk-A2A2YTMG.js";
|
|
11
|
+
import {
|
|
12
|
+
HOOK_LOCATOR_BINARY,
|
|
13
|
+
withHookHoldBudget
|
|
14
|
+
} from "./chunk-NQTHFF4W.js";
|
|
15
|
+
|
|
16
|
+
// src/claude-config.ts
|
|
17
|
+
import { join } from "path";
|
|
18
|
+
import { isDeepStrictEqual } from "util";
|
|
19
|
+
var PUSHARY_MCP_URL = "https://pushary.com/api/mcp/mcp";
|
|
20
|
+
var PUSHARY_PERMISSION_RULE = "mcp__pushary__*";
|
|
21
|
+
var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
22
|
+
var ensureRecord = (target, key) => {
|
|
23
|
+
const existing = asRecord(target[key]);
|
|
24
|
+
if (existing) return existing;
|
|
25
|
+
const created = {};
|
|
26
|
+
target[key] = created;
|
|
27
|
+
return created;
|
|
28
|
+
};
|
|
29
|
+
var isPusharyPermission = (rule) => typeof rule === "string" && (rule.includes("pushary") || rule.includes("MCP(pushary"));
|
|
30
|
+
var HOOK_BINARIES = /* @__PURE__ */ new Set([
|
|
31
|
+
...CLAUDE_HOOK_EVENTS.map((hook) => hook.binary),
|
|
32
|
+
HOOK_LOCATOR_BINARY
|
|
33
|
+
]);
|
|
34
|
+
var namesPusharyBinary = (command) => command.split(/[\s/\\"']+/).some((segment) => HOOK_BINARIES.has(segment.replace(/\.(cmd|exe)$/i, "")));
|
|
35
|
+
var isPusharyHook = (entry2, scope) => {
|
|
36
|
+
const hooks = asRecord(entry2)?.hooks;
|
|
37
|
+
if (!Array.isArray(hooks)) return false;
|
|
38
|
+
return hooks.some((hook) => {
|
|
39
|
+
const command = String(asRecord(hook)?.command ?? "");
|
|
40
|
+
if (isProtectedByScope(command, scope)) return false;
|
|
41
|
+
return namesPusharyBinary(command);
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
var pusharyMcpServer = (apiKey) => ({
|
|
45
|
+
type: "http",
|
|
46
|
+
url: PUSHARY_MCP_URL,
|
|
47
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
48
|
+
});
|
|
49
|
+
var addClaudeMcpServer = (config, apiKey) => {
|
|
50
|
+
const mcpServers = ensureRecord(config, "mcpServers");
|
|
51
|
+
mcpServers.pushary = pusharyMcpServer(apiKey);
|
|
52
|
+
};
|
|
53
|
+
var isClaudeMcpSnapshot = (value) => {
|
|
54
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
55
|
+
const snapshot = value;
|
|
56
|
+
return typeof snapshot.hadMcpServers === "boolean" && typeof snapshot.mcpServersWasRecord === "boolean" && (!snapshot.hadMcpServers || snapshot.mcpServersWasRecord || Object.hasOwn(snapshot, "mcpServers")) && typeof snapshot.hadPushary === "boolean" && (!snapshot.hadPushary || Object.hasOwn(snapshot, "pushary")) && (!Object.hasOwn(snapshot, "installedPushary") || asRecord(snapshot.installedPushary)?.type === "http" && asRecord(snapshot.installedPushary)?.url === PUSHARY_MCP_URL);
|
|
57
|
+
};
|
|
58
|
+
var captureClaudeMcpServer = (config, apiKey) => {
|
|
59
|
+
const mcpServers = asRecord(config.mcpServers);
|
|
60
|
+
const hadMcpServers = Object.hasOwn(config, "mcpServers");
|
|
61
|
+
return {
|
|
62
|
+
hadMcpServers,
|
|
63
|
+
mcpServersWasRecord: Boolean(mcpServers),
|
|
64
|
+
...hadMcpServers && !mcpServers ? { mcpServers: structuredClone(config.mcpServers) } : {},
|
|
65
|
+
hadPushary: Boolean(mcpServers && Object.hasOwn(mcpServers, "pushary")),
|
|
66
|
+
...mcpServers && Object.hasOwn(mcpServers, "pushary") ? { pushary: structuredClone(mcpServers.pushary) } : {},
|
|
67
|
+
...apiKey ? { installedPushary: pusharyMcpServer(apiKey) } : {}
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
var updateClaudeMcpSnapshot = (snapshot, apiKey) => ({
|
|
71
|
+
...snapshot,
|
|
72
|
+
installedPushary: pusharyMcpServer(apiKey)
|
|
73
|
+
});
|
|
74
|
+
var restoreClaudeMcpServer = (config, snapshot) => {
|
|
75
|
+
const mcpServers = asRecord(config.mcpServers);
|
|
76
|
+
const current = asRecord(mcpServers?.pushary);
|
|
77
|
+
if (Object.hasOwn(snapshot, "installedPushary")) {
|
|
78
|
+
if (!isDeepStrictEqual(mcpServers?.pushary, snapshot.installedPushary)) return false;
|
|
79
|
+
} else if (current?.type !== "http" || current.url !== PUSHARY_MCP_URL) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
if (!snapshot.mcpServersWasRecord) {
|
|
83
|
+
if (Object.keys(mcpServers).length === 1) {
|
|
84
|
+
if (snapshot.hadMcpServers) config.mcpServers = structuredClone(snapshot.mcpServers);
|
|
85
|
+
else delete config.mcpServers;
|
|
86
|
+
} else {
|
|
87
|
+
delete mcpServers.pushary;
|
|
88
|
+
}
|
|
89
|
+
} else if (snapshot.hadPushary) {
|
|
90
|
+
mcpServers.pushary = structuredClone(snapshot.pushary);
|
|
91
|
+
} else {
|
|
92
|
+
delete mcpServers.pushary;
|
|
93
|
+
if (!snapshot.hadMcpServers && Object.keys(mcpServers).length === 0) delete config.mcpServers;
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
};
|
|
97
|
+
var removeClaudeMcpServers = (config) => {
|
|
98
|
+
let changed = false;
|
|
99
|
+
const removeFrom = (target) => {
|
|
100
|
+
const mcpServers = asRecord(target.mcpServers);
|
|
101
|
+
if (!mcpServers?.pushary) return;
|
|
102
|
+
delete mcpServers.pushary;
|
|
103
|
+
if (Object.keys(mcpServers).length === 0) delete target.mcpServers;
|
|
104
|
+
changed = true;
|
|
105
|
+
};
|
|
106
|
+
removeFrom(config);
|
|
107
|
+
const projects = asRecord(config.projects);
|
|
108
|
+
if (projects) {
|
|
109
|
+
for (const project of Object.values(projects)) {
|
|
110
|
+
const projectConfig = asRecord(project);
|
|
111
|
+
if (projectConfig) removeFrom(projectConfig);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return changed;
|
|
115
|
+
};
|
|
116
|
+
var removePusharyToolPermissions = (settings) => {
|
|
117
|
+
const permissions = asRecord(settings.permissions);
|
|
118
|
+
if (!permissions || !Array.isArray(permissions.allow)) return false;
|
|
119
|
+
const filtered = permissions.allow.filter((rule) => !isPusharyPermission(rule));
|
|
120
|
+
if (filtered.length === permissions.allow.length) return false;
|
|
121
|
+
if (filtered.length === 0) delete permissions.allow;
|
|
122
|
+
else permissions.allow = filtered;
|
|
123
|
+
if (Object.keys(permissions).length === 0) delete settings.permissions;
|
|
124
|
+
return true;
|
|
125
|
+
};
|
|
126
|
+
var addPusharyToolPermissions = (settings) => {
|
|
127
|
+
const permissions = ensureRecord(settings, "permissions");
|
|
128
|
+
const allow = Array.isArray(permissions.allow) ? permissions.allow : [];
|
|
129
|
+
const filtered = allow.filter((rule) => !isPusharyPermission(rule));
|
|
130
|
+
if (!filtered.includes(PUSHARY_PERMISSION_RULE)) {
|
|
131
|
+
filtered.push(PUSHARY_PERMISSION_RULE);
|
|
132
|
+
}
|
|
133
|
+
permissions.allow = filtered;
|
|
134
|
+
};
|
|
135
|
+
var addPusharyHooks = (settings, binDir, version = null) => {
|
|
136
|
+
const resolve = (name) => guardBinary(binDir ? join(binDir, name) : name);
|
|
137
|
+
const hooks = ensureRecord(settings, "hooks");
|
|
138
|
+
const supported = new Map(supportedClaudeEvents(version).map((hook) => [hook.event, hook]));
|
|
139
|
+
for (const known of CLAUDE_HOOK_EVENTS) {
|
|
140
|
+
const current = Array.isArray(hooks[known.event]) ? hooks[known.event] : [];
|
|
141
|
+
const definition = supported.get(known.event);
|
|
142
|
+
if (definition) {
|
|
143
|
+
hooks[known.event] = registerHookEntry(current, isPusharyHook, entry(definition, resolve));
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const others = current.filter((item) => !isPusharyHook(item));
|
|
147
|
+
if (others.length > 0) {
|
|
148
|
+
hooks[known.event] = others;
|
|
149
|
+
} else {
|
|
150
|
+
delete hooks[known.event];
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
var entry = (definition, resolve) => ({
|
|
155
|
+
...definition.matcher ? { matcher: definition.matcher } : {},
|
|
156
|
+
hooks: [{
|
|
157
|
+
type: "command",
|
|
158
|
+
command: withHookHoldBudget(resolve(definition.binary), holdBudgetOf(definition)),
|
|
159
|
+
timeout: definition.timeout
|
|
160
|
+
}]
|
|
161
|
+
});
|
|
162
|
+
var removePusharySettings = (settings, scope) => {
|
|
163
|
+
let changed = scope?.keepMcpServer === true ? false : removeClaudeMcpServers(settings);
|
|
164
|
+
if (scope?.keepMcpServer !== true && removePusharyToolPermissions(settings)) changed = true;
|
|
165
|
+
const hooks = asRecord(settings.hooks);
|
|
166
|
+
if (hooks) {
|
|
167
|
+
for (const { event: key } of CLAUDE_HOOK_EVENTS) {
|
|
168
|
+
const entries = hooks[key];
|
|
169
|
+
if (!Array.isArray(entries)) continue;
|
|
170
|
+
const filtered = entries.filter((entry2) => !isPusharyHook(entry2, scope));
|
|
171
|
+
if (filtered.length !== entries.length) {
|
|
172
|
+
if (filtered.length === 0) {
|
|
173
|
+
delete hooks[key];
|
|
174
|
+
} else {
|
|
175
|
+
hooks[key] = filtered;
|
|
176
|
+
}
|
|
177
|
+
changed = true;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (Object.keys(hooks).length === 0) delete settings.hooks;
|
|
181
|
+
}
|
|
182
|
+
return changed;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
export {
|
|
186
|
+
isPusharyHook,
|
|
187
|
+
addClaudeMcpServer,
|
|
188
|
+
isClaudeMcpSnapshot,
|
|
189
|
+
captureClaudeMcpServer,
|
|
190
|
+
updateClaudeMcpSnapshot,
|
|
191
|
+
restoreClaudeMcpServer,
|
|
192
|
+
removeClaudeMcpServers,
|
|
193
|
+
removePusharyToolPermissions,
|
|
194
|
+
addPusharyToolPermissions,
|
|
195
|
+
addPusharyHooks,
|
|
196
|
+
removePusharySettings
|
|
197
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HOOK_LOCATOR_BINARY
|
|
3
|
+
} from "./chunk-NQTHFF4W.js";
|
|
4
|
+
|
|
5
|
+
// src/diagnostics/bridge.ts
|
|
6
|
+
import { execFileSync } from "child_process";
|
|
7
|
+
import { existsSync, readFileSync } from "fs";
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
import { dirname, join } from "path";
|
|
10
|
+
var BRIDGE_BUNDLE_IDENTIFIER = "com.pushary.app";
|
|
11
|
+
var BRIDGE_HELPER_SUFFIX = "/Contents/Helpers/pushary-bridge";
|
|
12
|
+
var BRIDGE_BUNDLE_ROOTS = ["/Applications/Pushary.app", "$HOME/Applications/Pushary.app"];
|
|
13
|
+
var expandHome = (path, home) => path.replaceAll("$HOME", home);
|
|
14
|
+
var parseBridgeStub = (contents, stubPath) => {
|
|
15
|
+
const helperSuffix = contents.match(/^H=(\S+)$/m)?.[1] ?? BRIDGE_HELPER_SUFFIX;
|
|
16
|
+
const cachePath = contents.match(/^C="([^"\n]+)"$/m)?.[1] ?? join(dirname(stubPath), ".bridge-cache");
|
|
17
|
+
const rootsLine = contents.match(/^for P in ((?:"[^"\n]*"\s*)+); do$/m)?.[1];
|
|
18
|
+
const bundleRoots = rootsLine ? [...rootsLine.matchAll(/"([^"]*)"/g)].map((match) => match[1]) : BRIDGE_BUNDLE_ROOTS;
|
|
19
|
+
const bundleIdentifier = contents.match(/kMDItemCFBundleIdentifier == "([^"]+)"/)?.[1] ?? BRIDGE_BUNDLE_IDENTIFIER;
|
|
20
|
+
return { helperSuffix, cachePath, bundleRoots, bundleIdentifier };
|
|
21
|
+
};
|
|
22
|
+
var resolveBridgeStub = (stubPath, probe) => {
|
|
23
|
+
const contents = probe.readFile(stubPath);
|
|
24
|
+
if (contents === null) return { kind: "no-stub", stub: stubPath };
|
|
25
|
+
const shape = parseBridgeStub(contents, stubPath);
|
|
26
|
+
const helperIn = (bundle) => `${bundle}${shape.helperSuffix}`;
|
|
27
|
+
const looked = [];
|
|
28
|
+
for (const root of shape.bundleRoots.map((root2) => expandHome(root2, probe.home))) {
|
|
29
|
+
looked.push(root);
|
|
30
|
+
if (probe.exists(helperIn(root))) return { kind: "bundle", helper: helperIn(root), via: "root" };
|
|
31
|
+
}
|
|
32
|
+
const cached = shape.cachePath ? probe.readFile(shape.cachePath)?.split("\n")[0]?.trim() : void 0;
|
|
33
|
+
if (cached) {
|
|
34
|
+
looked.push(cached);
|
|
35
|
+
if (probe.exists(helperIn(cached))) return { kind: "bundle", helper: helperIn(cached), via: "cache" };
|
|
36
|
+
}
|
|
37
|
+
for (const found of probe.mdfind(shape.bundleIdentifier)) {
|
|
38
|
+
looked.push(found);
|
|
39
|
+
if (probe.exists(helperIn(found))) return { kind: "bundle", helper: helperIn(found), via: "spotlight" };
|
|
40
|
+
}
|
|
41
|
+
return { kind: "missing", stub: stubPath, looked };
|
|
42
|
+
};
|
|
43
|
+
var BRIDGE_GONE_MESSAGE = "the Mac app is gone but its hooks remain; run pushary setup or reinstall the app";
|
|
44
|
+
var describeBridgeResolution = (resolution) => {
|
|
45
|
+
switch (resolution.kind) {
|
|
46
|
+
case "bundle":
|
|
47
|
+
return `${resolution.helper} (${resolution.via === "root" ? "installed" : resolution.via === "cache" ? "cached path" : "found by Spotlight"})`;
|
|
48
|
+
case "missing":
|
|
49
|
+
return BRIDGE_GONE_MESSAGE;
|
|
50
|
+
case "no-stub":
|
|
51
|
+
return `${resolution.stub} is not there; ${BRIDGE_GONE_MESSAGE}`;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
var isBridgeLocator = (executable) => executable !== null && executable.split(/[/\\]/).pop() === HOOK_LOCATOR_BINARY;
|
|
55
|
+
var spotlight = (bundleIdentifier) => {
|
|
56
|
+
if (process.platform !== "darwin") return [];
|
|
57
|
+
try {
|
|
58
|
+
return execFileSync("/usr/bin/mdfind", [`kMDItemCFBundleIdentifier == "${bundleIdentifier}"`], {
|
|
59
|
+
encoding: "utf-8",
|
|
60
|
+
timeout: 5e3,
|
|
61
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
62
|
+
}).split("\n").map((line) => line.trim()).filter((line) => line !== "");
|
|
63
|
+
} catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
var readOrNull = (path) => {
|
|
68
|
+
try {
|
|
69
|
+
return readFileSync(path, "utf-8");
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
var liveBridgeProbe = () => ({
|
|
75
|
+
exists: existsSync,
|
|
76
|
+
readFile: readOrNull,
|
|
77
|
+
mdfind: spotlight,
|
|
78
|
+
home: homedir()
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
export {
|
|
82
|
+
resolveBridgeStub,
|
|
83
|
+
describeBridgeResolution,
|
|
84
|
+
isBridgeLocator,
|
|
85
|
+
liveBridgeProbe
|
|
86
|
+
};
|