yourskills 0.2.0 → 0.3.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.
Files changed (2) hide show
  1. package/dist/main.js +182 -58
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -611,6 +611,7 @@ async function localAccount(server) {
611
611
  return {
612
612
  signedIn: Boolean(config.token),
613
613
  email: config.userEmail ?? null,
614
+ organization: config.orgName ?? null,
614
615
  server: serverUrl(config, server)
615
616
  };
616
617
  }
@@ -5036,6 +5037,8 @@ function report2(manifest, answer) {
5036
5037
  else if (entry.state === "unknown") {
5037
5038
  unknown.push({
5038
5039
  name: asked[at]?.name ?? entry.externalId,
5040
+ reason: entry.reason,
5041
+ ..."repo" in entry ? { repo: entry.repo } : {},
5039
5042
  message: entry.message
5040
5043
  });
5041
5044
  }
@@ -5054,6 +5057,19 @@ async function driftNow(client, nearest) {
5054
5057
  });
5055
5058
  return report2(nearest.manifest, answer);
5056
5059
  }
5060
+ function markFor(reason) {
5061
+ return reason === "foreign" ? "fail" : "pending";
5062
+ }
5063
+ function nextFor(drift) {
5064
+ if (drift.moved.length > 0)
5065
+ return { command: "yourskills update --all", why: "install what moved" };
5066
+ if (drift.unknown.some((row) => row.reason === "orphaned"))
5067
+ return {
5068
+ command: "yourskills doctor",
5069
+ why: "see what is no longer watched"
5070
+ };
5071
+ return;
5072
+ }
5057
5073
  async function outdatedReport(client, nearest, quiet = false) {
5058
5074
  let drift;
5059
5075
  try {
@@ -5075,7 +5091,11 @@ async function outdatedReport(client, nearest, quiet = false) {
5075
5091
  note: [row.bundle ?? "-", lifecycleTag(row.lifecycle)].filter(Boolean).join(" ")
5076
5092
  }));
5077
5093
  for (const row of drift.unknown) {
5078
- lines.push({ mark: "fail", text: row.name, note: row.message });
5094
+ lines.push({
5095
+ mark: markFor(row.reason),
5096
+ text: row.name,
5097
+ note: row.message
5098
+ });
5079
5099
  }
