@promptster/teams-cli 0.12.1 → 0.12.3
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/bin/promptster-teams.js +35 -0
- package/lib/install.js +272 -0
- package/package.json +7 -7
- package/scripts/postinstall.js +23 -184
package/bin/promptster-teams.js
CHANGED
|
@@ -31,6 +31,41 @@ function usable(p) {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// Converge the managed binary before choosing which one to run.
|
|
35
|
+
//
|
|
36
|
+
// npm is not a reliable place to hang this work any more: its install-script
|
|
37
|
+
// approval gate can decline to run our postinstall while reporting a completely
|
|
38
|
+
// successful install, so the managed binary — the one the daemon executes and
|
|
39
|
+
// autostart points at — is never written and `autostart repair` never runs. The
|
|
40
|
+
// engineer sees "added 2 packages" and a working CLI, and only the parts that
|
|
41
|
+
// matter are missing. Doing it here means running the CLI at all converges the
|
|
42
|
+
// machine, whatever npm decided about our scripts.
|
|
43
|
+
//
|
|
44
|
+
// Cost when there is nothing to do (the overwhelming case): one readFileSync of
|
|
45
|
+
// a tiny marker file. No spawns, no stat of the bundled binary.
|
|
46
|
+
//
|
|
47
|
+
// `uninstall` is excluded — reinstalling the binary a person is in the middle of
|
|
48
|
+
// removing is the one thing this must never do.
|
|
49
|
+
try {
|
|
50
|
+
const {
|
|
51
|
+
installManagedBinary,
|
|
52
|
+
mayNeedInstall,
|
|
53
|
+
shouldConvergeOnInvocation,
|
|
54
|
+
} = require("../lib/install");
|
|
55
|
+
if (shouldConvergeOnInvocation(process.argv[2])) {
|
|
56
|
+
if (mayNeedInstall(require("../package.json").version)) {
|
|
57
|
+
// stderr, never stdout: this must not corrupt the output of the command
|
|
58
|
+
// the engineer actually ran (`version`, `status --json`, …).
|
|
59
|
+
const say = (msg) => console.error(`promptster-teams: ${msg}`);
|
|
60
|
+
installManagedBinary({ log: say, warn: say });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} catch (err) {
|
|
64
|
+
// Never block the command. The worst case is the pre-existing behaviour: run
|
|
65
|
+
// the bundled binary and let npm's metadata drift.
|
|
66
|
+
console.error(`promptster-teams: could not check the managed install: ${err && err.message}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
34
69
|
const bundled = bundledBinPath();
|
|
35
70
|
|
|
36
71
|
// A project-local install runs ITS OWN binary, never the shared managed one.
|
package/lib/install.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Installing the managed binary, shared by postinstall AND the launcher.
|
|
4
|
+
//
|
|
5
|
+
// WHY THE LAUNCHER ALSO DOES THIS
|
|
6
|
+
// -------------------------------
|
|
7
|
+
// postinstall used to be the only path, and postinstall is not guaranteed to
|
|
8
|
+
// run. npm gained an install-script approval gate, and a machine that has not
|
|
9
|
+
// approved ours reports a completely successful `npm i -g` while doing none of
|
|
10
|
+
// the work this package exists to do:
|
|
11
|
+
//
|
|
12
|
+
// npm warn allow-scripts 1 package has install scripts not yet covered by allowScripts
|
|
13
|
+
//
|
|
14
|
+
// Nothing about that is loud. The engineer sees "added 2 packages", the CLI
|
|
15
|
+
// still runs (the launcher falls back to the bundled copy), and the two things
|
|
16
|
+
// that silently do not happen are the two that matter: the managed binary — the
|
|
17
|
+
// one the daemon runs, autostart points at, and self-update owns — is never
|
|
18
|
+
// written, and `autostart repair` never re-points a unit at it. Observed in the
|
|
19
|
+
// field on npm's newer gate, where `npm approve-scripts --allow-scripts-pending`
|
|
20
|
+
// answered "No packages with unreviewed install scripts" and the next install
|
|
21
|
+
// warned again.
|
|
22
|
+
//
|
|
23
|
+
// So the install is idempotent and reachable from the launcher too. Running the
|
|
24
|
+
// CLI at all is now enough to converge the machine, whatever the package manager
|
|
25
|
+
// decided about our scripts. --ignore-scripts lands in the same place.
|
|
26
|
+
//
|
|
27
|
+
// Everything here is best-effort by contract. It runs inside `npm install`,
|
|
28
|
+
// where a throw aborts the install and leaves the engineer with no CLI at all,
|
|
29
|
+
// and on the hot path of every CLI invocation, where a throw breaks a command
|
|
30
|
+
// that had nothing to do with installing. Every path returns a result; none
|
|
31
|
+
// throw.
|
|
32
|
+
|
|
33
|
+
const { spawnSync } = require("child_process");
|
|
34
|
+
const fs = require("fs");
|
|
35
|
+
const path = require("path");
|
|
36
|
+
|
|
37
|
+
const {
|
|
38
|
+
PLATFORMS,
|
|
39
|
+
bundledBinPath,
|
|
40
|
+
managedBinPath,
|
|
41
|
+
isGlobalInstall,
|
|
42
|
+
isNewer,
|
|
43
|
+
platformKey,
|
|
44
|
+
platformPackage,
|
|
45
|
+
} = require("./resolve");
|
|
46
|
+
|
|
47
|
+
// markerPath records the BUNDLED version last evaluated against the managed
|
|
48
|
+
// binary. It exists purely to keep the launcher's check cheap: without it every
|
|
49
|
+
// invocation would spawn two `--version` probes (~20ms) to answer "nothing to
|
|
50
|
+
// do", which is the answer approximately always.
|
|
51
|
+
//
|
|
52
|
+
// It is a HINT, never the decision. The real guard still compares binary to
|
|
53
|
+
// binary — see installManagedBinary.
|
|
54
|
+
function markerPath() {
|
|
55
|
+
const managed = managedBinPath();
|
|
56
|
+
return managed ? path.join(path.dirname(managed), ".npm-installed") : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readMarker() {
|
|
60
|
+
try {
|
|
61
|
+
const p = markerPath();
|
|
62
|
+
return p ? fs.readFileSync(p, "utf8").trim() : null;
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function writeMarker(version) {
|
|
69
|
+
try {
|
|
70
|
+
const p = markerPath();
|
|
71
|
+
if (!p) return;
|
|
72
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
73
|
+
fs.writeFileSync(p, `${version}\n`, { mode: 0o600 });
|
|
74
|
+
} catch {
|
|
75
|
+
// A missing marker costs two spawns on the next run, nothing more.
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// mayNeedInstall is the cheap gate the launcher consults on every invocation:
|
|
80
|
+
// one readFileSync of a ~7-byte file, no spawns.
|
|
81
|
+
//
|
|
82
|
+
// It answers true when the managed binary is absent, or when the marker is older
|
|
83
|
+
// than the version npm has on disk (or missing entirely — an install whose
|
|
84
|
+
// postinstall never ran leaves no marker). A false answer must be conclusive;
|
|
85
|
+
// a true answer only buys the right to look properly.
|
|
86
|
+
function mayNeedInstall(bundledPkgVersion) {
|
|
87
|
+
const managed = managedBinPath();
|
|
88
|
+
if (!managed) return false;
|
|
89
|
+
if (!fs.existsSync(managed)) return true;
|
|
90
|
+
const marker = readMarker();
|
|
91
|
+
if (!marker) return true;
|
|
92
|
+
return isNewer(bundledPkgVersion, marker);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// binVersion asks a binary what it is. One that cannot answer (missing, corrupt,
|
|
96
|
+
// wrong arch, not executable) reports null.
|
|
97
|
+
//
|
|
98
|
+
// Both sides of the downgrade guard go through this deliberately. The obvious
|
|
99
|
+
// shortcut is to compare package.json's version against the managed binary — but
|
|
100
|
+
// that trusts package.json to describe the bytes actually shipped, and when those
|
|
101
|
+
// two disagree the guard decides on a fiction (and the success log prints a
|
|
102
|
+
// version that was never installed). Comparing the actual binary to the actual
|
|
103
|
+
// binary means the guard cannot be wrong even if the pipeline's package.json gate
|
|
104
|
+
// is ever lost. It costs one ~10ms spawn.
|
|
105
|
+
function binVersion(bin) {
|
|
106
|
+
if (!bin || !fs.existsSync(bin)) return null;
|
|
107
|
+
try {
|
|
108
|
+
const r = spawnSync(bin, ["--version"], { encoding: "utf8", timeout: 10_000 });
|
|
109
|
+
if (r.status !== 0 || !r.stdout) return null;
|
|
110
|
+
return r.stdout.trim().split("\n")[0].trim() || null;
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// repairAutostart re-points an ALREADY-ENABLED launchd/systemd/schtasks unit at
|
|
117
|
+
// the managed binary.
|
|
118
|
+
//
|
|
119
|
+
// THIS IS A MIGRATION, and without it this package is a silent regression for
|
|
120
|
+
// every engineer who has run `autostart enable`. The unit bakes an ABSOLUTE path
|
|
121
|
+
// at enable time. Installs predating the managed-binary layout baked
|
|
122
|
+
// <pkg>/binaries/promptster-teams-<platform> — a path that no longer exists in
|
|
123
|
+
// the package, because the binary moved to a per-platform dependency and the
|
|
124
|
+
// wrapper stopped shipping binaries/ entirely. So `npm i -g` deletes the exact
|
|
125
|
+
// file the supervisor is pointed at.
|
|
126
|
+
//
|
|
127
|
+
// Nothing about that fails loudly. The running daemon holds its inode and keeps
|
|
128
|
+
// capturing, so the upgrade looks clean; the unit only breaks at the NEXT LOGIN,
|
|
129
|
+
// when launchd runs a path that is gone. Capture never comes back — the precise
|
|
130
|
+
// failure autostart exists to prevent.
|
|
131
|
+
//
|
|
132
|
+
// Delegated to the Go binary (`autostart repair`) rather than reimplemented here:
|
|
133
|
+
// this must not grow a plist writer, a systemd unit writer and a schtasks caller
|
|
134
|
+
// in Node, three platforms out of sync with the Go ones. `repair` is a no-op when
|
|
135
|
+
// autostart was never enabled and never exits non-zero.
|
|
136
|
+
function repairAutostart(managed, log) {
|
|
137
|
+
try {
|
|
138
|
+
const r = spawnSync(managed, ["autostart", "repair"], {
|
|
139
|
+
encoding: "utf8",
|
|
140
|
+
timeout: 30_000,
|
|
141
|
+
});
|
|
142
|
+
const out = `${r.stdout || ""}${r.stderr || ""}`.trim();
|
|
143
|
+
if (out) log(out);
|
|
144
|
+
} catch {
|
|
145
|
+
// Best-effort: a failed repair leaves autostart pointing at the old path,
|
|
146
|
+
// which the engineer can fix with `promptster-teams autostart enable`.
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// installManagedBinary copies the bundled binary to the managed path when that
|
|
151
|
+
// is an upgrade, then repairs autostart. Returns a short status string for the
|
|
152
|
+
// caller's own reporting; never throws.
|
|
153
|
+
//
|
|
154
|
+
// log/warn are injected because the two callers need different streams:
|
|
155
|
+
// postinstall talks on stdout inside an npm install, while the launcher must
|
|
156
|
+
// stay on stderr so it cannot corrupt the output of the command being run.
|
|
157
|
+
// bundledOverride exists so the test suite can supply a real fixture binary:
|
|
158
|
+
// the actual bundled path resolves through an optionalDependency that is not
|
|
159
|
+
// installed in a source checkout, which would make every test take the
|
|
160
|
+
// "no binary to install" branch and prove nothing about the install itself.
|
|
161
|
+
function installManagedBinary({ log, warn, bundled: bundledOverride }) {
|
|
162
|
+
// A project-local install must not touch the shared managed binary. The
|
|
163
|
+
// lockfile is a per-PROJECT pin; the managed binary is per-USER. Letting a
|
|
164
|
+
// local install write it means two repos pinning different versions fight over
|
|
165
|
+
// one file and neither gets what its lockfile selected — a worse bug than the
|
|
166
|
+
// npm-ls drift this whole design removes.
|
|
167
|
+
if (!isGlobalInstall()) return "project-local";
|
|
168
|
+
|
|
169
|
+
const bundled = bundledOverride || bundledBinPath();
|
|
170
|
+
if (!bundled) {
|
|
171
|
+
// Either an unsupported platform, or the optionalDependency carrying the
|
|
172
|
+
// binary is absent. npm treats a missing optional dep as SUCCESS and says
|
|
173
|
+
// nothing, so this warning is the only signal the engineer will ever get
|
|
174
|
+
// that they have a CLI with no binary behind it. Say which package.
|
|
175
|
+
if (!PLATFORMS.includes(platformKey())) {
|
|
176
|
+
warn(`unsupported platform ${platformKey()} — no binary to install`);
|
|
177
|
+
return "unsupported";
|
|
178
|
+
}
|
|
179
|
+
warn(`${platformPackage()} is not installed, so there is no binary to install.`);
|
|
180
|
+
warn("If you used --omit=optional or --no-optional, reinstall without it.");
|
|
181
|
+
return "no-bundled-binary";
|
|
182
|
+
}
|
|
183
|
+
const managed = managedBinPath();
|
|
184
|
+
if (!managed) {
|
|
185
|
+
warn("could not resolve home directory — skipping binary install");
|
|
186
|
+
return "no-home";
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const incoming = binVersion(bundled);
|
|
190
|
+
const current = binVersion(managed);
|
|
191
|
+
|
|
192
|
+
if (!incoming) {
|
|
193
|
+
warn(`bundled binary at ${bundled} did not report a version — skipping`);
|
|
194
|
+
return "unreadable-bundled";
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Never downgrade. The managed binary self-updates forward on its own, so it is
|
|
198
|
+
// routinely NEWER than whatever version npm is installing (that is the normal
|
|
199
|
+
// steady state, not an error). Clobbering it would hand the daemon an older
|
|
200
|
+
// build that immediately re-updates — churn, plus a window on a version the
|
|
201
|
+
// engineer already moved past. Mirrors the Go updater's isNewer gate.
|
|
202
|
+
//
|
|
203
|
+
// A managed binary that cannot report a version (current === null) is treated
|
|
204
|
+
// as absent and overwritten: a corrupt or half-written file should be replaced,
|
|
205
|
+
// not preserved by a guard meant to protect a GOOD newer build.
|
|
206
|
+
if (current && !isNewer(incoming, current)) {
|
|
207
|
+
// Record what we evaluated even though nothing was written, or the launcher's
|
|
208
|
+
// cheap gate stays open and re-probes on every single invocation.
|
|
209
|
+
writeMarker(incoming);
|
|
210
|
+
return "already-current";
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
fs.mkdirSync(path.dirname(managed), { recursive: true });
|
|
215
|
+
// Write to a temp file in the SAME directory, then rename: rename is atomic
|
|
216
|
+
// on POSIX, so a concurrent `promptster-teams` exec sees either the whole old
|
|
217
|
+
// binary or the whole new one, never a half-written file. Copying straight
|
|
218
|
+
// onto `managed` would also fail with ETXTBSY on Linux if the daemon is
|
|
219
|
+
// running. The pid in the name keeps two concurrent launchers apart.
|
|
220
|
+
const tmp = `${managed}.tmp-${process.pid}`;
|
|
221
|
+
fs.copyFileSync(bundled, tmp);
|
|
222
|
+
fs.chmodSync(tmp, 0o755);
|
|
223
|
+
|
|
224
|
+
// Re-check immediately before the swap. The guard above and this rename are
|
|
225
|
+
// not atomic with respect to the Go self-updater, which renames onto this
|
|
226
|
+
// same path from a live daemon: read 1.0.0 -> daemon installs 1.2.0 -> rename
|
|
227
|
+
// 1.1.0 over it, and the guard has been defeated.
|
|
228
|
+
//
|
|
229
|
+
// This narrows the window from "the whole copy" to the microseconds around
|
|
230
|
+
// the rename rather than closing it. Closing it needs a lock protocol shared
|
|
231
|
+
// by a Node script and a Go daemon, which is a lot of machinery for this
|
|
232
|
+
// failure: rename is atomic so the file is always ONE whole valid binary
|
|
233
|
+
// (never corrupt), the only cost is running an older version, and the daemon
|
|
234
|
+
// re-updates forward within one check interval (<=30m). Deliberate tradeoff,
|
|
235
|
+
// not an oversight — see CLAUDE.md.
|
|
236
|
+
const stillCurrent = binVersion(managed);
|
|
237
|
+
if (stillCurrent && !isNewer(incoming, stillCurrent)) {
|
|
238
|
+
fs.unlinkSync(tmp);
|
|
239
|
+
writeMarker(incoming);
|
|
240
|
+
log(`${managed} changed to ${stillCurrent} mid-install; leaving it`);
|
|
241
|
+
return "raced";
|
|
242
|
+
}
|
|
243
|
+
fs.renameSync(tmp, managed);
|
|
244
|
+
writeMarker(incoming);
|
|
245
|
+
log(`installed ${incoming} to ${managed}${current ? ` (was ${current})` : ""}`);
|
|
246
|
+
repairAutostart(managed, log);
|
|
247
|
+
return "installed";
|
|
248
|
+
} catch (err) {
|
|
249
|
+
// Falls back to the bundled binary via bin/promptster-teams.js.
|
|
250
|
+
warn(`could not install to ${managed}: ${err.message}`);
|
|
251
|
+
warn("falling back to the bundled binary (npm ls may report a stale version)");
|
|
252
|
+
return "failed";
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// shouldConvergeOnInvocation gates the launcher's self-heal.
|
|
257
|
+
//
|
|
258
|
+
// `uninstall` is the one command that must never trigger it: reinstalling the
|
|
259
|
+
// binary a person is in the middle of removing would leave them with a CLI they
|
|
260
|
+
// asked us to delete, and `uninstall` is already the ONLY working uninstall path
|
|
261
|
+
// (npm runs no uninstall lifecycle script — see CLAUDE.md), so undermining it
|
|
262
|
+
// leaves no other lever.
|
|
263
|
+
function shouldConvergeOnInvocation(subcommand) {
|
|
264
|
+
return isGlobalInstall() && subcommand !== "uninstall";
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
module.exports = {
|
|
268
|
+
installManagedBinary,
|
|
269
|
+
mayNeedInstall,
|
|
270
|
+
binVersion,
|
|
271
|
+
shouldConvergeOnInvocation,
|
|
272
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@promptster/teams-cli",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.3",
|
|
4
4
|
"description": "On-device, auditable AI-coding capture for internal engineering teams",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"promptster",
|
|
@@ -37,11 +37,11 @@
|
|
|
37
37
|
"access": "public"
|
|
38
38
|
},
|
|
39
39
|
"optionalDependencies": {
|
|
40
|
-
"@promptster/teams-cli-linux-x64": "0.12.
|
|
41
|
-
"@promptster/teams-cli-linux-arm64": "0.12.
|
|
42
|
-
"@promptster/teams-cli-darwin-x64": "0.12.
|
|
43
|
-
"@promptster/teams-cli-darwin-arm64": "0.12.
|
|
44
|
-
"@promptster/teams-cli-win32-x64": "0.12.
|
|
45
|
-
"@promptster/teams-cli-win32-arm64": "0.12.
|
|
40
|
+
"@promptster/teams-cli-linux-x64": "0.12.3",
|
|
41
|
+
"@promptster/teams-cli-linux-arm64": "0.12.3",
|
|
42
|
+
"@promptster/teams-cli-darwin-x64": "0.12.3",
|
|
43
|
+
"@promptster/teams-cli-darwin-arm64": "0.12.3",
|
|
44
|
+
"@promptster/teams-cli-win32-x64": "0.12.3",
|
|
45
|
+
"@promptster/teams-cli-win32-arm64": "0.12.3"
|
|
46
46
|
}
|
|
47
47
|
}
|
package/scripts/postinstall.js
CHANGED
|
@@ -4,198 +4,37 @@
|
|
|
4
4
|
// Installs the platform binary from node_modules to the MANAGED path
|
|
5
5
|
// (~/.promptster-teams/bin/promptster-teams), which is what actually runs and
|
|
6
6
|
// what self-update owns. See lib/resolve.js for why the binary must not run
|
|
7
|
-
// from inside node_modules.
|
|
7
|
+
// from inside node_modules, and lib/install.js for the install itself.
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const fs = require("fs");
|
|
17
|
-
const path = require("path");
|
|
18
|
-
|
|
19
|
-
const {
|
|
20
|
-
PLATFORMS,
|
|
21
|
-
bundledBinPath,
|
|
22
|
-
managedBinPath,
|
|
23
|
-
isGlobalInstall,
|
|
24
|
-
isNewer,
|
|
25
|
-
platformKey,
|
|
26
|
-
platformPackage,
|
|
27
|
-
} = require("../lib/resolve");
|
|
28
|
-
|
|
29
|
-
function warn(msg) {
|
|
30
|
-
console.warn(`promptster-teams: ${msg}`);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// binVersion asks a binary what it is. One that cannot answer (missing,
|
|
34
|
-
// corrupt, wrong arch, not executable) reports null.
|
|
9
|
+
// This is no longer the ONLY path to that install: npm can decline to run
|
|
10
|
+
// install scripts at all (its allow-scripts approval gate, and --ignore-scripts),
|
|
11
|
+
// and it says so in a warning that reads like housekeeping while the install
|
|
12
|
+
// silently does nothing. So bin/promptster-teams.js performs the same converge
|
|
13
|
+
// on invocation. This file stays because running it at install time is still
|
|
14
|
+
// strictly better — the binary is in place before the engineer's first command,
|
|
15
|
+
// and `autostart repair` lands during the upgrade rather than after it.
|
|
35
16
|
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
// package.json against the tag, so they agree today; comparing the actual binary
|
|
42
|
-
// to the actual binary means the guard cannot be wrong even if that gate is ever
|
|
43
|
-
// lost. It costs one ~10ms spawn on install.
|
|
44
|
-
function binVersion(bin) {
|
|
45
|
-
if (!bin || !fs.existsSync(bin)) return null;
|
|
46
|
-
try {
|
|
47
|
-
const r = spawnSync(bin, ["--version"], {
|
|
48
|
-
encoding: "utf8",
|
|
49
|
-
timeout: 10_000,
|
|
50
|
-
});
|
|
51
|
-
if (r.status !== 0 || !r.stdout) return null;
|
|
52
|
-
return r.stdout.trim().split("\n")[0].trim() || null;
|
|
53
|
-
} catch {
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
17
|
+
// Contract: this script must NEVER fail an npm install. A postinstall that exits
|
|
18
|
+
// non-zero aborts `npm i -g` and leaves the engineer with no CLI at all — far
|
|
19
|
+
// worse than the drift it exists to fix. Every failure path warns and exits 0;
|
|
20
|
+
// the launcher then falls back to the bundled binary, which works exactly as it
|
|
21
|
+
// did before this file existed.
|
|
57
22
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
//
|
|
61
|
-
// THIS IS A MIGRATION, and without it this package is a silent regression for
|
|
62
|
-
// every engineer who has run `autostart enable`. The unit bakes an ABSOLUTE
|
|
63
|
-
// path at enable time. Installs predating the managed-binary layout baked
|
|
64
|
-
// <pkg>/binaries/promptster-teams-<platform> — a path that no longer exists in
|
|
65
|
-
// the package, because the binary moved to a per-platform dependency and the
|
|
66
|
-
// wrapper stopped shipping binaries/ entirely. So `npm i -g` deletes the exact
|
|
67
|
-
// file the supervisor is pointed at.
|
|
68
|
-
//
|
|
69
|
-
// Nothing about that fails loudly. The running daemon holds its inode and keeps
|
|
70
|
-
// capturing, so the upgrade looks clean; the unit only breaks at the NEXT LOGIN,
|
|
71
|
-
// when launchd runs a path that is gone. Capture never comes back — the precise
|
|
72
|
-
// failure autostart exists to prevent.
|
|
73
|
-
//
|
|
74
|
-
// Delegated to the Go binary (`autostart repair`) rather than reimplemented
|
|
75
|
-
// here: this must not grow a plist writer, a systemd unit writer and a schtasks
|
|
76
|
-
// caller in Node, three platforms out of sync with the Go ones. `repair` is a
|
|
77
|
-
// no-op when autostart was never enabled and never exits non-zero.
|
|
78
|
-
function repairAutostart(managed) {
|
|
79
|
-
try {
|
|
80
|
-
const r = spawnSync(managed, ["autostart", "repair"], {
|
|
81
|
-
encoding: "utf8",
|
|
82
|
-
timeout: 30_000,
|
|
83
|
-
});
|
|
84
|
-
const out = `${r.stdout || ""}${r.stderr || ""}`.trim();
|
|
85
|
-
if (out) console.log(out);
|
|
86
|
-
} catch {
|
|
87
|
-
// Best-effort: a failed repair leaves autostart pointing at the old path,
|
|
88
|
-
// which the engineer can fix with `promptster-teams autostart enable`.
|
|
89
|
-
// Aborting the install over it would be strictly worse.
|
|
90
|
-
}
|
|
91
|
-
}
|
|
23
|
+
const { installManagedBinary } = require("../lib/install");
|
|
24
|
+
const { isGlobalInstall } = require("../lib/resolve");
|
|
92
25
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
// over one file and neither gets what its lockfile selected — a worse bug
|
|
98
|
-
// than the npm-ls drift this whole design removes. Local installs keep
|
|
99
|
-
// running out of their own node_modules, exactly as before.
|
|
26
|
+
const log = (msg) => console.log(`promptster-teams: ${msg}`);
|
|
27
|
+
const warn = (msg) => console.warn(`promptster-teams: ${msg}`);
|
|
28
|
+
|
|
29
|
+
try {
|
|
100
30
|
if (!isGlobalInstall()) {
|
|
101
|
-
|
|
102
|
-
"
|
|
31
|
+
log(
|
|
32
|
+
"project-local install — leaving ~/.promptster-teams/bin alone " +
|
|
103
33
|
"and running the version your lockfile pins"
|
|
104
34
|
);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const bundled = bundledBinPath();
|
|
109
|
-
if (!bundled) {
|
|
110
|
-
// Either an unsupported platform, or the optionalDependency carrying the
|
|
111
|
-
// binary is absent. npm treats a missing optional dep as SUCCESS and says
|
|
112
|
-
// nothing, so this warning is the only signal the engineer will ever get
|
|
113
|
-
// that they have a CLI with no binary behind it. Say which package.
|
|
114
|
-
if (!PLATFORMS.includes(platformKey())) {
|
|
115
|
-
warn(`unsupported platform ${platformKey()} — no binary to install`);
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
warn(`${platformPackage()} is not installed, so there is no binary to install.`);
|
|
119
|
-
warn("If you used --omit=optional or --no-optional, reinstall without it.");
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
const managed = managedBinPath();
|
|
123
|
-
if (!managed) {
|
|
124
|
-
warn("could not resolve home directory — skipping binary install");
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const incoming = binVersion(bundled);
|
|
129
|
-
const current = binVersion(managed);
|
|
130
|
-
|
|
131
|
-
if (!incoming) {
|
|
132
|
-
warn(`bundled binary at ${bundled} did not report a version — skipping`);
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Never downgrade. The managed binary self-updates forward on its own, so it
|
|
137
|
-
// is routinely NEWER than whatever version npm is installing (that is the
|
|
138
|
-
// normal steady state, not an error). Clobbering it would hand the daemon an
|
|
139
|
-
// older build that immediately re-updates — churn, plus a window on a version
|
|
140
|
-
// the engineer already moved past. Mirrors the Go updater's isNewer gate.
|
|
141
|
-
//
|
|
142
|
-
// A managed binary that cannot report a version (current === null) is treated
|
|
143
|
-
// as absent and overwritten: a corrupt or half-written file should be
|
|
144
|
-
// replaced, not preserved by a guard meant to protect a GOOD newer build.
|
|
145
|
-
if (current && !isNewer(incoming, current)) {
|
|
146
|
-
console.log(
|
|
147
|
-
`promptster-teams: ${managed} is ${current}; not replacing it with ${incoming}`
|
|
148
|
-
);
|
|
149
|
-
return;
|
|
35
|
+
} else {
|
|
36
|
+
installManagedBinary({ log, warn });
|
|
150
37
|
}
|
|
151
|
-
|
|
152
|
-
try {
|
|
153
|
-
fs.mkdirSync(path.dirname(managed), { recursive: true });
|
|
154
|
-
// Write to a temp file in the SAME directory, then rename: rename is atomic
|
|
155
|
-
// on POSIX, so a concurrent `promptster-teams` exec sees either the whole
|
|
156
|
-
// old binary or the whole new one, never a half-written file. Copying
|
|
157
|
-
// straight onto `managed` would also fail with ETXTBSY on Linux if the
|
|
158
|
-
// daemon is running.
|
|
159
|
-
const tmp = `${managed}.tmp-${process.pid}`;
|
|
160
|
-
fs.copyFileSync(bundled, tmp);
|
|
161
|
-
fs.chmodSync(tmp, 0o755);
|
|
162
|
-
|
|
163
|
-
// Re-check immediately before the swap. The guard above and this rename are
|
|
164
|
-
// not atomic with respect to the Go self-updater, which renames onto this
|
|
165
|
-
// same path from a live daemon: read 1.0.0 -> daemon installs 1.2.0 ->
|
|
166
|
-
// rename 1.1.0 over it, and the guard has been defeated.
|
|
167
|
-
//
|
|
168
|
-
// This narrows the window from "the whole copy" to the microseconds around
|
|
169
|
-
// the rename rather than closing it. Closing it needs a lock protocol shared
|
|
170
|
-
// by a Node script and a Go daemon, which is a lot of machinery for this
|
|
171
|
-
// failure: rename is atomic so the file is always ONE whole valid binary
|
|
172
|
-
// (never corrupt), the only cost is running an older version, and the daemon
|
|
173
|
-
// re-updates forward within one check interval (<=30m). Deliberate tradeoff,
|
|
174
|
-
// not an oversight — see CLAUDE.md.
|
|
175
|
-
const stillCurrent = binVersion(managed);
|
|
176
|
-
if (stillCurrent && !isNewer(incoming, stillCurrent)) {
|
|
177
|
-
fs.unlinkSync(tmp);
|
|
178
|
-
console.log(
|
|
179
|
-
`promptster-teams: ${managed} changed to ${stillCurrent} mid-install; leaving it`
|
|
180
|
-
);
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
fs.renameSync(tmp, managed);
|
|
184
|
-
console.log(
|
|
185
|
-
`promptster-teams: installed ${incoming} to ${managed}${
|
|
186
|
-
current ? ` (was ${current})` : ""
|
|
187
|
-
}`
|
|
188
|
-
);
|
|
189
|
-
repairAutostart(managed);
|
|
190
|
-
} catch (err) {
|
|
191
|
-
// Falls back to the bundled binary via bin/promptster-teams.js.
|
|
192
|
-
warn(`could not install to ${managed}: ${err.message}`);
|
|
193
|
-
warn("falling back to the bundled binary (npm ls may report a stale version)");
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
try {
|
|
198
|
-
main();
|
|
199
38
|
} catch (err) {
|
|
200
39
|
// Belt and braces: nothing here may abort an npm install.
|
|
201
40
|
warn(`postinstall skipped: ${err && err.message}`);
|