fleetfizz 0.14.2 → 0.15.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.
Files changed (2) hide show
  1. package/dist/fleetfizz.mjs +354 -88
  2. package/package.json +1 -1
@@ -16,7 +16,7 @@ import * as readline from "node:readline";
16
16
  import process$1, { cwd, env } from "node:process";
17
17
  import tty from "node:tty";
18
18
  import { EventEmitter } from "node:events";
19
- import { types } from "node:util";
19
+ import { stripVTControlCharacters, types } from "node:util";
20
20
  //#region \0rolldown/runtime.js
21
21
  var __create = Object.create;
22
22
  var __defProp = Object.defineProperty;
@@ -3791,7 +3791,7 @@ const ServiceProto = {
3791
3791
  .../*#__PURE__*/ Prototype({
3792
3792
  label: "Service",
3793
3793
  evaluate(fiber) {
3794
- return exitSucceed(get$3(fiber.context, this));
3794
+ return exitSucceed(get$4(fiber.context, this));
3795
3795
  }
3796
3796
  }),
3797
3797
  toJSON() {
@@ -3807,10 +3807,10 @@ const ServiceProto = {
3807
3807
  return make$32(this, self);
3808
3808
  },
3809
3809
  use(f) {
3810
- return withFiber$1((fiber) => f(get$3(fiber.context, this)));
3810
+ return withFiber$1((fiber) => f(get$4(fiber.context, this)));
3811
3811
  },
3812
3812
  useSync(f) {
3813
- return withFiber$1((fiber) => exitSucceed(f(get$3(fiber.context, this))));
3813
+ return withFiber$1((fiber) => exitSucceed(f(get$4(fiber.context, this))));
3814
3814
  }
3815
3815
  };
3816
3816
  const cacheKeys = /*#__PURE__*/ new Set();
@@ -4170,7 +4170,7 @@ const getUnsafe = /*#__PURE__*/ dual(2, (self, service) => {
4170
4170
  * @category getters
4171
4171
  * @since 2.0.0
4172
4172
  */
4173
- const get$3 = getUnsafe;
4173
+ const get$4 = getUnsafe;
4174
4174
  const defaultValueCacheKey = "~effect/Context/defaultValue";
4175
4175
  const getDefaultValue = (ref) => {
4176
4176
  if (defaultValueCacheKey in ref) return ref[defaultValueCacheKey];
@@ -6908,7 +6908,7 @@ var FiberImpl = class {
6908
6908
  return this._dispatcher ??= this.cache.scheduler.makeDispatcher();
6909
6909
  }
6910
6910
  getRef(ref) {
6911
- return get$3(this.context, ref);
6911
+ return get$4(this.context, ref);
6912
6912
  }
6913
6913
  addObserver(cb) {
6914
6914
  if (this._exit) {
@@ -7066,17 +7066,17 @@ var FiberImpl = class {
7066
7066
  const makeFiberContextCache = (context) => {
7067
7067
  const currentTracer = getOrUndefinedUnsafe(context, TracerKey);
7068
7068
  return {
7069
- scheduler: get$3(context, Scheduler),
7069
+ scheduler: get$4(context, Scheduler),
7070
7070
  tracer: currentTracer,
7071
7071
  tracerContext: currentTracer ? currentTracer["context"] : void 0,
7072
- tracerEnabled: get$3(context, TracerEnabled),
7072
+ tracerEnabled: get$4(context, TracerEnabled),
7073
7073
  span: getOrUndefinedUnsafe(context, ParentSpanKey),
7074
- logLevel: get$3(context, CurrentLogLevel),
7075
- minimumLogLevel: get$3(context, MinimumLogLevel$1),
7076
- stackFrame: get$3(context, CurrentStackFrame),
7074
+ logLevel: get$4(context, CurrentLogLevel),
7075
+ minimumLogLevel: get$4(context, MinimumLogLevel$1),
7076
+ stackFrame: get$4(context, CurrentStackFrame),
7077
7077
  runtimeMetrics: getOrUndefinedUnsafe(context, FiberRuntimeMetricsKey),
7078
- maxOpsBeforeYield: get$3(context, MaxOpsBeforeYield),
7079
- preventYield: get$3(context, PreventSchedulerYield)
7078
+ maxOpsBeforeYield: get$4(context, MaxOpsBeforeYield),
7079
+ preventYield: get$4(context, PreventSchedulerYield)
7080
7080
  };
7081
7081
  };
7082
7082
  const deferredInterruptCont = {
@@ -8434,11 +8434,11 @@ const NoopSpanProto = {
8434
8434
  const noopSpan = (options) => Object.assign(Object.create(NoopSpanProto), options);
8435
8435
  const filterDisablePropagation = (span) => {
8436
8436
  if (!span) return none();
8437
- return get$3(span.annotations, DisablePropagation) ? span._tag === "Span" ? filterDisablePropagation(getOrUndefined$1(span.parent)) : none() : some(span);
8437
+ return get$4(span.annotations, DisablePropagation) ? span._tag === "Span" ? filterDisablePropagation(getOrUndefined$1(span.parent)) : none() : some(span);
8438
8438
  };
8439
8439
  /** @internal */
8440
8440
  const makeSpanUnsafe = (fiber, name, options) => {
8441
- const disablePropagation = !fiber.cache.tracerEnabled || options?.annotations && get$3(options.annotations, DisablePropagation);
8441
+ const disablePropagation = !fiber.cache.tracerEnabled || options?.annotations && get$4(options.annotations, DisablePropagation);
8442
8442
  const parent = options?.parent !== void 0 ? some(options.parent) : options?.root ? none() : filterDisablePropagation(fiber.cache.span);
8443
8443
  let span;
8444
8444
  if (disablePropagation) span = noopSpan({
@@ -18071,7 +18071,7 @@ var RcRefImpl = class {
18071
18071
  /** @internal */
18072
18072
  const make$24 = (options) => withFiber((fiber) => {
18073
18073
  const context = fiber.context;
18074
- const scope = get$3(context, Scope);
18074
+ const scope = get$4(context, Scope);
18075
18075
  const ref = new RcRefImpl(options.acquire, context, scope, options.idleTimeToLive !== void 0 ? fromInputUnsafe$1(options.idleTimeToLive) : void 0);
18076
18076
  return as(addFinalizerExit(scope, () => {
18077
18077
  const state = ref.state;
@@ -18134,7 +18134,7 @@ const awaitAcquiring = (self, acquiring, restore) => onExit$1(restore(join(acqui
18134
18134
  return interrupt(acquiring.fiber);
18135
18135
  });
18136
18136
  /** @internal */
18137
- const get$2 = (self_) => {
18137
+ const get$3 = (self_) => {
18138
18138
  const self = self_;
18139
18139
  return uninterruptibleMask((restore) => flatMap$1(scope, (scope) => flatMap$1(getState(self, restore), (state) => as(addFinalizerExit(scope, () => release(self, state)), state.value))));
18140
18140
  };
@@ -18252,7 +18252,7 @@ const make$23 = make$24;
18252
18252
  * @category combinators
18253
18253
  * @since 3.5.0
18254
18254
  */
18255
- const get$1 = get$2;
18255
+ const get$2 = get$3;
18256
18256
  //#endregion
18257
18257
  //#region ../../node_modules/.pnpm/effect@4.0.2/node_modules/effect/dist/Stream.js
18258
18258
  /**
@@ -24846,7 +24846,7 @@ const TypeId$15 = /*#__PURE__*/ Symbol.for("~effect/http/Headers");
24846
24846
  const Proto$7 = /*#__PURE__*/ Object.defineProperties(/*#__PURE__*/ Object.create(null), {
24847
24847
  [TypeId$15]: { value: TypeId$15 },
24848
24848
  [symbolRedactable]: { value(context) {
24849
- return redact(this, get$3(context, CurrentRedactedNames));
24849
+ return redact(this, get$4(context, CurrentRedactedNames));
24850
24850
  } },
24851
24851
  toJSON: { value() {
24852
24852
  return redact$1(this);
@@ -28692,13 +28692,13 @@ const layer$5 = /*#__PURE__*/ effect(Terminal, /*#__PURE__*/ (/* @__PURE__ */ fn
28692
28692
  stdin.on("keypress", handleKeypress);
28693
28693
  if (inputEnded) handleEnd();
28694
28694
  else {
28695
- yield* get$1(rlRef);
28695
+ yield* get$2(rlRef);
28696
28696
  stdin.once("end", handleEnd);
28697
28697
  }
28698
28698
  return queue;
28699
28699
  });
28700
28700
  const readLine = suspend$2(() => poll(lines).pipe(flatMap$1(match$5({
28701
- onNone: () => scoped(andThen(get$1(rlRef), take(lines))),
28701
+ onNone: () => scoped(andThen(get$2(rlRef), take(lines))),
28702
28702
  onSome: succeed$3
28703
28703
  })), mapError(() => new QuitError({}))));
28704
28704
  const display = (prompt) => uninterruptible(callback$1((resume) => {
@@ -35654,7 +35654,7 @@ const runWith = (command, config) => {
35654
35654
  };
35655
35655
  //#endregion
35656
35656
  //#region package.json
35657
- var version = "0.14.2";
35657
+ var version = "0.15.0";
35658
35658
  //#endregion
35659
35659
  //#region ../../node_modules/.pnpm/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/scanner.js
35660
35660
  /**
@@ -38460,7 +38460,8 @@ const ReposSection = section({ cloneRoot: optionalKey(variants(withShorthand(Str
38460
38460
  encode: (root) => root.path
38461
38461
  }))).annotateKey({ description: `Where a Machine keeps its Managed Clones: a path, or a list of \`{ path, platforms?, machines? }\` where a Machine gets the first that targets it. \`~\` expands on that Machine. Default: \`${DEFAULT_CLONE_ROOT}\`.` }) }, Record(NonEmptyString, variants(TrackedRepo)), `Tracked Repos, keyed by name, which names the Managed Clone and the \`repos:<name>\` Item. Each is \`{ origin, ref, install, ... }\`, ${VARIANTS}.`);
38462
38462
  const ITEM_NAME = /^[A-Za-z0-9._-]+$/;
38463
- const badName = (names) => names.find((n) => !ITEM_NAME.test(n) || n === "." || n === "..");
38463
+ const isItemName = (name) => ITEM_NAME.test(name) && name !== "." && name !== "..";
38464
+ const badName = (names) => names.find((name) => !isItemName(name));
38464
38465
  const HomePath = NonEmptyString.check(isPattern(/^(\/|~\/)./, {
38465
38466
  title: "path",
38466
38467
  description: "an absolute path, or one under ~"
@@ -38562,6 +38563,11 @@ var ConfigNotFound = class extends TaggedError()("ConfigNotFound", { path: Strin
38562
38563
  return `no config at ${this.path}; run fizz init to write one, or pass --config`;
38563
38564
  }
38564
38565
  };
38566
+ var ConfigIsLink = class extends TaggedError()("ConfigIsLink", { path: String$6 }) {
38567
+ get message() {
38568
+ return `${this.path} is a symlink. fleetfizz copies the config to every Machine itself, so it must be a regular file; to keep its history, put its directory under git instead`;
38569
+ }
38570
+ };
38565
38571
  const lineOf = (text, offset) => text.slice(0, offset).split("\n").length;
38566
38572
  const repeatedKeys = (text) => {
38567
38573
  const problems = [];
@@ -38637,10 +38643,19 @@ const parseConfig = fn("parseConfig")(function* (path, text) {
38637
38643
  })));
38638
38644
  });
38639
38645
  const formatIssue = makeFormatterDefault();
38640
- const loadConfig = fn("loadConfig")(function* (path) {
38646
+ const refuseLink = fn("refuseLink")(function* (path) {
38641
38647
  const fs = yield* FileSystem;
38642
- if (!(yield* fs.exists(path))) return yield* new ConfigNotFound({ path });
38643
- const text = yield* fs.readFileString(path);
38648
+ if (isSome(yield* option(fs.readLink(path)))) return yield* new ConfigIsLink({ path });
38649
+ });
38650
+ const readConfigText = fn("readConfigText")(function* (path) {
38651
+ yield* refuseLink(path);
38652
+ const fs = yield* FileSystem;
38653
+ if (!(yield* fs.exists(path))) return void 0;
38654
+ return yield* fs.readFileString(path);
38655
+ });
38656
+ const loadConfig = fn("loadConfig")(function* (path) {
38657
+ const text = yield* readConfigText(path);
38658
+ if (text === void 0) return yield* new ConfigNotFound({ path });
38644
38659
  return yield* parseConfig(path, text);
38645
38660
  });
38646
38661
  //#endregion
@@ -38766,9 +38781,11 @@ const insertSpread = (text, path, value) => {
38766
38781
  const result = insertText(text, path, value);
38767
38782
  if (!("text" in result)) return result;
38768
38783
  const at = firstDifference(text, result.text);
38769
- if (lineLength(result.text, at) <= WIDTH) return result;
38784
+ const tooLong = lineLength(result.text, at) > WIDTH;
38785
+ const added = result.text.slice(at, at + result.text.length - text.length);
38786
+ if (!tooLong && !added.includes("\n")) return result;
38770
38787
  const container = oneLineAround(text, parse$2(text), Math.min(at, text.length - 1));
38771
- if (container === void 0) return result;
38788
+ if (container === void 0 || !tooLong && at >= container.offset + container.length - 1) return result;
38772
38789
  const spreadText = spread(text, container);
38773
38790
  return sameComments(text, spreadText) ? insertSpread(spreadText, path, value) : result;
38774
38791
  };
@@ -38863,12 +38880,12 @@ const targetingLists = (text, machine) => {
38863
38880
  };
38864
38881
  const saveConfig = fn("saveConfig")(function* (path, text) {
38865
38882
  const config = yield* parseConfig(path, text);
38883
+ yield* refuseLink(path);
38866
38884
  const fs = yield* FileSystem;
38867
- const target = (yield* fs.exists(path)) ? yield* fs.realPath(path) : path;
38868
- yield* fs.makeDirectory((yield* Path$1).dirname(target), { recursive: true });
38869
- yield* fs.writeFileString(`${target}.tmp`, text);
38870
- if (yield* fs.exists(target)) yield* fs.chmod(`${target}.tmp`, (yield* fs.stat(target)).mode & 4095);
38871
- yield* fs.rename(`${target}.tmp`, target);
38885
+ yield* fs.makeDirectory((yield* Path$1).dirname(path), { recursive: true });
38886
+ yield* fs.writeFileString(`${path}.tmp`, text);
38887
+ if (yield* fs.exists(path)) yield* fs.chmod(`${path}.tmp`, (yield* fs.stat(path)).mode & 4095);
38888
+ yield* fs.rename(`${path}.tmp`, path);
38872
38889
  return config;
38873
38890
  });
38874
38891
  //#endregion
@@ -39196,6 +39213,10 @@ const EngineFact = Union([
39196
39213
  sha: optionalKey(String$6),
39197
39214
  base64: optionalKey(String$6)
39198
39215
  }),
39216
+ Struct({
39217
+ fact: Literal("configLink"),
39218
+ path: String$6
39219
+ }),
39199
39220
  Struct({
39200
39221
  fact: Literal("configRecord"),
39201
39222
  record: Unknown
@@ -39250,6 +39271,12 @@ const assembleState = (facts) => {
39250
39271
  }
39251
39272
  };
39252
39273
  break;
39274
+ case "configLink":
39275
+ state = {
39276
+ ...state,
39277
+ configLink: f.path
39278
+ };
39279
+ break;
39253
39280
  case "configRecord":
39254
39281
  state = {
39255
39282
  ...state,
@@ -39341,7 +39368,7 @@ const checkScript = (input) => {
39341
39368
  lines.push(provider.observe(items, input.scope));
39342
39369
  if (provider.rule === "record") for (const item of items) lines.push(...recordFact(provider.name, item.name));
39343
39370
  }
39344
- lines.push(`ff_c=${CONFIG_DIR_SH}/${quote(CONFIG_FILE)}`, `if [ -f "$ff_c" ]; then ff_b=$(base64 < "$ff_c" | tr -d '\\n'); ff_state "{\\"fact\\":\\"config\\",\\"present\\":true,\\"sha\\":\\"$(printf '%s' "$ff_b" | base64 -d | ff_sha)\\",\\"base64\\":\\"$ff_b\\"}"; else ff_state '{"fact":"config","present":false}'; fi`, `ff_r=${quotePath(CONFIG_RECORD)}`, `if [ -f "$ff_r" ]; then ff_state "{\\"fact\\":\\"configRecord\\",\\"record\\":$(tr -d '\\n' < "$ff_r")}"; fi`);
39371
+ lines.push(`ff_c=${CONFIG_DIR_SH}/${quote(CONFIG_FILE)}`, `if [ -L "$ff_c" ]; then ff_state "{\\"fact\\":\\"configLink\\",\\"path\\":\\"$(ff_json "$ff_c")\\"}"; elif [ -f "$ff_c" ]; then ff_b=$(base64 < "$ff_c" | tr -d '\\n'); ff_state "{\\"fact\\":\\"config\\",\\"present\\":true,\\"sha\\":\\"$(printf '%s' "$ff_b" | base64 -d | ff_sha)\\",\\"base64\\":\\"$ff_b\\"}"; else ff_state '{"fact":"config","present":false}'; fi`, `ff_r=${quotePath(CONFIG_RECORD)}`, `if [ -f "$ff_r" ]; then ff_state "{\\"fact\\":\\"configRecord\\",\\"record\\":$(tr -d '\\n' < "$ff_r")}"; fi`);
39345
39372
  lines.push(`ff_state '{"fact":"end"}'`);
39346
39373
  return lines.join("\n");
39347
39374
  };
@@ -40091,7 +40118,7 @@ const decideFile = (item, observed, resolved, ctx) => {
40091
40118
  };
40092
40119
  const recorded = record === void 0 ? void 0 : fingerprint(recordEntries(record));
40093
40120
  const same = ctx.machine.name === from || current !== void 0 && fingerprint(current) === ours;
40094
- const rerun = same && item.after !== void 0 ? ctx.rebuild(subject) ? "--rebuild" : ctx.machine.name !== from && recorded !== ours ? "its after has not finished" : void 0 : void 0;
40121
+ const rerun = same && item.after !== void 0 ? ctx.rebuild(subject) ? "--rebuild" : ctx.machine.name === from || recorded === ours ? void 0 : recorded === void 0 ? "its after has not succeeded here" : "its after has not finished" : void 0;
40095
40122
  if (rerun !== void 0) return {
40096
40123
  kind: "change",
40097
40124
  item: item.name,
@@ -41311,7 +41338,7 @@ const make$2 = (method) => (url, options) => modify(empty, {
41311
41338
  * @category constructors
41312
41339
  * @since 4.0.0
41313
41340
  */
41314
- const get = /*#__PURE__*/ make$2("GET");
41341
+ const get$1 = /*#__PURE__*/ make$2("GET");
41315
41342
  /**
41316
41343
  * Applies request options to an `HttpClientRequest`, returning a new request.
41317
41344
  *
@@ -41705,6 +41732,14 @@ const accessor = (method) => (...args) => flatMap$1(HttpClient, (client) => clie
41705
41732
  */
41706
41733
  const execute = /*#__PURE__*/ accessor("execute");
41707
41734
  /**
41735
+ * Executes a `GET` request using the `HttpClient` service from the environment.
41736
+ *
41737
+ * @stability unstable
41738
+ * @category accessors
41739
+ * @since 4.0.0
41740
+ */
41741
+ const get = /*#__PURE__*/ accessor("get");
41742
+ /**
41708
41743
  * Transforms a client by applying an effectful transformation to each response effect.
41709
41744
  *
41710
41745
  * @stability unstable
@@ -42205,7 +42240,7 @@ const TreeResponse = Struct({
42205
42240
  truncated: Boolean$4
42206
42241
  });
42207
42242
  const fetchFolders = fn("fetchFolders")(function* (repo, ref, token) {
42208
- const response = yield* execute(get(`https://api.github.com/repos/${repo}/git/trees/${encodeURIComponent(ref ?? "HEAD")}?recursive=1`).pipe(setHeaders({
42243
+ const response = yield* execute(get$1(`https://api.github.com/repos/${repo}/git/trees/${encodeURIComponent(ref ?? "HEAD")}?recursive=1`).pipe(setHeaders({
42209
42244
  Accept: "application/vnd.github+json",
42210
42245
  "User-Agent": "fleetfizz"
42211
42246
  }), (request) => token === void 0 ? request : bearerToken(request, token)));
@@ -73451,6 +73486,72 @@ const changeText = (path, before, after, summary) => toText(/* @__PURE__ */ (0,
73451
73486
  ]
73452
73487
  }));
73453
73488
  //#endregion
73489
+ //#region src/localSkills.ts
73490
+ const decodeLock = decodeUnknownOption(fromJsonString(Struct({
73491
+ version: Number$2,
73492
+ skills: Record(String$6, Struct({
73493
+ source: String$6,
73494
+ sourceType: String$6,
73495
+ ref: optionalKey(String$6)
73496
+ }))
73497
+ })));
73498
+ /**
73499
+ * The Skills in the CLI's lock, read where the CLI keeps it, that every
73500
+ * Machine can install: a GitHub source and a name the config can hold. One
73501
+ * from a path here, or a folder the CLI did not install, has no source another
73502
+ * Machine has. None when the lock is missing or from an older CLI.
73503
+ */
73504
+ const localSkills = fn("localSkills")(function* () {
73505
+ const fs = yield* FileSystem;
73506
+ const state = process.env.XDG_STATE_HOME;
73507
+ const path = state === void 0 || state === "" ? expandHome("~/.agents/.skill-lock.json") : `${state}/skills/.skill-lock.json`;
73508
+ const lock = decodeLock(yield* fs.readFileString(path).pipe(orElseSucceed(() => "")));
73509
+ if (lock._tag === "None" || lock.value.version < 3) return [];
73510
+ return Object.entries(lock.value.skills).filter(([name, entry]) => entry.sourceType === "github" && isItemName(name)).map(([name, entry]) => ({
73511
+ name,
73512
+ source: entry.source,
73513
+ ...entry.ref === void 0 ? {} : { ref: entry.ref }
73514
+ })).sort((a, b) => a.name.localeCompare(b.name));
73515
+ });
73516
+ const NO_TELEMETRY = { DISABLE_TELEMETRY: "1" };
73517
+ const decodeList = decodeUnknownOption(fromJsonString(ArraySchema(Struct({ agents: ArraySchema(String$6) }))));
73518
+ /**
73519
+ * The Agents the skills CLI sees Skills installed for here, as the ids
73520
+ * `skills add -a` takes, or none when the CLI is not here. `skills ls --json`
73521
+ * names them for display ("Claude Code" is `claude-code`), the way most ids
73522
+ * are written; the few that are not (`roo` is "Roo Code") the CLI rejects,
73523
+ * and they are dropped.
73524
+ */
73525
+ const localAgents = fn("localAgents")(function* () {
73526
+ const listed = yield* runLocal("skills", [
73527
+ "ls",
73528
+ "-g",
73529
+ "--json"
73530
+ ], NO_TELEMETRY);
73531
+ const list = listed.code === 0 ? decodeList(listed.stdout) : void 0;
73532
+ const ids = list === void 0 || list._tag === "None" ? [] : [...new Set(list.value.flatMap((skill) => skill.agents.map((agent) => agent.toLowerCase().replace(/\s+/g, "-"))))];
73533
+ if (ids.length === 0) return ids;
73534
+ const validated = yield* runLocal("skills", [
73535
+ "ls",
73536
+ "-g",
73537
+ "-a",
73538
+ ...ids,
73539
+ "--json"
73540
+ ], NO_TELEMETRY);
73541
+ const invalid = validated.code === 0 ? [] : /Invalid agents: (.*)/.exec(stripVTControlCharacters(validated.stdout))?.[1]?.split(", ") ?? ids;
73542
+ return ids.filter((id) => !invalid.includes(id));
73543
+ });
73544
+ /** The skills section's `agents`, picked from `agents`, all picked to start. */
73545
+ const pickAgents = (agents) => MultiSelect({
73546
+ message: "Which agents are skills installed for? (skills CLI ids)",
73547
+ choices: agents.map((agent) => ({
73548
+ title: agent,
73549
+ value: agent,
73550
+ selected: true
73551
+ })),
73552
+ min: 1
73553
+ });
73554
+ //#endregion
73454
73555
  //#region src/configAdd.tsx
73455
73556
  var ConfigAddError = class extends TaggedError()("ConfigAddError", { reason: String$6 }) {
73456
73557
  get message() {
@@ -73471,6 +73572,13 @@ const fileEntry = (path, exclude, targeting) => untargeted(targeting) && exclude
73471
73572
  ...exclude.length > 0 ? { exclude } : {},
73472
73573
  ...targeting
73473
73574
  };
73575
+ /** A Skill's entry: its source alone, or `{ source, ref, agents, ... }` when it has more. */
73576
+ const skillEntry = (source, ref, agents, targeting) => ref === void 0 && agents.length === 0 && untargeted(targeting) ? source : {
73577
+ source,
73578
+ ...ref === void 0 ? {} : { ref },
73579
+ ...agents.length > 0 ? { agents } : {},
73580
+ ...targeting
73581
+ };
73474
73582
  const parseSpec = (spec) => {
73475
73583
  const at = spec.lastIndexOf("@");
73476
73584
  return at > 0 ? {
@@ -73481,9 +73589,8 @@ const parseSpec = (spec) => {
73481
73589
  const fileName = (path) => (path.replace(/\/+$/, "").split("/").at(-1) ?? "").replace(/^\.+/, "").replace(/[^A-Za-z0-9._-]/g, "-");
73482
73590
  const openConfig = fn("openConfig")(function* (flag) {
73483
73591
  const path = flag ?? configPath(process.env);
73484
- const fs = yield* FileSystem;
73485
- if (!(yield* fs.exists(path))) return yield* new ConfigAddError({ reason: `no config at ${homeRelative(path)}; run fizz init to write one, or pass --config` });
73486
- const text = yield* fs.readFileString(path);
73592
+ const text = yield* readConfigText(path);
73593
+ if (text === void 0) return yield* new ConfigAddError({ reason: `no config at ${homeRelative(path)}; run fizz init to write one, or pass --config` });
73487
73594
  return {
73488
73595
  path,
73489
73596
  text,
@@ -73506,18 +73613,24 @@ const addItem = fn("addItem")(function* (input) {
73506
73613
  if (unknown.length > 0) return yield* new ConfigAddError({ reason: `not in machines: ${unknown.join(", ")} (add it with fizz config add machines)` });
73507
73614
  const tailnet = yield* readTailnet();
73508
73615
  const linux = config.machines.some((m) => tailnet.fleet.find((f) => f.name === m)?.platform === "linux");
73509
- const start = input.provider === "cask" && linux && config.cask?.platforms === void 0 ? insertAt(text, ["cask", "platforms"], ["darwin"]) : { text };
73510
- if ("reason" in start) return yield* new ConfigAddError({ reason: start.reason });
73511
- const at = [
73512
- input.provider,
73513
- "items",
73514
- input.name
73616
+ const edits = [
73617
+ ...input.provider === "cask" && linux && config.cask?.platforms === void 0 ? [[["cask", "platforms"], ["darwin"]]] : [],
73618
+ ...input.section ?? [],
73619
+ [[
73620
+ input.provider,
73621
+ "items",
73622
+ input.name
73623
+ ], input.entry]
73515
73624
  ];
73516
- const result = insertAt(start.text, at, input.entry);
73517
- if ("reason" in result) return yield* new ConfigAddError({ reason: result.reason });
73518
- const saved = yield* saveConfig(path, result.text);
73625
+ let next = text;
73626
+ for (const [at, value] of edits) {
73627
+ const edited = insertAt(next, at, value);
73628
+ if ("reason" in edited) return yield* new ConfigAddError({ reason: edited.reason });
73629
+ next = edited.text;
73630
+ }
73631
+ const saved = yield* saveConfig(path, next);
73519
73632
  const to = reach(saved, tailnet, input.provider, input.targeting);
73520
- yield* log(changeText(path, text, result.text, /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
73633
+ yield* log(changeText(path, text, next, /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
73521
73634
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
73522
73635
  tone: "ok",
73523
73636
  children: "added"
@@ -73578,6 +73691,40 @@ const addHomebrew = fn("addHomebrew")(function* (input) {
73578
73691
  notes
73579
73692
  });
73580
73693
  });
73694
+ /** The skills section's first `agents`, picked from the Agents the skills CLI sees here. */
73695
+ const pickHere = fn("pickHere")(function* () {
73696
+ const here = yield* localAgents();
73697
+ if (here.length === 0) return yield* new ConfigAddError({ reason: "the skills CLI sees no agents here; pass --agent" });
73698
+ return yield* pickAgents(here);
73699
+ });
73700
+ /**
73701
+ * `fizz config add skills <name>`. The source and ref default to the ones the
73702
+ * skills CLI installed it from here. The first Skill also writes the section's
73703
+ * `agents`, from --agent or a pick of the Agents here; after that, --agent is
73704
+ * the Skill's own.
73705
+ */
73706
+ const addSkill = fn("addSkill")(function* (input) {
73707
+ const { config } = yield* openConfig(input.config);
73708
+ const ask = interactive();
73709
+ const here = input.source === void 0 ? (yield* localSkills()).find((skill) => skill.name === input.name) : void 0;
73710
+ const source = input.source ?? here?.source ?? (ask ? yield* String$4({
73711
+ message: "Source (`owner/repo` on GitHub, or any source skills add takes)",
73712
+ validate: (value) => value.trim() === "" ? fail$3("a source is needed") : succeed$3(value.trim())
73713
+ }) : yield* needs("--source"));
73714
+ const ref = input.ref ?? here?.ref;
73715
+ const agents = [...new Set(input.agents)];
73716
+ const section = config.skills !== void 0 ? [] : [[["skills", "agents"], agents.length > 0 ? agents : !ask ? yield* needs("--agent") : yield* pickHere()]];
73717
+ const targeting = targetingOf(input.targeting);
73718
+ yield* addItem({
73719
+ config: input.config,
73720
+ provider: "skills",
73721
+ name: input.name,
73722
+ entry: skillEntry(source, ref, section.length > 0 ? [] : agents, targeting),
73723
+ targeting,
73724
+ section,
73725
+ notes: PACKAGE_MANAGERS.some((pm) => config[pm]?.items?.skills !== void 0) ? [] : [`No package manager in the config installs the skills CLI; add it with fizz config add ${PACKAGE_MANAGERS.join("|")} skills`]
73726
+ });
73727
+ });
73581
73728
  const defaultBranch = fn("defaultBranch")(function* (origin) {
73582
73729
  const remote = yield* runLocal("git", [
73583
73730
  "ls-remote",
@@ -74394,6 +74541,12 @@ const check = fn("check")(function* (target, script, ledger) {
74394
74541
  yield* ledger.fail(target.name, ["checking did not finish", ...output]);
74395
74542
  return { kind: "failed" };
74396
74543
  }
74544
+ if (state.configLink !== void 0) {
74545
+ const { configLink: link, home } = state;
74546
+ const path = home !== void 0 && link.startsWith(`${home}/`) ? `~${link.slice(home.length)}` : link;
74547
+ yield* ledger.fail(target.name, [new ConfigIsLink({ path }).message]);
74548
+ return { kind: "failed" };
74549
+ }
74397
74550
  return {
74398
74551
  kind: "checked",
74399
74552
  state
@@ -74949,33 +75102,74 @@ const DOTFILES = [
74949
75102
  ];
74950
75103
  const draftText = (picks) => {
74951
75104
  const edits = [
74952
- ...picks.globals.map((g) => [[
74953
- g.pm,
74954
- "items",
74955
- g.name
74956
- ], `^${g.version}`]),
74957
- ...picks.formulae.map((f) => [[
74958
- "brew",
74959
- "items",
75105
+ ...picks.machines.map((m, i) => [
75106
+ m,
75107
+ ["machines", i],
75108
+ m
75109
+ ]),
75110
+ ...picks.globals.map((g) => [
75111
+ g.name,
75112
+ [
75113
+ g.pm,
75114
+ "items",
75115
+ g.name
75116
+ ],
75117
+ `^${g.version}`
75118
+ ]),
75119
+ ...picks.formulae.map((f) => [
75120
+ f.name,
75121
+ [
75122
+ "brew",
75123
+ "items",
75124
+ f.name
75125
+ ],
75126
+ rangeEntry("*", picks.linux && f.macos ? { platforms: ["darwin"] } : {})
75127
+ ]),
75128
+ ...picks.casks.length > 0 && picks.linux ? [[
75129
+ "the cask platforms",
75130
+ ["cask", "platforms"],
75131
+ ["darwin"]
75132
+ ]] : [],
75133
+ ...picks.casks.map((c) => [
75134
+ c,
75135
+ [
75136
+ "cask",
75137
+ "items",
75138
+ c
75139
+ ],
75140
+ "*"
75141
+ ]),
75142
+ ...picks.skills.length > 0 ? [[
75143
+ "the skills agents",
75144
+ ["skills", "agents"],
75145
+ picks.agents
75146
+ ]] : [],
75147
+ ...picks.skills.map((skill) => [
75148
+ skill.name,
75149
+ [
75150
+ "skills",
75151
+ "items",
75152
+ skill.name
75153
+ ],
75154
+ skillEntry(skill.source, skill.ref, [], {})
75155
+ ]),
75156
+ ...picks.files.map((f) => [
75157
+ f,
75158
+ [
75159
+ "files",
75160
+ "items",
75161
+ fileName(f)
75162
+ ],
74960
75163
  f
74961
- ], "*"]),
74962
- ...picks.casks.length > 0 && picks.linux ? [[["cask", "platforms"], ["darwin"]]] : [],
74963
- ...picks.casks.map((c) => [[
74964
- "cask",
74965
- "items",
74966
- c
74967
- ], "*"]),
74968
- ...picks.files.map((f) => [[
74969
- "files",
74970
- "items",
74971
- fileName(f)
74972
- ], f])
75164
+ ])
74973
75165
  ];
74974
- const start = `{\n "$schema": "./${SCHEMA_FILE}",\n "version": ${JSON.stringify(picks.version)},\n "machines": [],\n}\n`;
74975
- return [...picks.machines.map((m, i) => [["machines", i], m]), ...edits].reduce((text, [path, value]) => {
75166
+ let text = `{\n "$schema": "./${SCHEMA_FILE}",\n "version": ${JSON.stringify(picks.version)},\n "machines": [],\n}\n`;
75167
+ for (const [pick, path, value] of edits) {
74976
75168
  const result = insertAt(text, path, value);
74977
- return "text" in result ? result.text : text;
74978
- }, start);
75169
+ if ("reason" in result) return { reason: `fizz init could not add ${pick}: ${result.reason}` };
75170
+ text = result.text;
75171
+ }
75172
+ return { text };
74979
75173
  };
74980
75174
  const CheckLine = (props) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
74981
75175
  tone: props.check.ok ? "ok" : props.check.required ? "fail" : "skip",
@@ -75005,6 +75199,39 @@ const lines = fn("lines")(function* (command, args) {
75005
75199
  const ran = yield* runLocal(command, args);
75006
75200
  return ran.code === 0 ? ran.stdout.split("\n").filter((l) => l.trim() !== "") : [];
75007
75201
  });
75202
+ const Requirements = ArraySchema(Struct({ name: String$6 }));
75203
+ const FormulaInfo = Struct({
75204
+ requirements: Requirements,
75205
+ variations: optionalKey(NullOr(Record(String$6, Struct({ requirements: optionalKey(Requirements) }))))
75206
+ });
75207
+ const macosOnly = (f) => {
75208
+ const linux = Object.entries(f.variations ?? {}).flatMap(([tag, v]) => tag.endsWith("_linux") ? [v.requirements ?? f.requirements] : []);
75209
+ return (linux.length > 0 ? linux : [f.requirements]).every((rs) => rs.some((r) => r.name === "macos"));
75210
+ };
75211
+ const BrewInfo = fromJsonString(Struct({ formulae: ArraySchema(Struct({
75212
+ ...FormulaInfo.fields,
75213
+ full_name: String$6
75214
+ })) }));
75215
+ const coreMacosOnly = fn("coreMacosOnly")(function* (name) {
75216
+ const response = yield* get(`https://formulae.brew.sh/api/formula/${name}.json`);
75217
+ return macosOnly(yield* schemaBodyJson(FormulaInfo)(response));
75218
+ }, timeout("10 seconds"), orElseSucceed(() => false), provide(layer$2));
75219
+ const tappedMacosOnly = fn("tappedMacosOnly")(function* (names) {
75220
+ if (names.length === 0) return [];
75221
+ const ran = yield* runLocal("brew", [
75222
+ "info",
75223
+ "--json=v2",
75224
+ "--variations",
75225
+ "--formula",
75226
+ ...names
75227
+ ]);
75228
+ return (yield* decodeEffect(BrewInfo)(ran.stdout)).formulae.filter(macosOnly).map((f) => f.full_name);
75229
+ }, orElseSucceed(() => []));
75230
+ const macosFormulae = fn("macosFormulae")(function* (names) {
75231
+ const tapped = yield* tappedMacosOnly(names.filter((n) => n.includes("/")));
75232
+ const core = yield* filter(names.filter((n) => !n.includes("/")), coreMacosOnly, { concurrency: 8 });
75233
+ return /* @__PURE__ */ new Set([...tapped, ...core]);
75234
+ });
75008
75235
  const pickSome = (message, choices) => choices.length === 0 ? succeed$3([]) : MultiSelect({
75009
75236
  message,
75010
75237
  choices,
@@ -75035,8 +75262,9 @@ const init = fn("init")(function* (flags) {
75035
75262
  return;
75036
75263
  }
75037
75264
  const tailnet = yield* readTailnet();
75038
- if (yield* fs.exists(path)) {
75039
- const config = yield* loadConfig(path);
75265
+ const existing = yield* readConfigText(path);
75266
+ if (existing !== void 0) {
75267
+ const config = yield* parseConfig(path, existing);
75040
75268
  yield* log(`\n${configText({
75041
75269
  path,
75042
75270
  config,
@@ -75054,35 +75282,57 @@ const init = fn("init")(function* (flags) {
75054
75282
  name,
75055
75283
  version
75056
75284
  })))))).flat().filter((g) => g.name !== SELF_PACKAGE);
75057
- const globals = yield* pickSome(`Which Globals from this Machine? (${installed.length} installed)`, installed.map((g) => ({
75285
+ const pickedGlobals = yield* pickSome(`Which Globals from this Machine? (${installed.length} installed)`, installed.map((g) => ({
75058
75286
  title: `${g.name} ${g.version} ${g.pm}`,
75059
75287
  value: g
75060
75288
  })));
75289
+ const linux = machines.some((m) => tailnet.fleet.find((f) => f.name === m)?.platform === "linux");
75061
75290
  const leaves = yield* lines("brew", ["leaves", "--installed-on-request"]);
75062
- const formulae = yield* pickSome(`Which Formulae from this Machine? (${leaves.length} installed on request)`, leaves.map((f) => ({
75291
+ const pickedFormulae = yield* pickSome(`Which Formulae from this Machine? (${leaves.length} installed on request)`, leaves.map((f) => ({
75063
75292
  title: f,
75064
75293
  value: f
75065
75294
  })));
75295
+ const macos = linux ? yield* macosFormulae(pickedFormulae) : /* @__PURE__ */ new Set();
75296
+ const formulae = pickedFormulae.map((name) => ({
75297
+ name,
75298
+ macos: macos.has(name)
75299
+ }));
75066
75300
  const installedCasks = process.platform === "darwin" ? yield* lines("brew", ["list", "--cask"]) : [];
75067
75301
  const casks = yield* pickSome(`Which Casks from this Machine? (${installedCasks.length} installed)`, installedCasks.map((c) => ({
75068
75302
  title: c,
75069
75303
  value: c
75070
75304
  })));
75305
+ const agentsHere = yield* localAgents();
75306
+ const fromGitHub = agentsHere.length === 0 ? [] : yield* localSkills();
75307
+ const skills = yield* pickSome(`Which skills from this Machine? (${fromGitHub.length} installed from GitHub)`, fromGitHub.map((skill) => ({
75308
+ title: `${skill.name} ${skill.source}${skill.ref === void 0 ? "" : `#${skill.ref}`}`,
75309
+ value: skill,
75310
+ selected: true
75311
+ })));
75312
+ const agents = skills.length === 0 ? [] : yield* pickAgents(agentsHere);
75313
+ const cli = installed.find((global) => global.name === "skills");
75314
+ const addCli = cli !== void 0 && skills.length > 0 && !pickedGlobals.some((global) => global.name === "skills");
75315
+ const globals = addCli ? [...pickedGlobals, cli] : pickedGlobals;
75316
+ if (addCli) yield* log(`The skills CLI is in the config too, as a ${cli.pm} Global: a Ship installs skills with it.`);
75317
+ if (cli === void 0 && skills.length > 0) yield* log(`No package manager here has the skills CLI, so a Ship cannot install skills; add it with fizz config add ${PACKAGE_MANAGERS.join("|")} skills.`);
75071
75318
  const present = yield* filter(DOTFILES, (f) => fs.exists(expandHome(f)));
75072
75319
  const files = yield* pickSome("Which Shared Files? A Ship copies each from the Machine it starts on.", present.map((f) => ({
75073
75320
  title: f,
75074
75321
  value: f
75075
75322
  })));
75076
- const text = draftText({
75323
+ const draft = draftText({
75077
75324
  version: flags.version,
75078
75325
  machines,
75079
75326
  globals,
75080
75327
  formulae,
75081
75328
  casks,
75329
+ skills,
75330
+ agents,
75082
75331
  files,
75083
- linux: machines.some((m) => tailnet.fleet.find((f) => f.name === m)?.platform === "linux")
75332
+ linux
75084
75333
  });
75085
- const config = yield* saveConfig(path, text);
75334
+ if ("reason" in draft) return yield* new ConfigAddError({ reason: draft.reason });
75335
+ const config = yield* saveConfig(path, draft.text);
75086
75336
  const paths = yield* Path$1;
75087
75337
  yield* fs.writeFileString(paths.join(paths.dirname(path), SCHEMA_FILE), fleetConfigSchemaText());
75088
75338
  yield* log(`\n${toText(/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(T, {
@@ -75093,7 +75343,7 @@ const init = fn("init")(function* (flags) {
75093
75343
  path,
75094
75344
  config,
75095
75345
  tailnet,
75096
- footer: [`Add more with fizz config add repos|files|${PACKAGE_MANAGERS.join("|")}|brew|cask.`]
75346
+ footer: [`Add more with fizz config add repos|files|${PACKAGE_MANAGERS.join("|")}|brew|cask|skills.`]
75097
75347
  }));
75098
75348
  if (yield* Confirm({
75099
75349
  message: "Run fizz ship --dry-run now?",
@@ -75350,10 +75600,10 @@ const install = fn("install")(function* (version, why) {
75350
75600
  if ((yield* runHere("sh", ["-c", installWith(installer, [spec])])) !== 0) return yield* new VersionError({ reason: `could not install ${spec} with ${installer}; run ${MANAGERS[installer].install} ${spec}` });
75351
75601
  });
75352
75602
  const atConfigVersion = (flag, running, effect) => gen(function* () {
75353
- const fs = yield* FileSystem;
75354
75603
  const path = flag ?? configPath(process.env);
75355
- if (!(yield* fs.exists(path))) return yield* effect;
75356
- const wanted = readVersion(yield* fs.readFileString(path));
75604
+ const text = yield* readConfigText(path);
75605
+ if (text === void 0) return yield* effect;
75606
+ const wanted = readVersion(text);
75357
75607
  if (wanted === void 0 || wanted === running) return yield* effect;
75358
75608
  if (process.env[SWITCHED] !== void 0) return yield* new VersionError({ reason: `${homeRelative(path)} needs fleetfizz ${wanted}, but the fizz on PATH is still ${running} after installing it; see which fizz comes first (command -v fizz)` });
75359
75609
  yield* install(wanted, `${homeRelative(path)} needs fleetfizz ${wanted}, not ${running}`);
@@ -75370,12 +75620,11 @@ const latest = fn("latest")(function* () {
75370
75620
  return version;
75371
75621
  });
75372
75622
  const selfUpdate = fn("selfUpdate")(function* (input) {
75373
- const fs = yield* FileSystem;
75374
75623
  const path = input.config ?? configPath(process.env);
75375
- if (!(yield* fs.exists(path))) return yield* new VersionError({ reason: `no config at ${homeRelative(path)}; run fizz init to write one, or pass --config` });
75624
+ const text = yield* readConfigText(path);
75625
+ if (text === void 0) return yield* new VersionError({ reason: `no config at ${homeRelative(path)}; run fizz init to write one, or pass --config` });
75376
75626
  const version = input.version ?? (yield* latest());
75377
75627
  if (predatesVersion(version)) return yield* new VersionError({ reason: `fleetfizz ${version} cannot read this config, which needs 0.14.0 or newer` });
75378
- const text = yield* fs.readFileString(path);
75379
75628
  const named = readVersion(text) === version;
75380
75629
  if (named && version === input.running) {
75381
75630
  yield* log(`fleetfizz ${version} already`);
@@ -75478,6 +75727,23 @@ const addCommand = make$4("add").pipe(withDescription("Add an Item or a Machine
75478
75727
  ...PACKAGE_MANAGERS.map(globalCommand),
75479
75728
  homebrewCommand("brew"),
75480
75729
  homebrewCommand("cask"),
75730
+ make$4("skills", {
75731
+ name: String$2("name").pipe(withDescription$2("The skill, by the name its source gives it.")),
75732
+ source: String$1("source").pipe(optional, withDescription$1("Where the skills CLI installs it from: `owner/repo` on GitHub, or any source `skills add` takes. Default: the GitHub repo the CLI installed it from here. Asked for when missing.")),
75733
+ ref: String$1("ref").pipe(optional, withDescription$1("A branch, tag, or full commit SHA. Default: the repo's default branch, or without `--source`, the ref the CLI installed it at here.")),
75734
+ agent: String$1("agent").pipe(atLeast(0), withMetavar("id"), withDescription$1("An agent to install it for, as a skills CLI id (`claude-code`). Repeat the flag for more. For the first skill, they are the section's `agents`, picked from this Machine's when missing; after that, the skill's own.")),
75735
+ ...targetingFlags
75736
+ }).pipe(withDescription("Add an agent skill."), withHandler((flags) => withConfig((config) => addSkill({
75737
+ config,
75738
+ name: flags.name,
75739
+ source: getOrUndefined$1(flags.source),
75740
+ ref: getOrUndefined$1(flags.ref),
75741
+ agents: flags.agent,
75742
+ targeting: {
75743
+ platforms: flags.platform,
75744
+ machines: flags.machine
75745
+ }
75746
+ })).pipe(printProblems))),
75481
75747
  make$4("repos", {
75482
75748
  origin: String$2("origin").pipe(withDescription$2("The GitHub `owner/repo`.")),
75483
75749
  name: String$1("name").pipe(optional, withDescription$1("The Item's name. Default: the repo's name.")),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fleetfizz",
3
- "version": "0.14.2",
3
+ "version": "0.15.0",
4
4
  "description": "Keep every Machine on your tailnet in line with one config.",
5
5
  "keywords": [
6
6
  "agent-skills",