@join-pantheon/cli 0.33.2 → 0.33.4-rc.1
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/accounts.js +53 -0
- package/dist/board.js +1 -2
- package/dist/cli.js +183 -22
- package/dist/config.js +2 -1
- package/dist/disk-writer.js +404 -0
- package/dist/doctor.js +6 -3
- package/dist/ledger.js +31 -14
- package/dist/notifier.js +1 -1
- package/dist/quickstart.js +18 -6
- package/dist/recover-offer.js +125 -0
- package/dist/relay.js +258 -74
- package/dist/roster-file.js +11 -6
- package/dist/store.js +28 -7
- package/dist/team-name.js +58 -0
- package/dist/tray.js +5 -0
- package/dist/worker.js +5 -3
- package/helper/macos/main.swift +5 -3
- package/helper/macos/prebuilt/manifest.json +29 -0
- package/helper/macos/prebuilt/pantheon-hud +0 -0
- package/helper/macos/prebuilt/pantheon-tray +0 -0
- package/installer/install.ps1 +12 -2
- package/installer/install.sh +5 -0
- package/package.json +1 -1
package/dist/roster-file.js
CHANGED
|
@@ -3,7 +3,7 @@ import { randomBytes } from "node:crypto";
|
|
|
3
3
|
import { createHash, timingSafeEqual } from "node:crypto";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { fingerprint } from "./identity.js";
|
|
6
|
-
import {
|
|
6
|
+
import { pendingView, writerFor } from "./disk-writer.js";
|
|
7
7
|
export class RosterUnreadable extends Error {
|
|
8
8
|
file;
|
|
9
9
|
constructor(file, detail) {
|
|
@@ -18,6 +18,8 @@ function hashToken(token) {
|
|
|
18
18
|
return createHash("sha256").update(token).digest("hex");
|
|
19
19
|
}
|
|
20
20
|
export class Allowlist {
|
|
21
|
+
dir;
|
|
22
|
+
stem;
|
|
21
23
|
entries = new Map();
|
|
22
24
|
invites = new Map();
|
|
23
25
|
tokenHash = null;
|
|
@@ -31,12 +33,15 @@ export class Allowlist {
|
|
|
31
33
|
repos = new Map();
|
|
32
34
|
recoveries = new Map();
|
|
33
35
|
file;
|
|
34
|
-
constructor(dir,
|
|
35
|
-
this.
|
|
36
|
-
|
|
36
|
+
constructor(dir, stem) {
|
|
37
|
+
this.dir = dir;
|
|
38
|
+
this.stem = stem;
|
|
39
|
+
this.file = join(dir, `${stem}.members.json`);
|
|
40
|
+
const pending = pendingView(dir, stem, this.file);
|
|
41
|
+
if (pending !== null && (pending !== undefined || existsSync(this.file))) {
|
|
37
42
|
let raw;
|
|
38
43
|
try {
|
|
39
|
-
raw = JSON.parse(readFileSync(this.file, "utf8"));
|
|
44
|
+
raw = JSON.parse(pending ? pending.toString("utf8") : readFileSync(this.file, "utf8"));
|
|
40
45
|
}
|
|
41
46
|
catch (e) {
|
|
42
47
|
throw new RosterUnreadable(this.file, e.message);
|
|
@@ -118,7 +123,7 @@ export class Allowlist {
|
|
|
118
123
|
}
|
|
119
124
|
founderGuess() { return this.founderGuessed; }
|
|
120
125
|
flush() {
|
|
121
|
-
|
|
126
|
+
writerFor(this.dir, this.stem).replace(this.file, JSON.stringify({
|
|
122
127
|
...this.extra,
|
|
123
128
|
...(this.id ? { id: this.id } : {}),
|
|
124
129
|
...(this.founderName ? { founder: this.founderName } : {}),
|
package/dist/store.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync, existsSync, mkdirSync, renameSync, openSync, writeSync, fsyncSync, closeSync, chmodSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { emptyRefusals } from "./admin-stats.js";
|
|
4
|
+
import { pendingView, writerFor } from "./disk-writer.js";
|
|
4
5
|
export function ensurePrivateDir(dir) {
|
|
5
6
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
6
7
|
try {
|
|
@@ -13,6 +14,8 @@ let slowestWaitMs = 0;
|
|
|
13
14
|
export function setWaitForDisk(on) { waitForDisk = on; }
|
|
14
15
|
export function waitsForDisk() { return waitForDisk; }
|
|
15
16
|
export function takeSlowestDiskWait() { const w = slowestWaitMs; slowestWaitMs = 0; return w; }
|
|
17
|
+
export function noteDiskWait(ms) { if (ms > slowestWaitMs)
|
|
18
|
+
slowestWaitMs = ms; }
|
|
16
19
|
export function syncFd(fd) {
|
|
17
20
|
if (!waitForDisk)
|
|
18
21
|
return;
|
|
@@ -22,8 +25,21 @@ export function syncFd(fd) {
|
|
|
22
25
|
if (took > slowestWaitMs)
|
|
23
26
|
slowestWaitMs = took;
|
|
24
27
|
}
|
|
28
|
+
export function slowDiskMs() {
|
|
29
|
+
const v = process.env.PANTHEON_RELAY_SLOW_DISK_MS ?? "";
|
|
30
|
+
const range = /^(\d+)-(\d+)$/.exec(v);
|
|
31
|
+
if (range) {
|
|
32
|
+
const a = Number(range[1]), b = Number(range[2]);
|
|
33
|
+
return a + Math.random() * Math.max(0, b - a);
|
|
34
|
+
}
|
|
35
|
+
const n = Number(v);
|
|
36
|
+
return Number.isFinite(n) && n > 0 ? Math.random() * n : 0;
|
|
37
|
+
}
|
|
25
38
|
export function writeFileAtomic(file, data, mode = 0o600) {
|
|
26
39
|
mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
|
|
40
|
+
const slow = slowDiskMs();
|
|
41
|
+
if (slow)
|
|
42
|
+
sleepSync(slow);
|
|
27
43
|
const tmp = `${file}.tmp`;
|
|
28
44
|
const fd = openSync(tmp, "w", mode);
|
|
29
45
|
try {
|
|
@@ -36,7 +52,7 @@ export function writeFileAtomic(file, data, mode = 0o600) {
|
|
|
36
52
|
renameRetrying(tmp, file);
|
|
37
53
|
fsyncDir(dirname(file));
|
|
38
54
|
}
|
|
39
|
-
const sleepSync = (ms) => { try {
|
|
55
|
+
export const sleepSync = (ms) => { try {
|
|
40
56
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
41
57
|
}
|
|
42
58
|
catch { } };
|
|
@@ -73,6 +89,8 @@ export function fsyncDir(dir) {
|
|
|
73
89
|
}
|
|
74
90
|
}
|
|
75
91
|
export class StateStore {
|
|
92
|
+
dir;
|
|
93
|
+
stem;
|
|
76
94
|
file;
|
|
77
95
|
timer = null;
|
|
78
96
|
dirty = false;
|
|
@@ -80,18 +98,21 @@ export class StateStore {
|
|
|
80
98
|
headOf;
|
|
81
99
|
rebuilt = null;
|
|
82
100
|
extra = {};
|
|
83
|
-
constructor(dir,
|
|
84
|
-
this.
|
|
101
|
+
constructor(dir, stem) {
|
|
102
|
+
this.dir = dir;
|
|
103
|
+
this.stem = stem;
|
|
104
|
+
this.file = join(dir, `${stem}.state.json`);
|
|
85
105
|
}
|
|
86
106
|
load(opts = {}) {
|
|
87
107
|
const goals = new Map();
|
|
88
108
|
const obligations = new Map();
|
|
89
109
|
const profiles = new Map();
|
|
90
|
-
|
|
110
|
+
const pending = pendingView(this.dir, this.stem, this.file);
|
|
111
|
+
if (pending === null || (pending === undefined && !existsSync(this.file))) {
|
|
91
112
|
return { goals, obligations, profiles, unclean: opts.expectHistory ? "the state snapshot is missing but the ledger has history" : null };
|
|
92
113
|
}
|
|
93
114
|
try {
|
|
94
|
-
const snap = JSON.parse(readFileSync(this.file, "utf8"));
|
|
115
|
+
const snap = JSON.parse(pending ? pending.toString("utf8") : readFileSync(this.file, "utf8"));
|
|
95
116
|
if (!snap || typeof snap !== "object" || Array.isArray(snap)
|
|
96
117
|
|| (snap.goals !== undefined && !Array.isArray(snap.goals))
|
|
97
118
|
|| (snap.obligations !== undefined && !Array.isArray(snap.obligations))) {
|
|
@@ -120,7 +141,7 @@ export class StateStore {
|
|
|
120
141
|
catch (e) {
|
|
121
142
|
const why = `the state snapshot could not be read (${e.message})`;
|
|
122
143
|
let quarantined;
|
|
123
|
-
if (opts.quarantine) {
|
|
144
|
+
if (opts.quarantine && !pending) {
|
|
124
145
|
const aside = `${this.file}.corrupt-${Date.now()}`;
|
|
125
146
|
try {
|
|
126
147
|
renameSync(this.file, aside);
|
|
@@ -162,7 +183,7 @@ export class StateStore {
|
|
|
162
183
|
...(this.rebuilt ? { rebuilt: this.rebuilt } : {}),
|
|
163
184
|
saved_at: Date.now(),
|
|
164
185
|
};
|
|
165
|
-
|
|
186
|
+
writerFor(this.dir, this.stem).replace(this.file, JSON.stringify(snap, null, 2) + "\n");
|
|
166
187
|
}
|
|
167
188
|
close(goals, obligations, profiles) {
|
|
168
189
|
if (this.timer) {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export const TEAM_NAME = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
|
|
2
|
+
export const RELAY_TEAM_NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
|
|
3
|
+
export const PLACEHOLDER_REFUSAL = "that looks like a placeholder from an example — pick your team's real name";
|
|
4
|
+
export function looksLikePlaceholder(raw) {
|
|
5
|
+
const s = raw.trim();
|
|
6
|
+
if (!s)
|
|
7
|
+
return false;
|
|
8
|
+
if (/[<>{}[\]]/.test(s))
|
|
9
|
+
return true;
|
|
10
|
+
if (/^\$\{?\w+\}?$/.test(s) || /^%\w+%$/.test(s))
|
|
11
|
+
return true;
|
|
12
|
+
if (/\.\.\.|…/.test(s))
|
|
13
|
+
return true;
|
|
14
|
+
const n = s.toLowerCase().replace(/[\s_.-]+/g, "");
|
|
15
|
+
return /^(your|the|my|a|some|new)?(team|space|org|organi[sz]ation|workspace|group)(name|slug|id)(here)?$/.test(n)
|
|
16
|
+
|| /^(your)(own)?(team|space|org|organi[sz]ation|workspace|group)(here)?$/.test(n)
|
|
17
|
+
|| /^(insert|put|enter|type|add)(your|the|a)?(team|space)(name)?(here)?$/.test(n)
|
|
18
|
+
|| /^(example|sample|placeholder|changeme|replaceme|tbd|todo|x{3,8}|name|nameofyourteam|nameoftheteam|teamhere|namehere)$/.test(n)
|
|
19
|
+
|| /^(example|sample|placeholder)(team|space)(name)?$/.test(n);
|
|
20
|
+
}
|
|
21
|
+
export function teamSlug(raw) {
|
|
22
|
+
const s = raw.toLowerCase().normalize("NFKD").replace(/[̀-ͯ]/g, "")
|
|
23
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
24
|
+
.replace(/-{2,}/g, "-")
|
|
25
|
+
.replace(/^[-_]+/, "")
|
|
26
|
+
.slice(0, 64)
|
|
27
|
+
.replace(/[-_]+$/, "");
|
|
28
|
+
return TEAM_NAME.test(s) ? s : "";
|
|
29
|
+
}
|
|
30
|
+
export function teamNameProblem(name, opts = {}) {
|
|
31
|
+
if (looksLikePlaceholder(name))
|
|
32
|
+
return PLACEHOLDER_REFUSAL;
|
|
33
|
+
if ((opts.relay ? RELAY_TEAM_NAME : TEAM_NAME).test(name))
|
|
34
|
+
return null;
|
|
35
|
+
const slug = teamSlug(name);
|
|
36
|
+
return `"${name}" cannot be a team name: use 1–64 letters, digits, - or _, starting with a letter or digit (it goes into invite links)` +
|
|
37
|
+
(slug ? ` — for example "${slug}"` : "");
|
|
38
|
+
}
|
|
39
|
+
export function shellArg(name, platform = process.platform) {
|
|
40
|
+
if (TEAM_NAME.test(name))
|
|
41
|
+
return name;
|
|
42
|
+
const dq = anyShellQuoted(name);
|
|
43
|
+
if (dq)
|
|
44
|
+
return dq;
|
|
45
|
+
return platform === "win32" ? psQuoted(name) : posixQuoted(name);
|
|
46
|
+
}
|
|
47
|
+
export function anyShellQuoted(name) {
|
|
48
|
+
return /[$`"\\!%]/.test(name) ? null : `"${name}"`;
|
|
49
|
+
}
|
|
50
|
+
export const posixQuoted = (name) => `'${name.replace(/'/g, `'\\''`)}'`;
|
|
51
|
+
export const psQuoted = (name) => `'${name.replace(/'/g, "''")}'`;
|
|
52
|
+
export const CMD_SPECIAL = /[&%^|<>]/;
|
|
53
|
+
export function relayShellArg(name) {
|
|
54
|
+
if (TEAM_NAME.test(name))
|
|
55
|
+
return name;
|
|
56
|
+
return anyShellQuoted(name) ?? `${posixQuoted(name)} (in PowerShell: ${psQuoted(name)})`;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=team-name.js.map
|
package/dist/tray.js
CHANGED
|
@@ -27,6 +27,11 @@ export function ensureTrayBinary(home = CONFIG_DIR, deps = {}) {
|
|
|
27
27
|
mkdirSync(dirname(binary), { recursive: true });
|
|
28
28
|
return installHelper({ name: TRAY_PREBUILT, source, binary, what: "the menu bar", service: "menubar" }, deps).binary;
|
|
29
29
|
}
|
|
30
|
+
export function menubarRefusal(f, here) {
|
|
31
|
+
if (Object.keys(f?.spaces ?? {}).length || here?.space)
|
|
32
|
+
return null;
|
|
33
|
+
return "this machine is not on a team yet — pantheon start (create one) or pantheon join <invite> (join one)";
|
|
34
|
+
}
|
|
30
35
|
export function trayStale(home = CONFIG_DIR) { return helperStale(trayBinaryPath(home), traySourcePath()); }
|
|
31
36
|
export function writeTrayCommands(home = CONFIG_DIR) {
|
|
32
37
|
const bin = whichPantheon() ?? "pantheon";
|
package/dist/worker.js
CHANGED
|
@@ -31,6 +31,7 @@ import { diagnoseRun, gitProbe, workNeedsCommands } from "./run-failure.js";
|
|
|
31
31
|
import { installedTools, preferredTool } from "./notifier.js";
|
|
32
32
|
import { loadPersonas, matchPersona, personaByName, personaMatches, effectivePolicy, personaBrief, describePersona, } from "./persona.js";
|
|
33
33
|
import { CONFIG_PATH, loadAll, repoInfo, updateMembershipToken } from "./config.js";
|
|
34
|
+
import { shellArg } from "./team-name.js";
|
|
34
35
|
import { repoName, routeRequest, safeName } from "./repo-route.js";
|
|
35
36
|
import { holdsRepo } from "./repo-find.js";
|
|
36
37
|
import { FOLDER_SNAPSHOT_LIMITS, keepDrafts, removeSnapshot, shown, snapshotDrafts, snapshotRoot, sweepSnapshots, takeSnapshot } from "./folder-snapshot.js";
|
|
@@ -1412,14 +1413,15 @@ export class Worker {
|
|
|
1412
1413
|
this.keyChangeSaid.add(k);
|
|
1413
1414
|
this.markDone(k);
|
|
1414
1415
|
const mine = e.member === this.cfg.member;
|
|
1415
|
-
|
|
1416
|
+
const spaceArg = shellArg(this.cfg.space);
|
|
1417
|
+
this.opts.log(`! ${mine ? "someone asked to move YOUR seat" : `${e.member} asked to move their seat`} in ${this.cfg.space} to a new device ("${r.label}", key ${r.fp}) — ${mine ? `not you? pantheon recover cancel ${e.member} --space ${spaceArg}` : `check it with them, then pantheon recover confirm ${e.member} --space ${spaceArg} (or cancel)`}`);
|
|
1416
1418
|
try {
|
|
1417
1419
|
this.opts.notify?.({
|
|
1418
1420
|
kind: "worker", who: "Your worker", context: this.cfg.space, ttl: 0, sound: true,
|
|
1419
1421
|
heading: mine ? "Someone asked to move your seat" : `${e.member} asked to move their seat`,
|
|
1420
1422
|
body: mine
|
|
1421
|
-
? `A device signed in to your email account ("${r.label}", key ${r.fp}) asked for your seat in ${this.cfg.space}. It moves ${new Date(r.due_at).toLocaleString()} unless cancelled. Not you? In a terminal: pantheon recover cancel ${e.member} — and secure your email.`
|
|
1422
|
-
: `${e.member} signed in on a new device ("${r.label}", key ${r.fp}). Check the key with them on a call, then in a terminal: pantheon recover confirm ${e.member}. Not them? pantheon recover cancel ${e.member}.`,
|
|
1423
|
+
? `A device signed in to your email account ("${r.label}", key ${r.fp}) asked for your seat in ${this.cfg.space}. It moves ${new Date(r.due_at).toLocaleString()} unless cancelled. Not you? In a terminal: pantheon recover cancel ${e.member} --space ${spaceArg} — and secure your email.`
|
|
1424
|
+
: `${e.member} signed in on a new device ("${r.label}", key ${r.fp}). Check the key with them on a call, then in a terminal: pantheon recover confirm ${e.member} --space ${spaceArg}. Not them? pantheon recover cancel ${e.member} --space ${spaceArg}.`,
|
|
1423
1425
|
});
|
|
1424
1426
|
}
|
|
1425
1427
|
catch { }
|
package/helper/macos/main.swift
CHANGED
|
@@ -48,6 +48,8 @@ struct Notice: Decodable {
|
|
|
48
48
|
let primary: Primary?
|
|
49
49
|
let details: Details?
|
|
50
50
|
let fix: Fix?
|
|
51
|
+
/** the team it came from (0.33.4): written into every action, so the CLI answers in that team with no default picked */
|
|
52
|
+
let space: String?
|
|
51
53
|
/** where to write the chosen action and which .command files to open — supplied by the CLI */
|
|
52
54
|
let bin_dir: String
|
|
53
55
|
/** the pantheon CLI to run silently for one-click actions — supplied by the CLI, since this helper has no PATH worth trusting */
|
|
@@ -135,21 +137,21 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|
|
135
137
|
} else { NSApp.terminate(nil) }
|
|
136
138
|
return
|
|
137
139
|
}
|
|
138
|
-
act(["action": "open", "tool": toolId, "prompt": p.prompt, "decision": notice.details?.decision as Any, "act": notice.details?.act as Any].compactMapValues { $0 is NSNull ? nil : $0 }, command: "pantheon-explain.command")
|
|
140
|
+
act(["action": "open", "tool": toolId, "prompt": p.prompt, "decision": notice.details?.decision as Any, "act": notice.details?.act as Any, "space": notice.space as Any].compactMapValues { $0 is NSNull ? nil : $0 }, command: "pantheon-explain.command")
|
|
139
141
|
}
|
|
140
142
|
|
|
141
143
|
@objc func openTool(_ sender: NSMenuItem) { open((sender.representedObject as? String) ?? notice.primary?.default ?? "") }
|
|
142
144
|
@objc func openDefault(_ sender: Any?) { open(notice.primary?.default ?? "") }
|
|
143
145
|
|
|
144
146
|
@objc func showDetails(_ sender: Any?) {
|
|
145
|
-
act(["action": "details", "act": notice.details?.act as Any, "decision": notice.details?.decision as Any].compactMapValues { $0 is NSNull ? nil : $0 }, command: "pantheon-details.command")
|
|
147
|
+
act(["action": "details", "act": notice.details?.act as Any, "decision": notice.details?.decision as Any, "space": notice.space as Any].compactMapValues { $0 is NSNull ? nil : $0 }, command: "pantheon-details.command")
|
|
146
148
|
}
|
|
147
149
|
|
|
148
150
|
/// Done: write the action, run the CLI without a Terminal, close. The CLI answers the decision as handled.
|
|
149
151
|
@objc func markDone(_ sender: Any?) {
|
|
150
152
|
guard !notice.bin_dir.isEmpty, let decision = notice.details?.decision else { NSApp.terminate(nil); return }
|
|
151
153
|
let dir = URL(fileURLWithPath: notice.bin_dir)
|
|
152
|
-
let payload: [String: Any] = ["action": "done", "decision": decision, "act": notice.details?.act as Any].compactMapValues { $0 is NSNull ? nil : $0 }
|
|
154
|
+
let payload: [String: Any] = ["action": "done", "decision": decision, "act": notice.details?.act as Any, "space": notice.space as Any].compactMapValues { $0 is NSNull ? nil : $0 }
|
|
153
155
|
if let data = try? JSONSerialization.data(withJSONObject: payload) {
|
|
154
156
|
try? data.write(to: dir.appendingPathComponent(".hud-action.json"), options: .atomic)
|
|
155
157
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"format": 1,
|
|
3
|
+
"built_at": "2026-10-07T10:18:03.223Z",
|
|
4
|
+
"swiftc": "Apple Swift version 6.3.3 (swiftlang-6.3.3.1.3 clang-2100.1.1.101)",
|
|
5
|
+
"helpers": {
|
|
6
|
+
"pantheon-tray": {
|
|
7
|
+
"source": "menubar.swift",
|
|
8
|
+
"source_sha256": "f079629099a81f9b679ef4133d897ceb09025f675099c5f941951604df3531fa",
|
|
9
|
+
"sha256": "cc081120555f528c83b38951663d37e8394e3a3e119a19b0bb3b2a737f4d617f",
|
|
10
|
+
"archs": [
|
|
11
|
+
"arm64",
|
|
12
|
+
"x86_64"
|
|
13
|
+
],
|
|
14
|
+
"target": "macos11",
|
|
15
|
+
"version": "2"
|
|
16
|
+
},
|
|
17
|
+
"pantheon-hud": {
|
|
18
|
+
"source": "main.swift",
|
|
19
|
+
"source_sha256": "ffda2f8dbec3144ac902f952df1048affce6ec0e6f1b2c65ff87643a484c55c6",
|
|
20
|
+
"sha256": "3eb87eddbfba8c7d149b10ee34921e336d5670a5fa89243758184e5204264ccb",
|
|
21
|
+
"archs": [
|
|
22
|
+
"arm64",
|
|
23
|
+
"x86_64"
|
|
24
|
+
],
|
|
25
|
+
"target": "macos11",
|
|
26
|
+
"version": "4"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
Binary file
|
|
Binary file
|
package/installer/install.ps1
CHANGED
|
@@ -601,6 +601,14 @@ function Install-Pantheon {
|
|
|
601
601
|
return @{ code = $p.ExitCode; out = $out.Result; err = $err.Result }
|
|
602
602
|
}
|
|
603
603
|
|
|
604
|
+
# Exit 3 from pantheon: it ended having said what happens next, and nothing
|
|
605
|
+
# failed -- a seat move that waits for a teammate to confirm it (recover, or
|
|
606
|
+
# a join of a team this person is already in). Not a failed install, and its
|
|
607
|
+
# words stay the last ones: no "Next" line after them.
|
|
608
|
+
function Resolve-PantheonSaid([hashtable]$St) {
|
|
609
|
+
if ($St.code -eq 3) { $St.code = 0; $St.said = $true }
|
|
610
|
+
}
|
|
611
|
+
|
|
604
612
|
# Hand over to pantheon with the person's own arguments (join <invite>,
|
|
605
613
|
# start): after an install, and also when the update was put off because a
|
|
606
614
|
# teammate's run was in progress -- a join or a start needs a working
|
|
@@ -623,6 +631,7 @@ function Install-Pantheon {
|
|
|
623
631
|
$null = $p.Handle # without this, ExitCode is empty once the process is gone
|
|
624
632
|
$p.WaitForExit()
|
|
625
633
|
$State.code = $p.ExitCode
|
|
634
|
+
Resolve-PantheonSaid $State
|
|
626
635
|
return
|
|
627
636
|
}
|
|
628
637
|
|
|
@@ -644,6 +653,7 @@ function Install-Pantheon {
|
|
|
644
653
|
$null = $p.Handle
|
|
645
654
|
$p.WaitForExit()
|
|
646
655
|
$State.code = $p.ExitCode
|
|
656
|
+
Resolve-PantheonSaid $State
|
|
647
657
|
$said = if (Test-Path -LiteralPath $errFile) { [IO.File]::ReadAllText($errFile) } else { '' }
|
|
648
658
|
if ($said) { [Console]::Error.Write($said) }
|
|
649
659
|
if ($State.code -ne 0 -and $said -match 'sign in first|NOT_SIGNED_IN' -and $Arguments[0] -in @('join', 'start')) {
|
|
@@ -1152,7 +1162,7 @@ function Install-Pantheon {
|
|
|
1152
1162
|
}
|
|
1153
1163
|
Say ''
|
|
1154
1164
|
Invoke-PantheonHandoff $nodeExe $entry $ca $Arguments
|
|
1155
|
-
if ($State.code -eq 0) {
|
|
1165
|
+
if ($State.code -eq 0 -and -not $State.said) {
|
|
1156
1166
|
Say ''
|
|
1157
1167
|
Say "Next, in this window: $(& $nextFor @('doctor')) (checks that everything works)"
|
|
1158
1168
|
& $byPathNote
|
|
@@ -1174,7 +1184,7 @@ function Install-Pantheon {
|
|
|
1174
1184
|
try {
|
|
1175
1185
|
Say ''
|
|
1176
1186
|
Invoke-PantheonHandoff $nodeExe $entry $caNow $Arguments
|
|
1177
|
-
if ($State.code -eq 0 -and $nextFor) {
|
|
1187
|
+
if ($State.code -eq 0 -and -not $State.said -and $nextFor) {
|
|
1178
1188
|
Say ''
|
|
1179
1189
|
Say "Next, in this window: $(& $nextFor @('doctor')) (checks that everything works)"
|
|
1180
1190
|
& $byPathNote
|
package/installer/install.sh
CHANGED
|
@@ -714,6 +714,11 @@ export PATH
|
|
|
714
714
|
# pantheon (a trap is reset in a child, where an ignored signal would not be)
|
|
715
715
|
# and this shell waits for it rather than dying first.
|
|
716
716
|
finish() {
|
|
717
|
+
# 3: pantheon ended having said what happens next, and nothing failed -- a
|
|
718
|
+
# seat move that waits for a teammate to confirm it (recover, or a join of a
|
|
719
|
+
# team this person is already in). Its words stay the last ones, and the
|
|
720
|
+
# install is not reported as failed.
|
|
721
|
+
if [ "$1" -eq 3 ]; then exit 0; fi
|
|
717
722
|
if [ "$1" -eq 0 ]; then
|
|
718
723
|
say ""
|
|
719
724
|
say "Next, in this terminal: $(next_line doctor) (checks that everything works)"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@join-pantheon/cli",
|
|
3
|
-
"version": "0.33.
|
|
3
|
+
"version": "0.33.4-rc.1",
|
|
4
4
|
"description": "Let your coding agent collaborate directly with your teammates' agents — ask, agree an interface, and deliver, without a human relaying messages.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|