@webinson/peer-core 0.1.975 → 0.1.976

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 CHANGED
@@ -16,4 +16,4 @@ peer core serve --detach # or let the editor start it on demand
16
16
  `peer core status` says who runs Peer here; `peer core stop` stops Peer Core. The Peer desktop app,
17
17
  when it starts, takes coordination over. Any other `peer <command>` goes to the running Peer.
18
18
 
19
- Version 0.1.975 of Peer Local (https://github.com/SemanS/peer-app, branch `peer`), based on T3 Code.
19
+ Version 0.1.976 of Peer Local (https://github.com/SemanS/peer-app, branch `peer`), based on T3 Code.
package/dist/bin.mjs CHANGED
@@ -77,7 +77,7 @@ if (isEntrypoint({
77
77
  } else {
78
78
  const { attachConsoleStdin } = await import("./consoleStdin-DO-vIhyg.mjs");
79
79
  attachConsoleStdin();
80
- const { runCli } = await import("./binCli-CwrAHPco.mjs");
80
+ const { runCli } = await import("./binCli-CO9uk2ZU.mjs");
81
81
  runCli(npmPeerBin ? "peer" : "t3");
82
82
  }
83
83
  }
@@ -27996,118 +27996,198 @@ async function findRepositories(home, limit = 25) {
27996
27996
  await Promise.all([...new Set(roots.filter((root) => root !== null))].map((root) => look(root, 2)));
27997
27997
  return [...found.entries()].sort((a, b) => b[1] - a[1]).slice(0, limit).map(([dir]) => dir);
27998
27998
  }
27999
+ const RECOMMENDED = "Recommended";
28000
+ /** `~/…` for a path under the home directory: the name matters, the full path is context. */
28001
+ const shortPath = (path, home) => path === home || path.startsWith(`${home}${NodePath.sep}`) ? `~${path.slice(home.length)}` : path;
28002
+ /** The lines above the menu: who, which workspace, and the project here. */
28003
+ const menuContext = (status, here, home) => {
28004
+ const names = status.workspaces.map((w) => w.slug);
28005
+ const lines = [`Signed in as ${status.email ?? "nobody"}`, names.length === 0 ? "Workspace: none yet" : names.length === 1 ? `Workspace: ${names[0]}` : `Workspaces: ${names.join(", ")}`];
28006
+ if (here !== null) lines.push(`Current project: ${NodePath.basename(here.root)} (${shortPath(here.root, home)}) · ${here.state === "connected" ? `connected to ${here.workspace}/${here.project}` : here.state === "clone" ? `a clone of ${here.project}, not connected` : "not connected"}`);
28007
+ return lines;
28008
+ };
27999
28009
  /**
28000
- * What the menu offers now: what waits for the person first (invitations, foundings, a domain
28001
- * match), then sharing `folder` when it is a repository no workspace has, inviting where they may,
28002
- * creating and joining, an editor, and done.
28010
+ * What the menu offers, the step that gets the person to a shared project first and marked
28011
+ * recommended: an invitation that waits, else a workspace when there is none, else connecting
28012
+ * the project here, else inviting a teammate to it. Inviting is offered where they own or
28013
+ * administer a workspace; joining or creating one stays reachable, Exit last.
28003
28014
  */
28004
- const menuItems = (status, folder) => [
28005
- ...(status.foundings ?? []).filter((f) => f.expired !== true).map((f) => ({
28006
- title: `Accept ${f.name} (${f.slug}), founded for you`,
28015
+ const menuItems = (status, here) => {
28016
+ const offers = [...(status.foundings ?? []).filter((f) => f.expired !== true).map((f) => ({
28017
+ title: `Accept ${f.name}, founded for you`,
28007
28018
  value: {
28008
28019
  kind: "founding",
28009
28020
  slug: f.slug,
28010
28021
  name: f.name
28011
28022
  }
28012
- })),
28013
- ...status.joinable.map((offer) => ({
28014
- title: offer.reason === "invite" ? `Accept the invitation to ${offer.name} (${offer.slug})` : `Join ${offer.name} (${offer.slug}), which your email domain admits`,
28023
+ })), ...status.joinable.map((offer) => ({
28024
+ title: offer.reason === "invite" ? `Accept the invitation to ${offer.name}` : `Join ${offer.name} (your email domain is admitted)`,
28015
28025
  value: {
28016
28026
  kind: offer.reason === "invite" ? "accept" : "domain",
28017
28027
  slug: offer.slug,
28018
28028
  name: offer.name
28019
28029
  }
28020
- })),
28021
- ...folder === null || status.workspaces.length === 0 ? [] : [{
28022
- title: `Share ${folder} with a workspace`,
28023
- value: {
28024
- kind: "share",
28025
- folder
28026
- }
28027
- }],
28028
- ...status.workspaces.length === 0 ? [] : [{
28029
- title: "Share a project from this computer…",
28030
- value: { kind: "browse" }
28031
- }],
28032
- ...status.workspaces.filter((w) => inviter(w.role)).map((w) => ({
28033
- title: `Invite someone to ${w.name} (${w.slug})`,
28030
+ }))];
28031
+ const inWorkspace = status.workspaces.length > 0;
28032
+ const canInvite = status.workspaces.some((w) => inviter(w.role));
28033
+ const name = here === null ? "" : NodePath.basename(here.root);
28034
+ const target = status.workspaces.length === 1 ? status.workspaces[0].slug : "the workspace you choose";
28035
+ const connectHere = !inWorkspace || here === null || here.state === "connected" ? [] : [{
28036
+ title: `Connect this project (${name})`,
28037
+ description: here.state === "clone" ? `It is a clone of ${here.project}: it joins that project, nothing new is created.` : `Your team in ${target} sees it and can clone it; the code stays in its repository.`,
28034
28038
  value: {
28035
- kind: "invite",
28036
- slug: w.slug
28039
+ kind: "here",
28040
+ folder: here.root
28037
28041
  }
28038
- })),
28039
- {
28040
- title: "Create a workspace",
28041
- value: { kind: "create" }
28042
- },
28043
- {
28044
- title: "Join a workspace by its name",
28045
- value: { kind: "join" }
28046
- },
28047
- {
28042
+ }];
28043
+ const invite = canInvite ? [{
28044
+ title: "Invite a teammate",
28045
+ description: "They get an email with a link; you choose the projects.",
28046
+ value: { kind: "invite" }
28047
+ }] : [];
28048
+ const items = inWorkspace ? [
28049
+ ...offers,
28050
+ ...here?.state === "connected" ? [...invite, ...connectHere] : [...connectHere, ...invite],
28051
+ {
28052
+ title: "Connect another local project",
28053
+ description: "Pick one of your repositories, or browse for a folder.",
28054
+ value: { kind: "another" }
28055
+ },
28056
+ {
28057
+ title: "Join or create a workspace",
28058
+ value: { kind: "workspace" }
28059
+ }
28060
+ ] : [
28061
+ ...offers,
28062
+ {
28063
+ title: "Create a workspace",
28064
+ description: "A place for your team's projects; you become its owner.",
28065
+ value: { kind: "create" }
28066
+ },
28067
+ {
28068
+ title: "Join a workspace",
28069
+ description: "By the name in your invitation.",
28070
+ value: { kind: "join" }
28071
+ }
28072
+ ];
28073
+ items.push({
28048
28074
  title: "Connect an editor",
28075
+ description: "So your agents (Claude Code, Codex, Cursor, …) coordinate through Peer.",
28049
28076
  value: { kind: "editor" }
28050
- },
28051
- {
28052
- title: "Done",
28053
- value: { kind: "done" }
28054
- }
28055
- ];
28077
+ }, {
28078
+ title: "Exit",
28079
+ value: { kind: "exit" }
28080
+ });
28081
+ const [first, ...rest] = items;
28082
+ return first === void 0 || first.value.kind === "exit" || first.value.kind === "editor" ? items : [{
28083
+ ...first,
28084
+ description: first.description === void 0 ? RECOMMENDED : `${RECOMMENDED} · ${first.description}`
28085
+ }, ...rest];
28086
+ };
28056
28087
  /**
28057
- * Asks what to do next until the person is done; a step that fails says why and the menu asks
28058
- * again, so one refusal does not end it.
28088
+ * Asks what to do next until the person exits; a step that fails says why and the menu asks
28089
+ * again, so one refusal does not end it. A connected project ends in what is ready for the team.
28059
28090
  */
28060
28091
  const menu = (ask, steps, io) => gen$1(function* () {
28092
+ let shown = "";
28093
+ const createStep = gen$1(function* () {
28094
+ const name = yield* ask.text("The workspace's name");
28095
+ if (name === null || name.trim() === "") return;
28096
+ yield* steps.create(name);
28097
+ shown = "";
28098
+ });
28099
+ const joinStep = gen$1(function* () {
28100
+ const slug = yield* ask.text("The workspace's short name (as your invitation names it)");
28101
+ if (slug === null || slug.trim() === "") return;
28102
+ yield* steps.join(slug.trim());
28103
+ shown = "";
28104
+ });
28061
28105
  for (;;) {
28062
28106
  const status = yield* steps.status;
28063
- const item = yield* ask.choose("What next?", menuItems(status, status.workspaces.length === 0 ? null : yield* steps.shareable));
28064
- if (item === null || item.kind === "done") return;
28107
+ const here = yield* steps.here;
28108
+ const context = menuContext(status, here, steps.home).join("\n");
28109
+ if (context !== shown) {
28110
+ yield* io.say(context);
28111
+ shown = context;
28112
+ }
28113
+ const item = yield* ask.choose("What would you like to do?", menuItems(status, here));
28114
+ if (item === null || item.kind === "exit") return;
28115
+ const connect = (folder) => gen$1(function* () {
28116
+ const joined = yield* steps.share(folder);
28117
+ if (joined === null) return;
28118
+ const ready = yield* steps.agentsReady;
28119
+ yield* io.say([
28120
+ "",
28121
+ `✓ Signed in as ${status.email ?? ""}`,
28122
+ `✓ Project "${NodePath.basename(folder)}" connected to ${joined.workspace} (${joined.project})`,
28123
+ ready ? "✓ Agent integration ready" : "· Agent integration not set up yet: choose Connect an editor",
28124
+ "",
28125
+ "Your project is ready for collaboration."
28126
+ ].join("\n"));
28127
+ shown = "";
28128
+ });
28065
28129
  yield* gen$1(function* () {
28066
28130
  switch (item.kind) {
28067
28131
  case "accept":
28068
28132
  case "domain": return yield* steps.join(item.slug);
28069
28133
  case "founding": return yield* steps.founding(item.slug);
28070
- case "share": return yield* steps.share(item.folder);
28071
- case "browse": {
28134
+ case "here": return yield* connect(item.folder);
28135
+ case "another": {
28072
28136
  const found = yield* steps.repositories;
28073
28137
  const picked = yield* ask.choose(found.length === 0 ? "No repositories found in the usual folders" : "Which project?", [
28074
28138
  ...found.map((dir) => ({
28075
- title: dir,
28139
+ title: NodePath.basename(dir),
28140
+ description: shortPath(dir, steps.home),
28076
28141
  value: dir
28077
28142
  })),
28078
28143
  {
28079
28144
  title: "Browse folders…",
28080
- value: "browse"
28145
+ value: "\0browse"
28081
28146
  },
28082
28147
  {
28083
28148
  title: "Type a path…",
28084
- value: "type"
28149
+ value: "\0type"
28085
28150
  }
28086
28151
  ]);
28087
- const folder = picked === "browse" ? yield* ask.folder("The project's folder (Enter, then y opens a folder, n chooses it)") : picked === "type" ? yield* ask.text("The project's folder (a path)") : picked;
28088
- if (folder !== null && folder.trim() !== "") yield* steps.share(folder.trim());
28152
+ const folder = picked === "\0browse" ? yield* ask.folder("The project's folder (Enter, then y opens a folder, n chooses it)") : picked === "\0type" ? yield* ask.text("The project's folder (a path)") : picked;
28153
+ if (folder !== null && folder.trim() !== "") yield* connect(folder.trim());
28089
28154
  return;
28090
28155
  }
28091
- case "editor": return yield* steps.editor;
28092
- case "create": {
28093
- const name = yield* ask.text("The workspace's name");
28094
- if (name !== null && name.trim() !== "") yield* steps.create(name);
28156
+ case "editor":
28157
+ yield* steps.editor;
28158
+ shown = "";
28095
28159
  return;
28096
- }
28097
- case "join": {
28098
- const slug = yield* ask.text("The workspace's short name (as its invitation names it)");
28099
- if (slug !== null && slug.trim() !== "") yield* steps.join(slug.trim());
28160
+ case "workspace": {
28161
+ const next = yield* ask.choose("Join or create", [{
28162
+ title: "Join a workspace",
28163
+ description: "By the name in your invitation.",
28164
+ value: "join"
28165
+ }, {
28166
+ title: "Create a workspace",
28167
+ description: "You become its owner.",
28168
+ value: "create"
28169
+ }]);
28170
+ if (next === "create") return yield* createStep;
28171
+ if (next === "join") return yield* joinStep;
28100
28172
  return;
28101
28173
  }
28174
+ case "create": return yield* createStep;
28175
+ case "join": return yield* joinStep;
28102
28176
  case "invite": {
28103
- const email = yield* ask.text(`Whom to invite to ${item.slug} (their email)`);
28177
+ const slugs = status.workspaces.filter((w) => inviter(w.role)).map((w) => w.slug);
28178
+ const slug = slugs.length === 1 ? slugs[0] : yield* ask.choose("Invite to which workspace?", slugs.map((s) => ({
28179
+ title: s,
28180
+ value: s
28181
+ })));
28182
+ if (slug === null) return;
28183
+ const email = yield* ask.text(`Their email (invited to ${slug})`);
28104
28184
  if (email === null || email.trim() === "") return;
28105
- const projects = status.workspaces.find((w) => w.slug === item.slug)?.projects.map((p) => p.project.id) ?? [];
28106
- const chosen = projects.length === 0 ? [] : yield* ask.pick("Which projects (none chosen: all of them, now and later)", projects);
28185
+ const projects = status.workspaces.find((w) => w.slug === slug)?.projects.map((p) => p.project.id) ?? [];
28186
+ const chosen = projects.length === 0 ? [] : yield* ask.pick("Which projects (none chosen: all, now and later)", projects);
28107
28187
  if (chosen === null) return;
28108
28188
  return yield* steps.invite({
28109
28189
  email,
28110
- workspace: item.slug,
28190
+ workspace: slug,
28111
28191
  projects: chosen
28112
28192
  });
28113
28193
  }
@@ -28155,7 +28235,7 @@ const share = (input, ask, io, register) => gen$1(function* () {
28155
28235
  }
28156
28236
  const place = yield* state.placeOf(folder, { bindByRemote: false });
28157
28237
  if (place !== null && place.workspace !== "@local") {
28158
- yield* io.say(`Shared already: ${folder} is project ${place.project} of ${place.workspace}.`);
28238
+ yield* io.say(`Connected already: ${folder} is project ${place.project} of ${place.workspace}.`);
28159
28239
  return;
28160
28240
  }
28161
28241
  const { persisted } = yield* state.read;
@@ -28176,7 +28256,7 @@ const share = (input, ask, io, register) => gen$1(function* () {
28176
28256
  const what = `${folder} is a clone of ${offered.map(title).join(", ")}`;
28177
28257
  if (asking) {
28178
28258
  yield* io.say(`${what}.`);
28179
- choice = yield* ask.choose("Use it as", [...offered.map((match) => ({
28259
+ choice = yield* ask.choose("Connect it as", [...offered.map((match) => ({
28180
28260
  title: `the checkout of ${title(match)}`,
28181
28261
  value: {
28182
28262
  kind: "bind",
@@ -28219,8 +28299,9 @@ const share = (input, ask, io, register) => gen$1(function* () {
28219
28299
  return yield* bindTo(projectOf$1(match), match.repository);
28220
28300
  }
28221
28301
  const name = input.name?.trim() || NodePath.basename(folder);
28302
+ const shown = shortPath(folder, realpathOrSelf$2(NodeOS.homedir()));
28222
28303
  if (asking) {
28223
- if (!(yield* ask.confirm(`Share ${folder} with ${choice.workspace} as project "${name}"? Everyone in ${choice.workspace} sees it and can clone it.`, true))) {
28304
+ if (!(yield* ask.confirm(`Connect ${shown} to ${choice.workspace} as project "${name}"? Everyone in ${choice.workspace} sees it and can clone it; the code stays in its repository.`, true))) {
28224
28305
  yield* io.say("Nothing changed.");
28225
28306
  return;
28226
28307
  }
@@ -28232,7 +28313,7 @@ const share = (input, ask, io, register) => gen$1(function* () {
28232
28313
  ...input.name?.trim() ? { name: input.name.trim() } : {}
28233
28314
  });
28234
28315
  const shared = yield* state.placeOf(folder, { bindByRemote: false });
28235
- yield* io.say(shared === null ? `Shared ${folder} with ${choice.workspace}.` : `Shared ${folder} with ${choice.workspace} as project ${shared.project}.`);
28316
+ yield* io.say(shared === null ? `Connected ${shown} to ${choice.workspace}.` : `Connected ${shown} to ${choice.workspace} as project ${shared.project}.`);
28236
28317
  });
28237
28318
  //#endregion
28238
28319
  //#region src/cli/peerCoreTasks.ts
@@ -28608,12 +28689,34 @@ const runMenu = (stateDir) => gen$1(function* () {
28608
28689
  const status = workspaces.sync.pipe(hubFailure);
28609
28690
  yield* menu(menuAsk, {
28610
28691
  status,
28611
- shareable: gen$1(function* () {
28692
+ home: NodeOS.homedir(),
28693
+ here: gen$1(function* () {
28612
28694
  const checkout = yield* repositoryRoot(process.cwd());
28613
28695
  if (checkout === null) return null;
28614
28696
  const place = yield* state.placeOf(checkout.root, { bindByRemote: false });
28615
- return place === null || place.workspace === "@local" ? checkout.root : null;
28697
+ if (place !== null && place.workspace !== "@local") return {
28698
+ root: checkout.root,
28699
+ state: "connected",
28700
+ workspace: place.workspace,
28701
+ project: place.project
28702
+ };
28703
+ const remote = yield* promise(() => remoteOf(checkout.remoteRoot));
28704
+ const { persisted } = yield* state.read;
28705
+ const match = remote === null ? void 0 : matchRepositories(remote, persisted.workspaces.map((w) => w.manifest))[0];
28706
+ return match === void 0 ? {
28707
+ root: checkout.root,
28708
+ state: "new"
28709
+ } : {
28710
+ root: checkout.root,
28711
+ state: "clone",
28712
+ project: `${match.workspace}/${match.project}`
28713
+ };
28616
28714
  }),
28715
+ agentsReady: gen$1(function* () {
28716
+ const coordination = (yield* status).coordination;
28717
+ const mcp = yield* (yield* McpClients).connected;
28718
+ return coordination.claudeHooks || (coordination.codexHookHomes ?? []).some((home) => home.installed && home.trusted) || mcp.length > 0;
28719
+ }).pipe(orElseSucceed(() => false)),
28617
28720
  join: (slug) => join({ target: slug }, terminalIo).pipe(flatMap$1((joined) => sayNextStep(joined, terminalIo)), hubFailure),
28618
28721
  founding: (slug) => workspaces.acceptFounding({ slug }).pipe(andThen(log(`Accepted: ${slug}. You are its owner.`)), hubFailure),
28619
28722
  create: (name) => create({ name }, terminalIo).pipe(asVoid, hubFailure),
@@ -28630,7 +28733,10 @@ const runMenu = (stateDir) => gen$1(function* () {
28630
28733
  folder,
28631
28734
  named: true,
28632
28735
  yes: false
28633
- }, shareAsk, terminalIo, registerIn(stateDir))), hubFailure),
28736
+ }, shareAsk, terminalIo, registerIn(stateDir)).pipe(andThen(state.placeOf(folder, { bindByRemote: false })), map$3((place) => place === null || place.workspace === "@local" ? null : {
28737
+ workspace: place.workspace,
28738
+ project: place.project
28739
+ }))), hubFailure),
28634
28740
  invite: (input) => status.pipe(flatMap$1((now) => invite(input, now, terminalIo)), asVoid, hubFailure),
28635
28741
  editor: gen$1(function* () {
28636
28742
  const editor = yield* shareAsk.choose("Connect which editor?", editorChoices(yield* detectedEditors));
@@ -28656,7 +28762,6 @@ const loginCommand = make$62("login", {
28656
28762
  if (email === void 0 || signedIn !== null && signedIn === email) {
28657
28763
  if (signedIn === null) return yield* fail("peer core: nobody is signed in; run peer core login <email>.");
28658
28764
  if (!isTerminal()) return yield* log(`Signed in: ${signedIn}`);
28659
- yield* log(`Signed in: ${signedIn}`);
28660
28765
  return yield* runMenu(config.stateDir);
28661
28766
  }
28662
28767
  yield* login({
@@ -55267,4 +55372,4 @@ function runCli(root = "t3") {
55267
55372
  //#endregion
55268
55373
  export { runCli };
55269
55374
 
55270
- //# sourceMappingURL=binCli-CwrAHPco.mjs.map
55375
+ //# sourceMappingURL=binCli-CO9uk2ZU.mjs.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webinson/peer-core",
3
- "version": "0.1.975",
3
+ "version": "0.1.976",
4
4
  "description": "Peer's coordination for coding agents without the Peer app: the `peer` command (Peer Core).",
5
5
  "license": "MIT",
6
6
  "repository": {