5080
5100
  for (const name of drift.unpinned) {
5081
5101
  lines.push({
@@ -5085,6 +5105,7 @@ async function outdatedReport(client, nearest, quiet = false) {
5085
5105
  });
5086
5106
  }
5087
5107
  const current = drift.moved.length === 0 && drift.unknown.length === 0;
5108
+ const next = nextFor(drift);
5088
5109
  if (current && !quiet) {
5089
5110
  lines.push({
5090
5111
  mark: "ok",
@@ -5101,12 +5122,7 @@ async function outdatedReport(client, nearest, quiet = false) {
5101
5122
  unpinned: drift.unpinned
5102
5123
  },
5103
5124
  silent: quiet && current,
5104
- ...drift.moved.length > 0 ? {
5105
- next: {
5106
- command: "yourskills update --all",
5107
- why: "install what moved"
5108
- }
5109
- } : {}
5125
+ ...next ? { next } : {}
5110
5126
  };
5111
5127
  }
5112
5128
  // src/hooks/emit.ts
@@ -5738,7 +5754,7 @@ import { realpathSync } from "node:fs";
5738
5754
  // package.json
5739
5755
  var package_default = {
5740
5756
  name: "yourskills",
5741
- version: "0.2.0",
5757
+ version: "0.3.1",
5742
5758
  description: "Browse, install and keep up to date the agent skills and bundles your team publishes on yourskills.",
5743
5759
  license: "UNLICENSED",
5744
5760
  homepage: "https://yourskills.store",
@@ -6183,8 +6199,41 @@ async function runSessionStart() {
6183
6199
  const outcome = await sessionStart();
6184
6200
  emit(outcome, await upgradeCheck());
6185
6201
  }
6186
- // src/org/org.ts
6202
+ // src/org/wire.ts
6187
6203
  var authBase2 = (base) => `${base.replace(/\/$/, "")}/api/auth`;
6204
+ async function activeOrganizationId(base, token) {
6205
+ const res = await fetch(`${authBase2(base)}/get-session`, {
6206
+ headers: { authorization: `Bearer ${token}` }
6207
+ });
6208
+ if (!res.ok)
6209
+ return null;
6210
+ const body = await res.json();
6211
+ return body?.session?.activeOrganizationId ?? null;
6212
+ }
6213
+ async function listOrganizations(base, token) {
6214
+ let res;
6215
+ try {
6216
+ res = await fetch(`${authBase2(base)}/organization/list`, {
6217
+ headers: { authorization: `Bearer ${token}` }
6218
+ });
6219
+ } catch (error) {
6220
+ throw new Error(`Could not reach ${base}: ${error instanceof Error ? error.message : String(error)}`);
6221
+ }
6222
+ if (!res.ok) {
6223
+ throw new Error(`Could not list organizations: ${res.status} ${res.statusText}`);
6224
+ }
6225
+ return await res.json();
6226
+ }
6227
+ async function rememberOrganization(name) {
6228
+ try {
6229
+ const config = await readConfig();
6230
+ if ((config.orgName ?? null) === name)
6231
+ return;
6232
+ await writeConfig(name === null ? { ...config, orgName: undefined } : { ...config, orgName: name });
6233
+ } catch {}
6234
+ }
6235
+
6236
+ // src/org/org.ts
6188
6237
  var NO_ACTIVE_ORGANIZATION = "none: choose one with `yourskills org <name>`, or in the web app";
6189
6238
  async function namedOrganization(server) {
6190
6239
  const client = await createClient(server);
@@ -6193,11 +6242,9 @@ async function namedOrganization(server) {
6193
6242
  }
6194
6243
  async function whoamiReport(server) {
6195
6244
  const who = await whoami(server);
6196
- return drawWhoami({
6197
- user: who.user,
6198
- server: who.server,
6199
- organization: who.org ? await namedOrganization(server) : null
6200
- });
6245
+ const organization = who.org ? await namedOrganization(server) : null;
6246
+ await rememberOrganization(organization?.name ?? null);
6247
+ return drawWhoami({ user: who.user, server: who.server, organization });
6201
6248
  }
6202
6249
  function drawWhoami({ user, server, organization }) {
6203
6250
  const label = (text) => text.padEnd(7);
@@ -6215,29 +6262,6 @@ function drawWhoami({ user, server, organization }) {
6215
6262
  json: { user, server, organization }
6216
6263
  };
6217
6264
  }
6218
- async function activeOrganizationId(base, token) {
6219
- const res = await fetch(`${authBase2(base)}/get-session`, {
6220
- headers: { authorization: `Bearer ${token}` }
6221
- });
6222
- if (!res.ok)
6223
- return null;
6224
- const body = await res.json();
6225
- return body?.session?.activeOrganizationId ?? null;
6226
- }
6227
- async function listOrganizations(base, token) {
6228
- let res;
6229
- try {
6230
- res = await fetch(`${authBase2(base)}/organization/list`, {
6231
- headers: { authorization: `Bearer ${token}` }
6232
- });
6233
- } catch (error) {
6234
- throw new Error(`Could not reach ${base}: ${error instanceof Error ? error.message : String(error)}`);
6235
- }
6236
- if (!res.ok) {
6237
- throw new Error(`Could not list organizations: ${res.status} ${res.statusText}`);
6238
- }
6239
- return await res.json();
6240
- }
6241
6265
  function rows(orgs, active) {
6242
6266
  if (orgs.length === 0) {
6243
6267
  return [
@@ -6254,12 +6278,40 @@ function rows(orgs, active) {
6254
6278
  note: org.slug ?? org.id
6255
6279
  }));
6256
6280
  }
6281
+ async function learnActiveOrganization(server) {
6282
+ try {
6283
+ const { token, base } = await requireToken(server);
6284
+ const [orgs, active] = await Promise.all([
6285
+ listOrganizations(base, token),
6286
+ activeOrganizationId(base, token)
6287
+ ]);
6288
+ const name = orgs.find((o) => o.id === active)?.name ?? null;
6289
+ await rememberOrganization(name);
6290
+ return name;
6291
+ } catch {
6292
+ return null;
6293
+ }
6294
+ }
6295
+ async function organizationChoices(server) {
6296
+ const { token, base } = await requireToken(server);
6297
+ const [orgs, active] = await Promise.all([
6298
+ listOrganizations(base, token),
6299
+ activeOrganizationId(base, token)
6300
+ ]);
6301
+ await rememberOrganization(orgs.find((o) => o.id === active)?.name ?? null);
6302
+ return orgs.map((org) => ({
6303
+ name: org.name,
6304
+ slug: org.slug ?? null,
6305
+ active: org.id === active
6306
+ }));
6307
+ }
6257
6308
  async function orgListReport(server) {
6258
6309
  const { token, base } = await requireToken(server);
6259
6310
  const [orgs, active] = await Promise.all([
6260
6311
  listOrganizations(base, token),
6261
6312
  activeOrganizationId(base, token)
6262
6313
  ]);
6314
+ await rememberOrganization(orgs.find((o) => o.id === active)?.name ?? null);
6263
6315
  return {
6264
6316
  title: "org",
6265
6317
  lines: rows(orgs, active),
@@ -6316,6 +6368,7 @@ async function orgSwitchReport(name, server) {
6316
6368
  failed: true
6317
6369
  };
6318
6370
  }
6371
+ await rememberOrganization(match.name);
6319
6372
  return {
6320
6373
  title: "org",
6321
6374
  lines: [{ mark: "ok", text: `Active organization: ${match.name}` }],
@@ -6558,6 +6611,37 @@ async function downstreamChecks(ask, server) {
6558
6611
  }))
6559
6612
  ];
6560
6613
  }
6614
+ var liveDrift = async (nearest, server) => driftNow(await createClient(server), nearest);
6615
+ async function unwatchedCheck(nearest, ask, server) {
6616
+ const name = "unwatched sources";
6617
+ let drift;
6618
+ try {
6619
+ drift = await ask(nearest, server);
6620
+ } catch (error) {
6621
+ return {
6622
+ name,
6623
+ ok: false,
6624
+ detail: `could not ask the registry: ${error instanceof Error ? error.message : String(error)}`
6625
+ };
6626
+ }
6627
+ const orphans = drift.unknown.filter((row) => row.reason === "orphaned");
6628
+ if (orphans.length === 0)
6629
+ return {
6630
+ name,
6631
+ ok: true,
6632
+ detail: "every installed skill came from a source this organization still connects"
6633
+ };
6634
+ const names = orphans.map((row) => row.name);
6635
+ const repos = [
6636
+ ...new Set(orphans.map((row) => row.repo).filter((repo) => repo !== undefined))
6637
+ ].sort((a, b) => a.localeCompare(b));
6638
+ const one = names.length === 1;
6639
+ return {
6640
+ name,
6641
+ ok: true,
6642
+ detail: `${names.join(", ")}: installed from ${repos.join(", ")}, which this ` + "organization does not connect any more, so nothing is watching " + `${repos.length === 1 ? "it" : "them"} for changes. The ` + `${one ? "copy on this machine goes" : "copies on this machine go"} on ` + "working. A team member can reconnect the source or vendor the skill into " + "your catalog, and then `yourskills update --all` moves this machine onto it."
6643
+ };
6644
+ }
6561
6645
  function version(config) {
6562
6646
  const latest = config.latestVersion;
6563
6647
  if (latest && isNewer(latest, VERSION))
@@ -6577,7 +6661,7 @@ async function diagnose(nearest, options = {}) {
6577
6661
  const account = await session(options.askOrganization ?? namedOrganization, options.server);
6578
6662
  checks.push(await reachable(base), ...account);
6579
6663
  if (account[0].ok) {
6580
- checks.push(...await downstreamChecks(options.ask ?? liveDiagnosis, options.server));
6664
+ checks.push(...await downstreamChecks(options.ask ?? liveDiagnosis, options.server), await unwatchedCheck(nearest, options.askDrift ?? liveDrift, options.server));
6581
6665
  }
6582
6666
  }
6583
6667
  checks.push(skillsResolvable(), await agents(nearest, local));
@@ -6614,7 +6698,7 @@ async function applyFixes(checks) {
6614
6698
  }
6615
6699
  return out;
6616
6700
  }
6617
- function nextFor(checks, fixing) {
6701
+ function nextFor2(checks, fixing) {
6618
6702
  if (!fixing && fixable(checks).length > 0)
6619
6703
  return {
6620
6704
  command: "yourskills doctor --fix",
@@ -6636,7 +6720,7 @@ function doctorLines(checks, fixing = false) {
6636
6720
  note
6637
6721
  };
6638
6722
  });
6639
- const next = nextFor(checks, fixing);
6723
+ const next = nextFor2(checks, fixing);
6640
6724
  return {
6641
6725
  title: fixing ? "doctor --fix" : "doctor",
6642
6726
  lines,
@@ -7049,7 +7133,7 @@ var TABLE = {
7049
7133
  label: "switch org",
7050
7134
  hint: "org <name>",
7051
7135
  when: (facts) => facts.signedIn,
7052
- opens: { kind: "org" }
7136
+ says: (facts) => facts.organization ? `switch this machine out of ${facts.organization}` : "choose which organization this machine installs from"
7053
7137
  }
7054
7138
  ],
7055
7139
  invitesHooks: false,
@@ -8010,18 +8094,21 @@ function paletteGroups(facts) {
8010
8094
  // src/shell/facts.ts
8011
8095
  import { existsSync as existsSync7 } from "node:fs";
8012
8096
  async function paletteFacts(scope, server) {
8013
- const [account, nearest, config] = await Promise.all([
8097
+ const [account, nearest, config, agents] = await Promise.all([
8014
8098
  localAccount(server),
8015
8099
  nearestManifest(scope),
8016
- readConfig()
8100
+ readConfig(),
8101
+ resolveAgents()
8017
8102
  ]);
8018
8103
  const skills = Object.keys(nearest.manifest.skills).length;
8019
8104
  return {
8020
8105
  signedIn: account.signedIn,
8021
8106
  email: account.email,
8107
+ organization: account.organization,
8022
8108
  server: account.server,
8023
8109
  firstRun: !account.signedIn && skills === 0 && !config.welcomed,
8024
8110
  agentsOnDisk: HOOK_ADAPTERS.filter((adapter) => existsSync7(adapter.home())).map((adapter) => adapter.agent),
8111
+ agents,
8025
8112
  scope: nearest.scope,
8026
8113
  manifest: nearest.path,
8027
8114
  skills,
@@ -8360,6 +8447,8 @@ function PickScreen({
8360
8447
  rows,
8361
8448
  empty,
8362
8449
  verb,
8450
+ single = false,
8451
+ marked,
8363
8452
  onPick,
8364
8453
  onBack
8365
8454
  }) {
@@ -8393,7 +8482,7 @@ function PickScreen({
8393
8482
  } else if (key.backspace || key.delete) {
8394
8483
  setQuery((q) => q.slice(0, -1));
8395
8484
  setCursor(0);
8396
- } else if (input === " ") {
8485
+ } else if (input === " " && !single) {
8397
8486
  const row = visible[cursor];
8398
8487
  if (!row)
8399
8488
  return;
@@ -8444,21 +8533,21 @@ function PickScreen({
8444
8533
  names,
8445
8534
  selectable: true,
8446
8535
  cursor: i === cursor,
8447
- selected: picked.has(row.name)
8536
+ selected: single ? row.name === marked : picked.has(row.name)
8448
8537
  }, row.name)),
8449
8538
  /* @__PURE__ */ jsx16(Divider, {
8450
8539
  width
8451
8540
  }),
8452
8541
  /* @__PURE__ */ jsxs14(Box12, {
8453
8542
  children: [
8454
- picked.size > 0 ? /* @__PURE__ */ jsx16(Text14, {
8543
+ picked.size > 0 && !single ? /* @__PURE__ */ jsx16(Text14, {
8455
8544
  color: tone.notice,
8456
8545
  children: `${mark.selected} ${picked.size} marked `
8457
8546
  }) : null,
8458
8547
  /* @__PURE__ */ jsx16(Hints, {
8459
8548
  hints: [
8460
8549
  ["^v", "move"],
8461
- ["space", "mark"],
8550
+ ...single ? [] : [["space", "mark"]],
8462
8551
  ["enter", verb],
8463
8552
  ["esc", query ? "clear" : "back"]
8464
8553
  ]
@@ -8575,11 +8664,15 @@ function failure(title, error) {
8575
8664
  };
8576
8665
  }
8577
8666
  function AccountLine({ facts, width }) {
8578
- const right = `${facts.scope} ${facts.skills} installed`;
8667
+ const agents = facts.agents.length > 0 ? facts.agents.join(",") : "agents unset";
8668
+ const right = `${agents} ${facts.scope} ${facts.skills} installed`;
8579
8669
  const head = facts.signedIn ? facts.email ?? "signed in" : "not signed in";
8580
8670
  const room = Math.max(width - FRAME_CHROME - right.length - 2, 12);
8581
8671
  const tail = facts.signedIn ? ` ${hostOf(facts.server)}` : " — the catalog needs a session";
8672
+ const org = facts.signedIn && facts.organization ? ` ${facts.organization}` : "";
8582
8673
  const spare = room - 2 - head.length;
8674
+ const showOrg = spare >= org.length;
8675
+ const showTail = spare - (showOrg ? org.length : 0) >= tail.length;
8583
8676
  return /* @__PURE__ */ jsxs16(Box14, {
8584
8677
  width: width - FRAME_CHROME,
8585
8678
  justifyContent: "space-between",
@@ -8594,7 +8687,11 @@ function AccountLine({ facts, width }) {
8594
8687
  color: facts.signedIn ? undefined : tone.notice,
8595
8688
  children: ` ${clip(head, room - 2)}`
8596
8689
  }),
8597
- spare >= tail.length ? /* @__PURE__ */ jsx18(Text16, {
8690
+ showOrg ? /* @__PURE__ */ jsx18(Text16, {
8691
+ color: tone.primary,
8692
+ children: org
8693
+ }) : null,
8694
+ showTail ? /* @__PURE__ */ jsx18(Text16, {
8598
8695
  dimColor: true,
8599
8696
  children: tail
8600
8697
  }) : null
@@ -8760,20 +8857,41 @@ function HomeScreen({
8760
8857
  held.current = null;
8761
8858
  return routeFrom(answer, say(command, facts));
8762
8859
  }, [catalogDoor, ctx, facts, routeFrom]);
8763
- const ownRoute = useCallback3((value) => {
8860
+ const ownRoute = useCallback3(async (value) => {
8764
8861
  switch (value) {
8765
8862
  case "catalog.search":
8766
8863
  return { kind: "browse", tab: "skills" };
8767
8864
  case "installed.remove":
8768
8865
  return {
8769
8866
  kind: "pick",
8867
+ title: "remove",
8770
8868
  rows: installedRows(where).map((row) => ({
8771
8869
  name: row.name,
8772
8870
  description: `${row.bundle ?? "loose"} ${shortRef(row.ref)}`
8773
- }))
8871
+ })),
8872
+ empty: "Nothing installed here.",
8873
+ verb: "remove",
8874
+ command: (names) => ({ kind: "remove", names })
8774
8875
  };
8876
+ case "account.org": {
8877
+ const orgs = await organizationChoices(server);
8878
+ const active = orgs.find((org) => org.active)?.name;
8879
+ return {
8880
+ kind: "pick",
8881
+ title: "org",
8882
+ rows: orgs.map((org) => ({
8883
+ name: org.name,
8884
+ description: org.slug ?? ""
8885
+ })),
8886
+ empty: "You are not a member of any organization yet.",
8887
+ verb: "switch",
8888
+ single: true,
8889
+ ...active ? { marked: active } : {},
8890
+ command: (names) => ({ kind: "org", name: names[0] ?? "" })
8891
+ };
8892
+ }
8775
8893
  }
8776
- }, [where]);
8894
+ }, [where, server]);
8777
8895
  const routeForDoor = useCallback3(async (door) => door.opens === undefined ? ownRoute(door.value) : routeForCommand(door.opens), [ownRoute, routeForCommand]);
8778
8896
  const open = useCallback3((pending, title) => {
8779
8897
  pending.then((next) => {
@@ -8824,6 +8942,9 @@ function HomeScreen({
8824
8942
  });
8825
8943
  }, [reload, resite]);
8826
8944
  const afterLogin = useCallback3(() => {
8945
+ learnActiveOrganization(server).then(() => reload()).then(setFacts).catch(() => {
8946
+ return;
8947
+ });
8827
8948
  if (!wizard) {
8828
8949
  back();
8829
8950
  return;
@@ -8833,7 +8954,7 @@ function HomeScreen({
8833
8954
  setFacts(next);
8834
8955
  setRoute({ kind: "browse" });
8835
8956
  }).catch(() => setRoute(null));
8836
- }, [wizard, back, reload]);
8957
+ }, [wizard, back, reload, server]);
8837
8958
  const leaveWelcome = useCallback3((start) => {
8838
8959
  setWelcome(false);
8839
8960
  onWelcomed().catch(() => {
@@ -8920,13 +9041,16 @@ function HomeScreen({
8920
9041
  });
8921
9042
  }
8922
9043
  if (route?.kind === "pick") {
9044
+ const picked = route;
8923
9045
  return /* @__PURE__ */ jsx18(PickScreen, {
8924
- title: "remove",
8925
- rows: route.rows,
8926
- empty: "Nothing installed here.",
8927
- verb: "remove",
9046
+ title: picked.title,
9047
+ rows: picked.rows,
9048
+ empty: picked.empty,
9049
+ verb: picked.verb,
9050
+ ...picked.single ? { single: true } : {},
9051
+ ...picked.marked ? { marked: picked.marked } : {},
8928
9052
  onPick: (names) => {
8929
- open(routeForCommand({ kind: "remove", names }), "remove");
9053
+ open(routeForCommand(picked.command(names)), picked.title);
8930
9054
  },
8931
9055
  onBack: back
8932
9056
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yourskills",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Browse, install and keep up to date the agent skills and bundles your team publishes on yourskills.",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://yourskills.store",