@join-pantheon/cli 0.19.1 → 0.21.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/README.md +38 -1
- package/dist/accounts.js +167 -1
- package/dist/admin-stats.js +116 -0
- package/dist/agents.js +6 -0
- package/dist/cli.js +417 -94
- package/dist/client.js +52 -7
- package/dist/config.js +243 -23
- package/dist/doctor.js +22 -1
- package/dist/mcp.js +67 -24
- package/dist/notifier.js +27 -7
- package/dist/notify.js +1 -1
- package/dist/pending.js +3 -1
- package/dist/persona.js +4 -1
- package/dist/plans.js +46 -0
- package/dist/profile.js +33 -4
- package/dist/protocol.js +22 -1
- package/dist/quickstart.js +37 -7
- package/dist/relay.js +732 -49
- package/dist/roster-file.js +39 -0
- package/dist/setup-guide.js +238 -0
- package/dist/store.js +5 -0
- package/dist/tray.js +22 -1
- package/dist/untrusted.js +16 -3
- package/dist/worker-fleet.js +84 -0
- package/dist/worker.js +116 -19
- package/package.json +2 -2
package/dist/notifier.js
CHANGED
|
@@ -17,10 +17,10 @@ export function dueHumanObligations(obligations, member, state, now, reminderMs)
|
|
|
17
17
|
return last === undefined || now - last >= reminderMs;
|
|
18
18
|
});
|
|
19
19
|
}
|
|
20
|
-
export function pruneNotificationState(state, obligations, member) {
|
|
20
|
+
export function pruneNotificationState(state, obligations, member, now = Date.now()) {
|
|
21
21
|
const active = new Set(humanObligations(obligations, member).map((o) => o.id));
|
|
22
22
|
return {
|
|
23
|
-
notified_at: Object.fromEntries(Object.entries(state.notified_at).filter(([id]) => active.has(id))),
|
|
23
|
+
notified_at: Object.fromEntries(Object.entries(state.notified_at).filter(([id, at]) => active.has(id) || (id.startsWith("act:") && now - at < 7 * 86_400_000))),
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
26
|
function readState(file) {
|
|
@@ -108,14 +108,17 @@ export class HumanNotifier {
|
|
|
108
108
|
this.opts.log(`notifier online as ${this.cfg.member}`);
|
|
109
109
|
this.opts.log(` reminders every ${Math.round(this.opts.reminderMs / 60_000)} minutes while unresolved`);
|
|
110
110
|
while (!this.stopped) {
|
|
111
|
-
const pruned = pruneNotificationState(state, client.obligations, this.cfg.member);
|
|
111
|
+
const pruned = pruneNotificationState(state, client.obligations, this.cfg.member, now());
|
|
112
112
|
let dirty = JSON.stringify(pruned) !== JSON.stringify(state);
|
|
113
113
|
state = pruned;
|
|
114
|
-
const
|
|
114
|
+
const profile = loadProfile(this.cfg);
|
|
115
|
+
const prefs = profile.notifications ?? { decisions: true, deliveries: false, worker_done: false };
|
|
116
|
+
const due = prefs.decisions
|
|
117
|
+
? dueHumanObligations(client.obligations, this.cfg.member, state, now(), this.opts.reminderMs)
|
|
118
|
+
: [];
|
|
115
119
|
for (const obligation of due) {
|
|
116
|
-
const profile = loadProfile(this.cfg);
|
|
117
120
|
const goal = obligation.goal ? client.goals.get(obligation.goal)?.title : undefined;
|
|
118
|
-
const ok = send("Your input is needed", oneLine(goal ?? "Pantheon decision"
|
|
121
|
+
const ok = send("Your input is needed", oneLine(`${this.opts.label ? `${this.opts.label} · ` : ""}${goal ?? "Pantheon decision"}`, 80), decisionTitle(obligation.body, 180), profile, decisionPrompt(obligation.id));
|
|
119
122
|
if (ok) {
|
|
120
123
|
state.notified_at[obligation.id] = now();
|
|
121
124
|
dirty = true;
|
|
@@ -127,7 +130,24 @@ export class HumanNotifier {
|
|
|
127
130
|
}
|
|
128
131
|
if (dirty || !existsSync(stateFile))
|
|
129
132
|
writeState(stateFile, state);
|
|
130
|
-
await client.next(pollMs);
|
|
133
|
+
const arrived = await client.next(pollMs);
|
|
134
|
+
if (prefs.deliveries) {
|
|
135
|
+
for (const r of arrived) {
|
|
136
|
+
if (r.act !== "DELIVERED" || r.from === this.cfg.member || state.notified_at[`act:${r.id}`])
|
|
137
|
+
continue;
|
|
138
|
+
const goal = r.goal ? client.goals.get(r.goal) : undefined;
|
|
139
|
+
const forMe = (r.to?.includes(this.cfg.member) ?? false) || (goal?.members.includes(this.cfg.member) ?? false);
|
|
140
|
+
if (!forMe)
|
|
141
|
+
continue;
|
|
142
|
+
const m = r.meta;
|
|
143
|
+
const ok = send("Work delivered to you", oneLine(`${this.opts.label ? `${this.opts.label} · ` : ""}${goal?.title ?? "Pantheon delivery"}`, 80), oneLine(`${r.from} pushed ${m?.branch ?? "a branch"}${m?.commit ? ` @ ${m.commit.slice(0, 7)}` : ""} — ${r.body}`, 180), profile, `A teammate delivered work in pantheon: fetch ${m?.branch ?? "their branch"}${m?.commit ? ` at ${m.commit}` : ""} from ${r.from}, check it against the goal "${goal?.title ?? r.goal ?? ""}", then ACCEPT or OBJECT.`, "Delivered");
|
|
144
|
+
if (ok) {
|
|
145
|
+
state.notified_at[`act:${r.id}`] = now();
|
|
146
|
+
writeState(stateFile, state);
|
|
147
|
+
this.opts.log(`> delivery alert for ${r.id} from ${r.from}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
131
151
|
}
|
|
132
152
|
client.close();
|
|
133
153
|
}
|
package/dist/notify.js
CHANGED
|
@@ -290,7 +290,7 @@ export async function decideStop(raw, deps) {
|
|
|
290
290
|
const acts = report.acts ?? [];
|
|
291
291
|
const owed = report.obligations ?? [];
|
|
292
292
|
const closed = new Set(report.closed_goals ?? []);
|
|
293
|
-
const DEMANDS_RESPONSE = new Set(["ASK", "PROPOSE", "OBJECT", "DELIVERED", "CONTRACT"
|
|
293
|
+
const DEMANDS_RESPONSE = new Set(["ASK", "PROPOSE", "OBJECT", "DELIVERED", "CONTRACT"]);
|
|
294
294
|
const work = acts.filter((a) => {
|
|
295
295
|
if (a.goal && closed.has(a.goal))
|
|
296
296
|
return false;
|
package/dist/pending.js
CHANGED
|
@@ -102,10 +102,12 @@ export async function pending(opts) {
|
|
|
102
102
|
const obligations = surfaced.filter((o) => !alreadySeen.has(o.id));
|
|
103
103
|
const head = obligationsBody?.seq;
|
|
104
104
|
const all = (historyBody?.acts ?? []);
|
|
105
|
+
const mine = all.filter((r) => r.from === cfg.member);
|
|
105
106
|
const acts = all
|
|
106
107
|
.filter((r) => r.from !== cfg.member)
|
|
107
108
|
.filter((r) => r.act !== "JOIN" && r.act !== "LEAVE")
|
|
108
|
-
.filter((r) => !r.to || r.to.length === 0 || r.to.includes(cfg.member))
|
|
109
|
+
.filter((r) => !r.to || r.to.length === 0 || r.to.includes(cfg.member))
|
|
110
|
+
.filter((r) => !mine.some((m) => m.seq > r.seq && (m.ref === r.id || (!!r.goal && m.goal === r.goal))));
|
|
109
111
|
const seq = first
|
|
110
112
|
? head
|
|
111
113
|
: historyBody === null
|
package/dist/persona.js
CHANGED
|
@@ -5,7 +5,7 @@ import { CONFIG_DIR } from "./config.js";
|
|
|
5
5
|
export const PERSONA_INSTRUCTIONS_MAX = 8_000;
|
|
6
6
|
const KNOWN_KEYS = new Set([
|
|
7
7
|
"allow_bash", "max_runs_per_hour", "max_runs_per_day",
|
|
8
|
-
"wake_by", "kinds", "goals", "agent", "name",
|
|
8
|
+
"wake_by", "kinds", "goals", "agent", "name", "spaces",
|
|
9
9
|
]);
|
|
10
10
|
const list = (v) => v.split(",").map((s) => s.trim()).filter(Boolean);
|
|
11
11
|
export function parsePersona(name, scope, file, raw) {
|
|
@@ -63,6 +63,9 @@ export function parsePersona(name, scope, file, raw) {
|
|
|
63
63
|
case "goals":
|
|
64
64
|
p.goals = list(value);
|
|
65
65
|
break;
|
|
66
|
+
case "spaces":
|
|
67
|
+
p.spaces = list(value);
|
|
68
|
+
break;
|
|
66
69
|
case "agent":
|
|
67
70
|
p.agent = value.trim();
|
|
68
71
|
break;
|
package/dist/plans.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export const UNLIMITED = 1_000_000;
|
|
2
|
+
export const DEFAULT_PLANS = {
|
|
3
|
+
free: { spaces: 1, members: 2, wake: true },
|
|
4
|
+
pro: { spaces: 10, members: 25, wake: true },
|
|
5
|
+
};
|
|
6
|
+
export const PLAN_NAME = /^[a-z][a-z0-9_-]{0,23}$/;
|
|
7
|
+
const int = (v, min, max) => typeof v === "number" && Number.isInteger(v) && v >= min && v <= max ? v : null;
|
|
8
|
+
export function parseLimits(raw) {
|
|
9
|
+
if (!raw || typeof raw !== "object")
|
|
10
|
+
return null;
|
|
11
|
+
const o = raw;
|
|
12
|
+
const num = (v) => typeof v === "string" && v.trim() !== "" ? Number(v) : v;
|
|
13
|
+
const spaces = int(num(o["spaces"]), 0, UNLIMITED);
|
|
14
|
+
const members = int(num(o["members"]), 1, UNLIMITED);
|
|
15
|
+
const wake = typeof o["wake"] === "boolean" ? o["wake"] : o["wake"] === "true" || o["wake"] === "on" || o["wake"] === "1" ? true : o["wake"] === "false" || o["wake"] === "off" || o["wake"] === "0" || o["wake"] === undefined ? false : null;
|
|
16
|
+
if (spaces === null || members === null || wake === null)
|
|
17
|
+
return null;
|
|
18
|
+
return { spaces, members, wake };
|
|
19
|
+
}
|
|
20
|
+
export function parsePlans(raw) {
|
|
21
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
22
|
+
return null;
|
|
23
|
+
const out = {};
|
|
24
|
+
for (const [name, lim] of Object.entries(raw)) {
|
|
25
|
+
if (!PLAN_NAME.test(name))
|
|
26
|
+
return null;
|
|
27
|
+
const l = parseLimits(lim);
|
|
28
|
+
if (!l)
|
|
29
|
+
return null;
|
|
30
|
+
out[name] = l;
|
|
31
|
+
}
|
|
32
|
+
return out["free"] ? out : null;
|
|
33
|
+
}
|
|
34
|
+
export function planLimits(plans, plan, override) {
|
|
35
|
+
if (override) {
|
|
36
|
+
try {
|
|
37
|
+
const l = parseLimits(JSON.parse(override));
|
|
38
|
+
if (l)
|
|
39
|
+
return l;
|
|
40
|
+
}
|
|
41
|
+
catch { }
|
|
42
|
+
}
|
|
43
|
+
return plans[plan] ?? plans["free"] ?? DEFAULT_PLANS["free"];
|
|
44
|
+
}
|
|
45
|
+
export const fmtLimit = (n) => n >= UNLIMITED ? "unlimited" : String(n);
|
|
46
|
+
//# sourceMappingURL=plans.js.map
|
package/dist/profile.js
CHANGED
|
@@ -25,6 +25,7 @@ export function defaultProfile(cfg) {
|
|
|
25
25
|
human: { display_name: cfg.display ?? cfg.member, title: "", responsibilities: [] },
|
|
26
26
|
agent: { primary_surface: surfaceFromAgent(cfg.agent), wake_enabled: false },
|
|
27
27
|
attention: { urgent: "primary_surface", decision: "primary_surface", fyi: "inbox" },
|
|
28
|
+
notifications: { decisions: true, deliveries: false, worker_done: false },
|
|
28
29
|
authority: {},
|
|
29
30
|
decision_keywords: {},
|
|
30
31
|
capabilities: [],
|
|
@@ -32,6 +33,9 @@ export function defaultProfile(cfg) {
|
|
|
32
33
|
updated_at: 0,
|
|
33
34
|
};
|
|
34
35
|
}
|
|
36
|
+
export function wakeDefault(profile) {
|
|
37
|
+
return (profile.agent.wake_default ?? "anyone") === "anyone";
|
|
38
|
+
}
|
|
35
39
|
export function loadProfile(cfg, file = profilePath()) {
|
|
36
40
|
if (!existsSync(file))
|
|
37
41
|
return defaultProfile(cfg);
|
|
@@ -53,11 +57,13 @@ export function loadProfile(cfg, file = profilePath()) {
|
|
|
53
57
|
},
|
|
54
58
|
agent: { ...defaultProfile(cfg).agent, ...raw.agent, primary_surface: surface },
|
|
55
59
|
attention: { ...defaultProfile(cfg).attention, ...raw.attention },
|
|
60
|
+
notifications: { ...defaultProfile(cfg).notifications, ...(raw.notifications ?? {}) },
|
|
56
61
|
authority: raw.authority ?? {},
|
|
57
62
|
decision_keywords: cleanKeywords(legacy.decision_keywords),
|
|
58
63
|
capabilities: Array.isArray(raw.capabilities) ? raw.capabilities.filter((x) => typeof x === "string") : [],
|
|
59
64
|
delegations: cleanDelegations(legacy.delegations),
|
|
60
65
|
updated_at: Number.isFinite(legacy.updated_at) ? legacy.updated_at : 0,
|
|
66
|
+
...(Object.keys(sanitizeSpaces(raw.spaces)).length ? { spaces: sanitizeSpaces(raw.spaces) } : {}),
|
|
61
67
|
};
|
|
62
68
|
}
|
|
63
69
|
catch {
|
|
@@ -95,16 +101,39 @@ function cleanDelegations(value) {
|
|
|
95
101
|
return [{ to: normalize(d.to), domains, starts_at: d.starts_at, expires_at: d.expires_at }];
|
|
96
102
|
});
|
|
97
103
|
}
|
|
98
|
-
|
|
104
|
+
function sanitizeSpaces(raw) {
|
|
105
|
+
const out = {};
|
|
106
|
+
if (!raw || typeof raw !== "object")
|
|
107
|
+
return out;
|
|
108
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
109
|
+
if (!v || typeof v !== "object")
|
|
110
|
+
continue;
|
|
111
|
+
const o = v;
|
|
112
|
+
out[k] = {
|
|
113
|
+
...(o.responsibilities ? { responsibilities: cleanList(o.responsibilities) } : {}),
|
|
114
|
+
...(o.authority && typeof o.authority === "object" ? { authority: Object.fromEntries(Object.entries(o.authority).filter(([, m]) => m === "human_required" || m === "agent_can_decide")) } : {}),
|
|
115
|
+
...(o.decision_keywords ? { decision_keywords: cleanKeywords(o.decision_keywords) } : {}),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
export function spaceOverride(profile, ...keys) {
|
|
121
|
+
for (const k of keys)
|
|
122
|
+
if (k && profile.spaces?.[k])
|
|
123
|
+
return profile.spaces[k];
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
export function organizationProfile(profile, ...forSpace) {
|
|
127
|
+
const ov = spaceOverride(profile, ...forSpace);
|
|
99
128
|
return {
|
|
100
129
|
version: 1,
|
|
101
130
|
member: profile.member,
|
|
102
131
|
display_name: profile.human.display_name || profile.member,
|
|
103
132
|
title: profile.human.title,
|
|
104
|
-
responsibilities: cleanList(profile.human.responsibilities),
|
|
105
|
-
authority: Object.fromEntries(Object.entries(profile.authority)
|
|
133
|
+
responsibilities: cleanList(ov?.responsibilities ?? profile.human.responsibilities),
|
|
134
|
+
authority: Object.fromEntries(Object.entries(ov?.authority ?? profile.authority)
|
|
106
135
|
.map(([domain, mode]) => [normalize(domain), mode]).filter(([domain]) => !!domain)),
|
|
107
|
-
decision_keywords: cleanKeywords(profile.decision_keywords),
|
|
136
|
+
decision_keywords: cleanKeywords(ov?.decision_keywords ?? profile.decision_keywords),
|
|
108
137
|
capabilities: cleanList(profile.capabilities),
|
|
109
138
|
delegations: cleanDelegations(profile.delegations),
|
|
110
139
|
updated_at: profile.updated_at,
|
package/dist/protocol.js
CHANGED
|
@@ -96,6 +96,8 @@ export const Hello = z.object({
|
|
|
96
96
|
agent: z.string().max(80).optional(),
|
|
97
97
|
pkg: z.string().max(32).optional(),
|
|
98
98
|
from_seq: z.number().int().nonnegative().optional(),
|
|
99
|
+
repo: z.string().regex(/^[rl]_[0-9a-f]{16}$/).optional(),
|
|
100
|
+
repo_origin: z.string().max(200).optional(),
|
|
99
101
|
});
|
|
100
102
|
export const OrganizationProfileSchema = z.object({
|
|
101
103
|
version: z.literal(1),
|
|
@@ -151,6 +153,14 @@ export const SendAct = z.object({
|
|
|
151
153
|
defers_to: z.string().max(64).optional(),
|
|
152
154
|
affects: z.array(z.string().min(1).max(48)).max(16).optional(),
|
|
153
155
|
meta: z.record(z.unknown()).optional(),
|
|
156
|
+
repo: z.string().regex(/^[rl]_[0-9a-f]{16}$/).optional(),
|
|
157
|
+
});
|
|
158
|
+
export const BindRepo = z.object({
|
|
159
|
+
t: z.literal("bind_repo"),
|
|
160
|
+
cid: z.string().max(64).optional(),
|
|
161
|
+
repo: z.string().regex(/^[rl]_[0-9a-f]{16}$/),
|
|
162
|
+
origin: z.string().max(200).nullable().optional(),
|
|
163
|
+
base: z.string().min(1).max(100).optional(),
|
|
154
164
|
});
|
|
155
165
|
export const OpenGoal = z.object({
|
|
156
166
|
t: z.literal("open_goal"),
|
|
@@ -162,7 +172,18 @@ export const OpenGoal = z.object({
|
|
|
162
172
|
deadline: z.number().int().positive().optional(),
|
|
163
173
|
});
|
|
164
174
|
export const Ping = z.object({ t: z.literal("ping") });
|
|
165
|
-
export const
|
|
175
|
+
export const DeleteSpace = z.object({
|
|
176
|
+
t: z.literal("delete_space"),
|
|
177
|
+
cid: z.string().max(64).optional(),
|
|
178
|
+
confirm: z.string().min(1).max(64),
|
|
179
|
+
});
|
|
180
|
+
export const WorkerState = z.object({
|
|
181
|
+
t: z.literal("worker_state"),
|
|
182
|
+
state: z.enum(["idle", "holding", "running"]),
|
|
183
|
+
detail: z.string().max(200).optional(),
|
|
184
|
+
until: z.number().int().positive().optional(),
|
|
185
|
+
});
|
|
186
|
+
export const Inbound = z.discriminatedUnion("t", [Hello, SendAct, OpenGoal, Claim, Admit, MakeInvite, PublishProfile, BindRepo, DeleteSpace, WorkerState, Ping]);
|
|
166
187
|
export function shouldDeferToHuman(sessions, work, opts = {}) {
|
|
167
188
|
const now = opts.now ?? Date.now();
|
|
168
189
|
const idleMs = opts.idleMs ?? LIMITS.workerIdleMs;
|
package/dist/quickstart.js
CHANGED
|
@@ -3,7 +3,8 @@ import { randomBytes } from "node:crypto";
|
|
|
3
3
|
import { basename } from "node:path";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { PantheonClient } from "./client.js";
|
|
6
|
-
import { load, save, toInvite, DEFAULT_RELAY } from "./config.js";
|
|
6
|
+
import { load, save, toInvite, resolveFor, repoInfo, bindRepo, DEFAULT_RELAY } from "./config.js";
|
|
7
|
+
import { loadProfile, wakeDefault } from "./profile.js";
|
|
7
8
|
import { newKeypair } from "./identity.js";
|
|
8
9
|
import { installForUser } from "./install.js";
|
|
9
10
|
import { writeIntent, installHooks } from "./notify.js";
|
|
@@ -91,7 +92,7 @@ export function wireUp(opts, deps = {}) {
|
|
|
91
92
|
}
|
|
92
93
|
else {
|
|
93
94
|
try {
|
|
94
|
-
installSvc("worker", project, [
|
|
95
|
+
installSvc("worker", project, []);
|
|
95
96
|
tools.push({ tool: "worker", status: `on — answers peers while you are away (${project})` });
|
|
96
97
|
granted.push(`A peer you have granted may_wake can start an agent run on this machine, in an isolated worktree under ${project}.`);
|
|
97
98
|
if (!existsSync(policyPath())) {
|
|
@@ -131,7 +132,9 @@ export function findReach(port) {
|
|
|
131
132
|
}
|
|
132
133
|
export async function start(opts) {
|
|
133
134
|
const warnings = [];
|
|
134
|
-
const
|
|
135
|
+
const here = resolveFor(process.cwd());
|
|
136
|
+
const priorCfg = here.cfg;
|
|
137
|
+
const repo = repoInfo(process.cwd());
|
|
135
138
|
const signedInRelay = priorCfg?.account ? (priorCfg.relay ?? DEFAULT_RELAY) : undefined;
|
|
136
139
|
const chosen = opts.relay ?? signedInRelay;
|
|
137
140
|
const reach = chosen
|
|
@@ -154,7 +157,8 @@ export async function start(opts) {
|
|
|
154
157
|
}
|
|
155
158
|
const existing = priorCfg;
|
|
156
159
|
const hasOwnSpace = !!existing?.space && !!existing?.token;
|
|
157
|
-
const reuse = !!existing && !opts.reset && hasOwnSpace && (
|
|
160
|
+
const reuse = !!existing && !opts.reset && hasOwnSpace && (here.reason === "binding" || here.reason === "single" || here.reason === "env")
|
|
161
|
+
&& (!opts.space || opts.space === existing.space);
|
|
158
162
|
const keepIdentity = !!existing && !opts.reset && !!existing.publicKey && !!existing.privateKey;
|
|
159
163
|
let cfg = reuse
|
|
160
164
|
? { ...existing, relay: reach.url, agent: detectAgent() }
|
|
@@ -166,7 +170,10 @@ export async function start(opts) {
|
|
|
166
170
|
agent: detectAgent(),
|
|
167
171
|
...(keepIdentity ? { publicKey: existing.publicKey, privateKey: existing.privateKey } : newKeypair()),
|
|
168
172
|
...(keepIdentity && existing.account ? { account: existing.account } : {}),
|
|
173
|
+
...(repo ? { repo: repo.fingerprint, ...(repo.origin ? { repoOrigin: repo.origin } : {}) } : {}),
|
|
169
174
|
};
|
|
175
|
+
if (repo && !repo.shared)
|
|
176
|
+
warnings.push(`this repo has no origin remote, so nothing you deliver from it can be fetched by a teammate — add one (git remote add origin …) before delivering`);
|
|
170
177
|
if (reuse) {
|
|
171
178
|
opts.log(` you ${cfg.member}, same as before`);
|
|
172
179
|
if (opts.me && opts.me !== cfg.member) {
|
|
@@ -185,9 +192,19 @@ export async function start(opts) {
|
|
|
185
192
|
const client = new PantheonClient(c);
|
|
186
193
|
try {
|
|
187
194
|
await client.connect();
|
|
195
|
+
if (client.spaceId)
|
|
196
|
+
c.spaceId = client.spaceId;
|
|
188
197
|
save(c);
|
|
189
|
-
|
|
190
|
-
|
|
198
|
+
if (repo && !client.repos.some((r) => r.fingerprint === repo.fingerprint)) {
|
|
199
|
+
await client.bindRepo(repo.fingerprint, repo.origin).catch((e) => warnings.push(`could not register this repo with the team: ${e.message}`));
|
|
200
|
+
}
|
|
201
|
+
if (repo && c.spaceId) {
|
|
202
|
+
const { replaced } = bindRepo(repo, c.spaceId);
|
|
203
|
+
if (replaced)
|
|
204
|
+
warnings.push(`this repo was bound to another team (${replaced.space}); it now belongs to "${c.space}"`);
|
|
205
|
+
}
|
|
206
|
+
opts.log(` space ${c.space} ${reuse ? "— reconnected" : "— created, you own it"}${repo ? ` ${repo.origin ?? "local repo"} → bound to it` : ""}`);
|
|
207
|
+
const inv = await client.makeInvite(wakeDefault(loadProfile(c)), opts.ttlMinutes);
|
|
191
208
|
invite = toInvite({ ...c, token: inv.url });
|
|
192
209
|
claimed = true;
|
|
193
210
|
}
|
|
@@ -233,6 +250,7 @@ export async function start(opts) {
|
|
|
233
250
|
}
|
|
234
251
|
export async function join(opts) {
|
|
235
252
|
const existing = opts.reset ? null : load();
|
|
253
|
+
const repo = repoInfo(process.cwd());
|
|
236
254
|
const keys = existing?.publicKey && existing.privateKey
|
|
237
255
|
? { publicKey: existing.publicKey, privateKey: existing.privateKey }
|
|
238
256
|
: newKeypair();
|
|
@@ -247,13 +265,25 @@ export async function join(opts) {
|
|
|
247
265
|
agent: detectAgent(),
|
|
248
266
|
...keys,
|
|
249
267
|
...(existing?.account ? { account: existing.account } : {}),
|
|
268
|
+
...(repo ? { repo: repo.fingerprint, ...(repo.origin ? { repoOrigin: repo.origin } : {}) } : {}),
|
|
250
269
|
};
|
|
251
270
|
const client = new PantheonClient(cfg, undefined, { enroll: opts.invite.token });
|
|
252
271
|
await client.connect();
|
|
253
272
|
if (!client.enrolledToken)
|
|
254
273
|
throw new Error("the relay accepted us but sent no space token — version mismatch?");
|
|
255
274
|
cfg.token = client.enrolledToken;
|
|
275
|
+
if (client.spaceId)
|
|
276
|
+
cfg.spaceId = client.spaceId;
|
|
256
277
|
save(cfg);
|
|
278
|
+
const warnings = [];
|
|
279
|
+
if (repo && cfg.spaceId) {
|
|
280
|
+
const { replaced } = bindRepo(repo, cfg.spaceId);
|
|
281
|
+
if (replaced)
|
|
282
|
+
warnings.push(`this repo was bound to another team (${replaced.space}); it now belongs to "${cfg.space}"`);
|
|
283
|
+
opts.log(` repo ${repo.origin ?? "local repo"} → bound to "${cfg.space}"${client.repos.length > 1 ? ` (team repos: ${client.repos.map((r) => r.origin ?? r.fingerprint).join(", ")})` : ""}`);
|
|
284
|
+
if (!repo.shared)
|
|
285
|
+
warnings.push(`this repo has no origin remote, so nothing you deliver from it can be fetched by a teammate`);
|
|
286
|
+
}
|
|
257
287
|
const peers = client.roster.filter((m) => m.id !== cfg.member).map((m) => m.id);
|
|
258
288
|
opts.log(` joined "${cfg.space}" as ${cfg.member}${peers.length ? ` — with ${peers.join(", ")}` : ""}`);
|
|
259
289
|
client.close();
|
|
@@ -264,7 +294,7 @@ export async function join(opts) {
|
|
|
264
294
|
unattended: opts.unattended ?? null,
|
|
265
295
|
...(opts.workerHow ? { workerHow: opts.workerHow } : {}),
|
|
266
296
|
});
|
|
267
|
-
return { cfg, tools: wired.tools, granted: wired.granted, warnings: wired.warnings };
|
|
297
|
+
return { cfg, tools: wired.tools, granted: wired.granted, warnings: [...warnings, ...wired.warnings] };
|
|
268
298
|
}
|
|
269
299
|
function detectAgent() {
|
|
270
300
|
const found = detectAvailable();
|