@join-pantheon/cli 0.33.0-rc.1 → 0.33.0-rc.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/dist/board-peer.js +136 -0
- package/dist/board.js +30 -46
- package/dist/cli.js +89 -50
- package/dist/client.js +16 -0
- package/dist/config.js +7 -1
- package/dist/home-safety.js +195 -0
- package/dist/machine.js +111 -6
- package/dist/service.js +5 -1
- package/dist/uninstall.js +1 -1
- package/dist/win-host.js +59 -7
- package/helper/macos/prebuilt/manifest.json +1 -1
- package/helper/windows/pantheon-notice.ps1 +44 -16
- package/installer/install.ps1 +130 -56
- package/installer/install.sh +15 -1
- package/package.json +1 -1
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, realpathSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir, platform, userInfo } from "node:os";
|
|
4
|
+
import { basename, dirname, join, posix, sep, win32 } from "node:path";
|
|
5
|
+
export const HOME_MARKER = ".pantheon-home";
|
|
6
|
+
export const PANTHEON_HOME_ENTRIES = [
|
|
7
|
+
{ name: HOME_MARKER, what: "the mark that says this folder is Pantheon's home" },
|
|
8
|
+
{ name: "config.json", what: "your key and team tokens", identity: true },
|
|
9
|
+
{ name: "config.json.bak", what: "the previous config.json", identity: true },
|
|
10
|
+
{ name: "keys.json", what: "the teammates' keys this machine pinned", identity: true },
|
|
11
|
+
{ name: "nonces.json", what: "the signed requests this machine has seen", identity: true },
|
|
12
|
+
{ name: "continued.json", what: "what this machine already handled", identity: true },
|
|
13
|
+
{ name: "cursors.json", what: "where each agent session last read", identity: true },
|
|
14
|
+
{ name: "worker-cursor.json", what: "where the worker last read", identity: true },
|
|
15
|
+
{ name: "profile.json", what: "your profile", identity: true },
|
|
16
|
+
{ name: "worker-policy.json", what: "the worker's wake policy", identity: true },
|
|
17
|
+
{ name: "spend.json", what: "the spend ledger", identity: true },
|
|
18
|
+
{ name: "personas", what: "the worker's personas", dir: true, identity: true },
|
|
19
|
+
{ name: "worker-state.json", what: "the worker's last state" },
|
|
20
|
+
{ name: "worker-run.json", what: "the worker's record of a run in flight" },
|
|
21
|
+
{ name: "run-slot.json", what: "the worker's run slot" },
|
|
22
|
+
{ name: "worker.pid", what: "the worker's pid file" },
|
|
23
|
+
{ name: /^notifier-state(-.+)?\.json$/, sample: "notifier-state-team%2Fone.json", what: "the alerts' state" },
|
|
24
|
+
{ name: "update-check.json", what: "the update check's cache" },
|
|
25
|
+
{ name: "uri-key", what: "the key pantheon:// links are signed with" },
|
|
26
|
+
{ name: "board.json", what: "the board's state" },
|
|
27
|
+
{ name: "hook-intent.json", what: "the hook intent" },
|
|
28
|
+
{ name: "hook-guard.json", what: "the hook guard" },
|
|
29
|
+
{ name: /^(worker|relay|notifier|menubar)\.log(\.1)?$/, sample: "worker.log", what: "service logs an older version wrote here" },
|
|
30
|
+
{ name: /^\.write-check\..+$/, sample: ".write-check.1234.5678", what: "a leftover write probe" },
|
|
31
|
+
{ name: "bin", what: "the menu bar's and alerts' files", dir: true },
|
|
32
|
+
{ name: "logs", what: "the service logs", dir: true },
|
|
33
|
+
{ name: "host", what: "the Windows host's queue, state and markers", dir: true },
|
|
34
|
+
{ name: "mcp", what: "the agent windows' server state", dir: true },
|
|
35
|
+
{ name: "run-homes", what: "the woken runs' homes", dir: true },
|
|
36
|
+
{ name: "worktrees", what: "the worker's worktrees", dir: true },
|
|
37
|
+
{ name: "no-hooks", what: "the empty hooks folder woken runs' git uses", dir: true },
|
|
38
|
+
{ name: "lfs-hooks", what: "the LFS-only hooks folder woken runs' git uses", dir: true },
|
|
39
|
+
{ name: "runtime", what: "Pantheon's own Node", dir: true, kept: "managed" },
|
|
40
|
+
{ name: "data", what: "the relay's ledger", dir: true, kept: "it is your team's history and may be its only copy; delete it yourself once no relay runs from it" },
|
|
41
|
+
{ name: /^(\.DS_Store|Thumbs\.db|desktop\.ini)$/i, sample: ".DS_Store", what: "the file browser's folder cache" },
|
|
42
|
+
];
|
|
43
|
+
const LOCK_OR_TMP = /^(.+?)(\.lock|(\.[0-9a-f]+)+\.tmp)$/i;
|
|
44
|
+
export function homeEntryFor(name) {
|
|
45
|
+
const direct = (n) => PANTHEON_HOME_ENTRIES.find((e) => typeof e.name === "string" ? e.name === n : e.name.test(n)) ?? null;
|
|
46
|
+
const e = direct(name);
|
|
47
|
+
if (e)
|
|
48
|
+
return e;
|
|
49
|
+
const m = LOCK_OR_TMP.exec(name);
|
|
50
|
+
const base = m ? direct(m[1]) : null;
|
|
51
|
+
return base && !base.dir ? base : null;
|
|
52
|
+
}
|
|
53
|
+
const realOrNull = (p) => { try {
|
|
54
|
+
return realpathSync.native(p);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
} };
|
|
59
|
+
function gitTopOf(dir) {
|
|
60
|
+
const env = { ...process.env };
|
|
61
|
+
for (const k of ["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_COMMON_DIR", "GIT_CEILING_DIRECTORIES"])
|
|
62
|
+
delete env[k];
|
|
63
|
+
try {
|
|
64
|
+
const out = execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd: dir, env, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000, windowsHide: true }).trim();
|
|
65
|
+
return out ? realOrNull(out) ?? out : null;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export const realHomeCheckIo = () => ({
|
|
72
|
+
real: realOrNull,
|
|
73
|
+
exists: (p) => existsSync(p),
|
|
74
|
+
read: (p) => { try {
|
|
75
|
+
return readFileSync(p, "utf8");
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
} },
|
|
80
|
+
homes: () => {
|
|
81
|
+
const out = [homedir()];
|
|
82
|
+
try {
|
|
83
|
+
const u = userInfo().homedir;
|
|
84
|
+
if (u)
|
|
85
|
+
out.push(u);
|
|
86
|
+
}
|
|
87
|
+
catch { }
|
|
88
|
+
if (process.env.USERPROFILE)
|
|
89
|
+
out.push(process.env.USERPROFILE);
|
|
90
|
+
return out;
|
|
91
|
+
},
|
|
92
|
+
cwd: () => process.cwd(),
|
|
93
|
+
repoTop: gitTopOf,
|
|
94
|
+
os: platform(),
|
|
95
|
+
});
|
|
96
|
+
export function isPantheonHomeName(dir, os = platform()) {
|
|
97
|
+
const b = (os === "win32" ? win32 : posix).basename(dir);
|
|
98
|
+
return os === "win32" ? [".pantheon", "pantheon"].includes(b.toLowerCase()) : b === ".pantheon" || b === "Pantheon";
|
|
99
|
+
}
|
|
100
|
+
function holdsPantheonIdentity(dir, io, pj = join) {
|
|
101
|
+
if (!io.exists(pj(dir, "keys.json")))
|
|
102
|
+
return false;
|
|
103
|
+
const raw = io.read(pj(dir, "config.json"));
|
|
104
|
+
if (raw === null)
|
|
105
|
+
return false;
|
|
106
|
+
try {
|
|
107
|
+
const c = JSON.parse(raw.replace(/^/, ""));
|
|
108
|
+
return !!c && typeof c === "object" && typeof c.member === "string" && typeof c.publicKey === "string" && typeof c.privateKey === "string" && typeof c.relay === "string";
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
export function pantheonHomeRefusal(dir, over = {}) {
|
|
115
|
+
const io = { ...realHomeCheckIo(), ...over };
|
|
116
|
+
const r = io.real(dir);
|
|
117
|
+
if (r === null)
|
|
118
|
+
return null;
|
|
119
|
+
const win = io.os === "win32";
|
|
120
|
+
const P = win ? win32 : posix;
|
|
121
|
+
const norm = (p) => { const s = p.replace(/[\\/]+$/, "") || p; return win ? s.replace(/\//g, "\\").toLowerCase() : s; };
|
|
122
|
+
const S = win ? "\\" : sep;
|
|
123
|
+
const d = norm(r);
|
|
124
|
+
const inside = (child, parent) => child.startsWith(parent.endsWith(S) ? parent : parent + S);
|
|
125
|
+
if (P.dirname(r) === r || /^[a-z]:\\?$/i.test(d) || d === "")
|
|
126
|
+
return `${dir} is the root of a ${win ? "drive" : "filesystem"}`;
|
|
127
|
+
const homes = [...new Set(io.homes().map((h) => io.real(h) ?? h).map(norm))];
|
|
128
|
+
for (const h of homes) {
|
|
129
|
+
if (d === h)
|
|
130
|
+
return `${dir} is your home folder`;
|
|
131
|
+
if (inside(h, d))
|
|
132
|
+
return `${dir} holds your home folder (${h})`;
|
|
133
|
+
}
|
|
134
|
+
const cwd = io.real(io.cwd());
|
|
135
|
+
if (cwd && norm(cwd) === d)
|
|
136
|
+
return `${dir} is the folder this command runs in`;
|
|
137
|
+
if (io.exists(P.join(r, ".git")))
|
|
138
|
+
return `${dir} is a git repository`;
|
|
139
|
+
let walked = null;
|
|
140
|
+
for (let p = P.dirname(r), prev = r; p !== prev; prev = p, p = P.dirname(p)) {
|
|
141
|
+
if (io.exists(P.join(p, ".git"))) {
|
|
142
|
+
walked = p;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const top = io.repoTop(r) ?? walked;
|
|
147
|
+
if (top) {
|
|
148
|
+
const t = norm(top);
|
|
149
|
+
const dotfiles = homes.some((h) => t === h && inside(d, h));
|
|
150
|
+
if (t === d)
|
|
151
|
+
return `${dir} is a git repository`;
|
|
152
|
+
if (!dotfiles)
|
|
153
|
+
return `${dir} is inside the git repository ${top}`;
|
|
154
|
+
}
|
|
155
|
+
if (!isPantheonHomeName(r, io.os) && !io.exists(P.join(r, HOME_MARKER)) && !holdsPantheonIdentity(r, io, P.join)) {
|
|
156
|
+
return `${dir} is not a folder Pantheon made: it is not named .pantheon, it has no ${HOME_MARKER} mark, and it holds no Pantheon identity`;
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
export function pantheonNamedFiles(dir) {
|
|
161
|
+
let names = [];
|
|
162
|
+
try {
|
|
163
|
+
names = readdirSync(dir);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
return names.sort().filter((n) => homeEntryFor(n)).map((n) => join(dir, n));
|
|
169
|
+
}
|
|
170
|
+
export function refusedHomeLine(dir, why) {
|
|
171
|
+
const files = pantheonNamedFiles(dir).filter((p) => !/^(\.DS_Store|Thumbs\.db|desktop\.ini)$/i.test(basename(p)));
|
|
172
|
+
const cwd = /the folder this command runs in$/.test(why);
|
|
173
|
+
return `nothing in ${dir} was removed: ${why}, so it may hold files of your own under the names Pantheon uses.` +
|
|
174
|
+
(cwd ? " Run the command again from another folder to have Pantheon's files there removed." : "") +
|
|
175
|
+
(files.length ? ` Pantheon's files there, if they are Pantheon's — check each, then delete it yourself: ${files.join(", ")}` : " No file there has a name Pantheon writes.");
|
|
176
|
+
}
|
|
177
|
+
export function markPantheonHome(dir, createdNow, over = {}) {
|
|
178
|
+
try {
|
|
179
|
+
const f = join(dir, HOME_MARKER);
|
|
180
|
+
if (existsSync(f))
|
|
181
|
+
return;
|
|
182
|
+
const io = { ...realHomeCheckIo(), ...over };
|
|
183
|
+
const r = io.real(dir);
|
|
184
|
+
if (r === null || dirname(r) === r)
|
|
185
|
+
return;
|
|
186
|
+
const homes = io.homes().map((h) => io.real(h) ?? h);
|
|
187
|
+
if (homes.some((h) => h === r || h.startsWith(r.endsWith(sep) ? r : r + sep)))
|
|
188
|
+
return;
|
|
189
|
+
if (!createdNow && readdirSync(r).some((n) => !/^(config\.json|keys\.json)(\.tmp[\w.-]*|\.lock)?$/.test(n)))
|
|
190
|
+
return;
|
|
191
|
+
writeFileSync(f, "This folder is Pantheon's home. `pantheon uninstall` removes what Pantheon wrote here, and nothing else.\n", { flag: "wx", mode: 0o600 });
|
|
192
|
+
}
|
|
193
|
+
catch { }
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=home-safety.js.map
|
package/dist/machine.js
CHANGED
|
@@ -6,6 +6,7 @@ import { CONFIG_DIR } from "./config.js";
|
|
|
6
6
|
import { borrowedHome, disableService, existingUnitPath, plistPath, serviceInstalled, serviceLogFile, stopWindowsHost, unitPath, unitPaths, uninstallService } from "./service.js";
|
|
7
7
|
import { LEGACY_TASK_NAME, hostAlive, hostChildRunning, hostFiles, isHostKind, readHostState, recordedTaskName } from "./win-host.js";
|
|
8
8
|
import { copilotMcpConfigFile, vsCodeUserDir, kiroMcpConfigFile, kiroDir } from "./install.js";
|
|
9
|
+
import { HOME_MARKER, homeEntryFor, isPantheonHomeName, pantheonHomeRefusal, refusedHomeLine } from "./home-safety.js";
|
|
9
10
|
const sh = (cmd) => {
|
|
10
11
|
try {
|
|
11
12
|
return execFileSync("sh", ["-lc", cmd], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
|
|
@@ -667,6 +668,9 @@ export function removePantheonWorktrees(repos, roots, git = (repo, args) => exec
|
|
|
667
668
|
const MANAGED_LAUNCHER = "pantheon";
|
|
668
669
|
export function removeHomeLeftovers(home, keep, opts = {}) {
|
|
669
670
|
const removed = [], left = [];
|
|
671
|
+
const refusal = opts.refusal !== undefined ? opts.refusal : pantheonHomeRefusal(home);
|
|
672
|
+
if (refusal)
|
|
673
|
+
return { removed, left: [refusedHomeLine(home, refusal)] };
|
|
670
674
|
const items = [
|
|
671
675
|
["worker-policy.json", "the worker's wake policy (who may wake it, and self-upgrade) — a rejoin starts from no grants"],
|
|
672
676
|
["profile.json", "your profile (name, title, what you decide)"],
|
|
@@ -705,6 +709,104 @@ export function removeHomeLeftovers(home, keep, opts = {}) {
|
|
|
705
709
|
}
|
|
706
710
|
return { removed, left };
|
|
707
711
|
}
|
|
712
|
+
export { PANTHEON_HOME_ENTRIES, homeEntryFor } from "./home-safety.js";
|
|
713
|
+
const pause = (ms) => { try {
|
|
714
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
715
|
+
}
|
|
716
|
+
catch { } };
|
|
717
|
+
const realSweepFs = {
|
|
718
|
+
list: (d) => readdirSync(d),
|
|
719
|
+
exists: (p) => existsSync(p),
|
|
720
|
+
remove: (p) => rmSync(p, { recursive: true, force: true }),
|
|
721
|
+
removeDir: (d) => rmdirSync(d),
|
|
722
|
+
};
|
|
723
|
+
export function sweepPantheonHome(home, o = {}) {
|
|
724
|
+
const fs = o.fs ?? realSweepFs;
|
|
725
|
+
const tries = o.tries ?? 5, waitMs = o.waitMs ?? 200;
|
|
726
|
+
const os = o.os ?? platform();
|
|
727
|
+
const removed = [], left = [];
|
|
728
|
+
if (!fs.exists(home))
|
|
729
|
+
return { removed, left, gone: true };
|
|
730
|
+
const refusal = o.refusal !== undefined ? o.refusal : o.fs ? null : pantheonHomeRefusal(home);
|
|
731
|
+
if (refusal)
|
|
732
|
+
return { removed, left: [refusedHomeLine(home, refusal)], gone: false };
|
|
733
|
+
const spare = new Set((o.spare ?? []).map((p) => resolve(p)));
|
|
734
|
+
const attempt = (p, rm) => {
|
|
735
|
+
let why = "";
|
|
736
|
+
for (let i = 0; i < tries; i++) {
|
|
737
|
+
try {
|
|
738
|
+
rm(p);
|
|
739
|
+
}
|
|
740
|
+
catch (e) {
|
|
741
|
+
why = String(e.code ?? e.message.split("\n")[0]);
|
|
742
|
+
}
|
|
743
|
+
if (!fs.exists(p))
|
|
744
|
+
return null;
|
|
745
|
+
if (i < tries - 1)
|
|
746
|
+
pause(waitMs * (i + 1));
|
|
747
|
+
}
|
|
748
|
+
return why || "still there";
|
|
749
|
+
};
|
|
750
|
+
const lockedHint = os === "win32" ? "; a Pantheon process (an agent window, the menu bar) may still have it open — close it, then delete it" : "; delete it yourself";
|
|
751
|
+
const gone = [], foreign = [], keptForIdentity = [];
|
|
752
|
+
let names = [];
|
|
753
|
+
try {
|
|
754
|
+
names = fs.list(home);
|
|
755
|
+
}
|
|
756
|
+
catch (e) {
|
|
757
|
+
return { removed, left: [`${home} — could not be read (${e.message.split("\n")[0]}), so nothing in it was removed`], gone: false };
|
|
758
|
+
}
|
|
759
|
+
for (const name of names.sort()) {
|
|
760
|
+
const p = join(home, name);
|
|
761
|
+
const e = homeEntryFor(name);
|
|
762
|
+
if (!e) {
|
|
763
|
+
foreign.push(name);
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
if (name === HOME_MARKER)
|
|
767
|
+
continue;
|
|
768
|
+
if (spare.has(resolve(p)))
|
|
769
|
+
continue;
|
|
770
|
+
if (e.kept === "managed")
|
|
771
|
+
continue;
|
|
772
|
+
if (e.kept) {
|
|
773
|
+
left.push(`${e.what}, ${p} — kept: ${e.kept}`);
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
if (o.keepIdentity && e.identity) {
|
|
777
|
+
keptForIdentity.push(name);
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
const why = attempt(p, fs.remove);
|
|
781
|
+
if (why === null)
|
|
782
|
+
gone.push(name);
|
|
783
|
+
else
|
|
784
|
+
left.push(`${e.what}, ${p} — could not remove (${why})${lockedHint}`);
|
|
785
|
+
}
|
|
786
|
+
if (gone.length)
|
|
787
|
+
removed.push(`Pantheon's own files in ${home}: ${gone.join(", ")}`);
|
|
788
|
+
if (keptForIdentity.length)
|
|
789
|
+
left.push(`what goes with your identity in ${home}: ${keptForIdentity.join(", ")} (--keep-identity)`);
|
|
790
|
+
let rest = [];
|
|
791
|
+
try {
|
|
792
|
+
rest = fs.list(home);
|
|
793
|
+
}
|
|
794
|
+
catch { }
|
|
795
|
+
if (rest.includes(HOME_MARKER) && (rest.length === 1 || isPantheonHomeName(home, os)) && attempt(join(home, HOME_MARKER), fs.remove) === null)
|
|
796
|
+
rest = rest.filter((n) => n !== HOME_MARKER);
|
|
797
|
+
if (!rest.length) {
|
|
798
|
+
const why = attempt(home, fs.removeDir);
|
|
799
|
+
if (why === null) {
|
|
800
|
+
removed.push(`${home} itself, now empty`);
|
|
801
|
+
return { removed, left, gone: true };
|
|
802
|
+
}
|
|
803
|
+
left.push(`${home} — empty, but could not remove it (${why})${lockedHint}`);
|
|
804
|
+
return { removed, left, gone: false };
|
|
805
|
+
}
|
|
806
|
+
if (foreign.length)
|
|
807
|
+
left.push(`${home}, for what is not Pantheon's: ${foreign.join(", ")} — Pantheon never writes ${foreign.length === 1 ? "it" : "these"}, so ${foreign.length === 1 ? "it was" : "they were"} not touched`);
|
|
808
|
+
return { removed, left, gone: false };
|
|
809
|
+
}
|
|
708
810
|
export const accountStillExists = (email) => `your account${email ? ` ${email}` : ""} still exists — this machine is signed out. Sign back in with: pantheon login${email ? ` --email ${email}` : ""} (pantheon signup would fail: the email is taken). To delete the account itself: pantheon account delete, before leaving`;
|
|
709
811
|
const LOG_KINDS = ["worker", "relay", "notifier", "menubar"];
|
|
710
812
|
export function uninstallMachine(opts, io) {
|
|
@@ -749,20 +851,22 @@ export function uninstallMachine(opts, io) {
|
|
|
749
851
|
if (!opts.stopRelay && (io.installed("relay") || io.proc("relay").running)) {
|
|
750
852
|
left.push("the relay — others may be using it. Stop and remove it too: pantheon leave --stop-relay");
|
|
751
853
|
}
|
|
752
|
-
if (io.hasIntent()) {
|
|
854
|
+
if (!io.homeRefusal && io.hasIntent()) {
|
|
753
855
|
io.clearIntent();
|
|
754
856
|
removed.push("the hook intent (hook-intent.json) — nothing will put the hooks back");
|
|
755
857
|
}
|
|
756
858
|
const mcp = io.removeMcpEntries();
|
|
757
859
|
removed.push(...mcp.removed);
|
|
758
860
|
left.push(...mcp.left);
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
861
|
+
if (!io.homeRefusal) {
|
|
862
|
+
try {
|
|
863
|
+
if (existsSync(io.logDir)) {
|
|
864
|
+
rmSync(io.logDir, { recursive: true, force: true });
|
|
865
|
+
removed.push(`service logs (${io.logDir})`);
|
|
866
|
+
}
|
|
763
867
|
}
|
|
868
|
+
catch { }
|
|
764
869
|
}
|
|
765
|
-
catch { }
|
|
766
870
|
const repoDir = join(opts.project, ".pantheon");
|
|
767
871
|
let repoLogs = 0;
|
|
768
872
|
for (const k of LOG_KINDS) {
|
|
@@ -916,6 +1020,7 @@ export function realUninstallIo(deps) {
|
|
|
916
1020
|
hasIntent: deps.hasIntent,
|
|
917
1021
|
clearIntent: deps.clearIntent,
|
|
918
1022
|
logDir: join(CONFIG_DIR, "logs"),
|
|
1023
|
+
homeRefusal: deps.homeRefusal !== undefined ? deps.homeRefusal : pantheonHomeRefusal(CONFIG_DIR),
|
|
919
1024
|
ownWorkerPid: () => ownWorkerPid(),
|
|
920
1025
|
borrowed: () => borrowedHome(),
|
|
921
1026
|
kill: (pid) => killPid(pid) === null,
|
package/dist/service.js
CHANGED
|
@@ -7,7 +7,7 @@ import { linuxGraphicalSession } from "./host-env.js";
|
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { isVersionManagedPath, launcher } from "./install.js";
|
|
9
9
|
import { childNodeFlags, managedLauncher, managedLayout } from "./managed.js";
|
|
10
|
-
import { CONFIG_DIR } from "./config.js";
|
|
10
|
+
import { CONFIG_DIR, ensurePantheonHome } from "./config.js";
|
|
11
11
|
import { HOST_KINDS, LEGACY_TASK_NAME, clearTrayQuit, ensureHostShim, fromUtf16le, hostAlive, hostArgv, hostChildRunning, hostFiles, hostLaunch, isHostKind, launchFileText, legacyTaskIsUsers, readDefinition, readHostState, taskName, taskUserId, taskXml, utf16le, windowsPowerShell, writeDefinition, } from "./win-host.js";
|
|
12
12
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
13
13
|
const LABEL = (kind) => `dev.pantheon.${kind}`;
|
|
@@ -272,6 +272,10 @@ export function installService(kind, cwd, args, sys = realSys()) {
|
|
|
272
272
|
const noServices = servicesUnavailable(sys.platform);
|
|
273
273
|
if (noServices)
|
|
274
274
|
throw new Error(noServices.replace(/^not available — /, ""));
|
|
275
|
+
try {
|
|
276
|
+
ensurePantheonHome();
|
|
277
|
+
}
|
|
278
|
+
catch { }
|
|
275
279
|
if (sys.platform === "win32")
|
|
276
280
|
return installWindows(kind, resolve(cwd), args, sys);
|
|
277
281
|
const out = [];
|
package/dist/uninstall.js
CHANGED
|
@@ -5,7 +5,7 @@ import { posix, win32 } from "node:path";
|
|
|
5
5
|
import { expectedLauncher } from "./managed.js";
|
|
6
6
|
export const BEGIN_MARK = "# >>> pantheon >>>";
|
|
7
7
|
export const END_MARK = "# <<< pantheon <<<";
|
|
8
|
-
export const PROFILES = [".profile", ".bash_profile", ".bashrc", ".zshrc", ".zprofile"];
|
|
8
|
+
export const PROFILES = [".profile", ".bash_profile", ".bash_login", ".bashrc", ".zshrc", ".zprofile"];
|
|
9
9
|
export const FISH_CONF = ".config/fish/conf.d/pantheon.fish";
|
|
10
10
|
export function withoutPathBlock(text) {
|
|
11
11
|
const lines = text.match(/[^\n]*\n|[^\n]+$/g) ?? [];
|
package/dist/win-host.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
2
|
+
import { closeSync, existsSync, linkSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { dirname, join, resolve, win32 } from "node:path";
|
|
@@ -633,10 +633,12 @@ export function realHostIo(o = {}) {
|
|
|
633
633
|
exit: (code) => process.exit(code),
|
|
634
634
|
};
|
|
635
635
|
}
|
|
636
|
-
export
|
|
636
|
+
export const HOST_LOCK_STARTUP_GRACE_MS = 2 * 60_000;
|
|
637
|
+
export function takeHostLock(home = CONFIG_DIR, pid = process.pid, now = Date.now, alive = pidAlive) {
|
|
637
638
|
const f = hostFiles(home);
|
|
638
639
|
mkdirSync(f.dir, { recursive: true });
|
|
639
|
-
|
|
640
|
+
let lastHolder = -1;
|
|
641
|
+
for (let i = 0; i < 4; i++) {
|
|
640
642
|
try {
|
|
641
643
|
const fd = openSync(f.lock, "wx");
|
|
642
644
|
writeSync(fd, String(pid));
|
|
@@ -646,13 +648,63 @@ export function takeHostLock(home = CONFIG_DIR, pid = process.pid) {
|
|
|
646
648
|
catch (e) {
|
|
647
649
|
if (e.code !== "EEXIST")
|
|
648
650
|
throw e;
|
|
649
|
-
|
|
650
|
-
|
|
651
|
+
}
|
|
652
|
+
const judged = readFileSafe(f.lock);
|
|
653
|
+
const held = Number(judged);
|
|
654
|
+
let age = 0;
|
|
655
|
+
try {
|
|
656
|
+
age = now() - statSync(f.lock).mtimeMs;
|
|
657
|
+
}
|
|
658
|
+
catch {
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
if (Number.isInteger(held) && held > 0) {
|
|
662
|
+
lastHolder = held;
|
|
663
|
+
if (held === pid)
|
|
664
|
+
return null;
|
|
665
|
+
if (alive(held) && (hostAlive(readHostState(home)) || age < HOST_LOCK_STARTUP_GRACE_MS))
|
|
651
666
|
return held;
|
|
652
|
-
rmSync(f.lock, { force: true });
|
|
653
667
|
}
|
|
668
|
+
else if (age < 10_000) {
|
|
669
|
+
return lastHolder;
|
|
670
|
+
}
|
|
671
|
+
const gone = `${f.lock}.stale-${pid}-${i}`;
|
|
672
|
+
try {
|
|
673
|
+
renameSync(f.lock, gone);
|
|
674
|
+
}
|
|
675
|
+
catch {
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
const moved = readFileSafe(gone);
|
|
679
|
+
if (moved !== judged) {
|
|
680
|
+
putBack(gone, f.lock);
|
|
681
|
+
const other = Number(moved);
|
|
682
|
+
return Number.isInteger(other) && other > 0 ? other : -1;
|
|
683
|
+
}
|
|
684
|
+
rmSync(gone, { force: true });
|
|
654
685
|
}
|
|
655
|
-
return
|
|
686
|
+
return lastHolder;
|
|
687
|
+
}
|
|
688
|
+
function putBack(moved, lock) {
|
|
689
|
+
try {
|
|
690
|
+
linkSync(moved, lock);
|
|
691
|
+
rmSync(moved, { force: true });
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
catch (e) {
|
|
695
|
+
if (e.code === "EEXIST") {
|
|
696
|
+
rmSync(moved, { force: true });
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
if (existsSync(lock)) {
|
|
701
|
+
rmSync(moved, { force: true });
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
try {
|
|
705
|
+
renameSync(moved, lock);
|
|
706
|
+
}
|
|
707
|
+
catch { }
|
|
656
708
|
}
|
|
657
709
|
const readFileSafe = (p) => { try {
|
|
658
710
|
return readFileSync(p, "utf8").trim();
|
|
@@ -30,37 +30,65 @@
|
|
|
30
30
|
#>
|
|
31
31
|
param([string]$Mode = '')
|
|
32
32
|
|
|
33
|
+
# Whether toasts are switched off for an app id: only an explicit Disabled*
|
|
34
|
+
# setting is off. Windows THROWS "Element not found" (0x80070490) for an app id
|
|
35
|
+
# that has never shown a toast for this user, and a plain property read turned
|
|
36
|
+
# that into $null -- which was taken for "off" for every id, so a fresh PC only
|
|
37
|
+
# ever got balloons. Unreadable is unknown, and unknown is tried.
|
|
38
|
+
function global:Get-PantheonToastSetting($Notifier) {
|
|
39
|
+
$s = $null
|
|
40
|
+
try { $s = $Notifier.get_Setting() } catch { try { $s = $Notifier.Setting } catch { $s = $null } }
|
|
41
|
+
if ($null -eq $s) { return 'unknown' }
|
|
42
|
+
return [string]$s
|
|
43
|
+
}
|
|
44
|
+
|
|
33
45
|
# One toast: the XML, tried under each app id in turn. Returns @{ code; said }.
|
|
34
|
-
|
|
46
|
+
# $Api stands in for the WinRT calls (the checks run it on Linux); left out,
|
|
47
|
+
# the real ToastNotificationManager is used.
|
|
48
|
+
function global:Show-PantheonToast([string]$Xml, [string]$Tag, [string[]]$Aumids, [hashtable]$Api = $null) {
|
|
35
49
|
$said = New-Object System.Collections.Generic.List[string]
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
$doc = $null
|
|
51
|
+
if (-not $Api) {
|
|
52
|
+
try {
|
|
53
|
+
[void][Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime]
|
|
54
|
+
[void][Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime]
|
|
55
|
+
$doc = [Windows.Data.Xml.Dom.XmlDocument]::new()
|
|
56
|
+
$doc.LoadXml($Xml)
|
|
57
|
+
} catch {
|
|
58
|
+
$said.Add('no-toast-api ' + $_.Exception.Message)
|
|
59
|
+
return @{ code = 3; said = $said }
|
|
60
|
+
}
|
|
61
|
+
$Api = @{
|
|
62
|
+
notifier = { param($id) [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($id) }
|
|
63
|
+
toast = { param($d) [Windows.UI.Notifications.ToastNotification]::new($d) }
|
|
64
|
+
history = { param($id) [Windows.UI.Notifications.ToastNotificationManager]::History.GetHistory($id) }
|
|
65
|
+
remove = { param($t, $id) [Windows.UI.Notifications.ToastNotificationManager]::History.Remove($t, 'pantheon', $id) }
|
|
66
|
+
wait = { Start-Sleep -Milliseconds 400 }
|
|
67
|
+
}
|
|
44
68
|
}
|
|
45
69
|
$ids = @($Aumids | Where-Object { $_ })
|
|
46
70
|
for ($i = 0; $i -lt $ids.Count; $i++) {
|
|
47
71
|
$id = $ids[$i]
|
|
48
72
|
$last = ($i -eq $ids.Count - 1)
|
|
49
73
|
try {
|
|
50
|
-
$notifier =
|
|
51
|
-
|
|
52
|
-
$
|
|
74
|
+
$notifier = & $Api.notifier $id
|
|
75
|
+
$setting = Get-PantheonToastSetting $notifier
|
|
76
|
+
if ($setting -like 'Disabled*') { $said.Add('off ' + $id + ' ' + $setting); continue }
|
|
77
|
+
$toast = & $Api.toast $doc
|
|
53
78
|
if ($Tag) { $toast.Tag = $Tag; $toast.Group = 'pantheon' }
|
|
54
79
|
$notifier.Show($toast)
|
|
55
80
|
# Pantheon's own app id is used only when Windows then has the toast in
|
|
56
81
|
# its history for that id: a build that ignores the registration falls
|
|
57
82
|
# through to the next id instead of swallowing the alert.
|
|
58
|
-
|
|
59
|
-
|
|
83
|
+
# The last id too, when Windows would not say its setting: an unknown
|
|
84
|
+
# setting that Windows then drops silently must still end in a balloon.
|
|
85
|
+
$knownOn = ($setting -eq 'Enabled')
|
|
86
|
+
if ($Tag -and (-not $last -or -not $knownOn)) {
|
|
87
|
+
& $Api.wait
|
|
60
88
|
$seen = $false
|
|
61
|
-
foreach ($h in
|
|
89
|
+
foreach ($h in @(& $Api.history $id)) { if ($h.Tag -eq $Tag) { $seen = $true } }
|
|
62
90
|
if (-not $seen) {
|
|
63
|
-
try {
|
|
91
|
+
try { & $Api.remove $Tag $id } catch { }
|
|
64
92
|
$said.Add('unseen ' + $id)
|
|
65
93
|
continue
|
|
66
94
|
}
|