replicas-cli 0.3.73 → 0.3.74

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/index.cjs +278 -143
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -111,17 +111,17 @@ var require_visit = __commonJS({
111
111
  visit.BREAK = BREAK;
112
112
  visit.SKIP = SKIP;
113
113
  visit.REMOVE = REMOVE;
114
- function visit_(key, node, visitor, path7) {
115
- const ctrl = callVisitor(key, node, visitor, path7);
114
+ function visit_(key, node, visitor, path8) {
115
+ const ctrl = callVisitor(key, node, visitor, path8);
116
116
  if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
117
- replaceNode(key, path7, ctrl);
118
- return visit_(key, ctrl, visitor, path7);
117
+ replaceNode(key, path8, ctrl);
118
+ return visit_(key, ctrl, visitor, path8);
119
119
  }
120
120
  if (typeof ctrl !== "symbol") {
121
121
  if (identity.isCollection(node)) {
122
- path7 = Object.freeze(path7.concat(node));
122
+ path8 = Object.freeze(path8.concat(node));
123
123
  for (let i = 0; i < node.items.length; ++i) {
124
- const ci = visit_(i, node.items[i], visitor, path7);
124
+ const ci = visit_(i, node.items[i], visitor, path8);
125
125
  if (typeof ci === "number")
126
126
  i = ci - 1;
127
127
  else if (ci === BREAK)
@@ -132,13 +132,13 @@ var require_visit = __commonJS({
132
132
  }
133
133
  }
134
134
  } else if (identity.isPair(node)) {
135
- path7 = Object.freeze(path7.concat(node));
136
- const ck = visit_("key", node.key, visitor, path7);
135
+ path8 = Object.freeze(path8.concat(node));
136
+ const ck = visit_("key", node.key, visitor, path8);
137
137
  if (ck === BREAK)
138
138
  return BREAK;
139
139
  else if (ck === REMOVE)
140
140
  node.key = null;
141
- const cv = visit_("value", node.value, visitor, path7);
141
+ const cv = visit_("value", node.value, visitor, path8);
142
142
  if (cv === BREAK)
143
143
  return BREAK;
144
144
  else if (cv === REMOVE)
@@ -159,17 +159,17 @@ var require_visit = __commonJS({
159
159
  visitAsync.BREAK = BREAK;
160
160
  visitAsync.SKIP = SKIP;
161
161
  visitAsync.REMOVE = REMOVE;
162
- async function visitAsync_(key, node, visitor, path7) {
163
- const ctrl = await callVisitor(key, node, visitor, path7);
162
+ async function visitAsync_(key, node, visitor, path8) {
163
+ const ctrl = await callVisitor(key, node, visitor, path8);
164
164
  if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
165
- replaceNode(key, path7, ctrl);
166
- return visitAsync_(key, ctrl, visitor, path7);
165
+ replaceNode(key, path8, ctrl);
166
+ return visitAsync_(key, ctrl, visitor, path8);
167
167
  }
168
168
  if (typeof ctrl !== "symbol") {
169
169
  if (identity.isCollection(node)) {
170
- path7 = Object.freeze(path7.concat(node));
170
+ path8 = Object.freeze(path8.concat(node));
171
171
  for (let i = 0; i < node.items.length; ++i) {
172
- const ci = await visitAsync_(i, node.items[i], visitor, path7);
172
+ const ci = await visitAsync_(i, node.items[i], visitor, path8);
173
173
  if (typeof ci === "number")
174
174
  i = ci - 1;
175
175
  else if (ci === BREAK)
@@ -180,13 +180,13 @@ var require_visit = __commonJS({
180
180
  }
181
181
  }
182
182
  } else if (identity.isPair(node)) {
183
- path7 = Object.freeze(path7.concat(node));
184
- const ck = await visitAsync_("key", node.key, visitor, path7);
183
+ path8 = Object.freeze(path8.concat(node));
184
+ const ck = await visitAsync_("key", node.key, visitor, path8);
185
185
  if (ck === BREAK)
186
186
  return BREAK;
187
187
  else if (ck === REMOVE)
188
188
  node.key = null;
189
- const cv = await visitAsync_("value", node.value, visitor, path7);
189
+ const cv = await visitAsync_("value", node.value, visitor, path8);
190
190
  if (cv === BREAK)
191
191
  return BREAK;
192
192
  else if (cv === REMOVE)
@@ -213,23 +213,23 @@ var require_visit = __commonJS({
213
213
  }
214
214
  return visitor;
215
215
  }
216
- function callVisitor(key, node, visitor, path7) {
216
+ function callVisitor(key, node, visitor, path8) {
217
217
  if (typeof visitor === "function")
218
- return visitor(key, node, path7);
218
+ return visitor(key, node, path8);
219
219
  if (identity.isMap(node))
220
- return visitor.Map?.(key, node, path7);
220
+ return visitor.Map?.(key, node, path8);
221
221
  if (identity.isSeq(node))
222
- return visitor.Seq?.(key, node, path7);
222
+ return visitor.Seq?.(key, node, path8);
223
223
  if (identity.isPair(node))
224
- return visitor.Pair?.(key, node, path7);
224
+ return visitor.Pair?.(key, node, path8);
225
225
  if (identity.isScalar(node))
226
- return visitor.Scalar?.(key, node, path7);
226
+ return visitor.Scalar?.(key, node, path8);
227
227
  if (identity.isAlias(node))
228
- return visitor.Alias?.(key, node, path7);
228
+ return visitor.Alias?.(key, node, path8);
229
229
  return void 0;
230
230
  }
231
- function replaceNode(key, path7, node) {
232
- const parent = path7[path7.length - 1];
231
+ function replaceNode(key, path8, node) {
232
+ const parent = path8[path8.length - 1];
233
233
  if (identity.isCollection(parent)) {
234
234
  parent.items[key] = node;
235
235
  } else if (identity.isPair(parent)) {
@@ -839,10 +839,10 @@ var require_Collection = __commonJS({
839
839
  var createNode = require_createNode();
840
840
  var identity = require_identity();
841
841
  var Node = require_Node();
842
- function collectionFromPath(schema, path7, value) {
842
+ function collectionFromPath(schema, path8, value) {
843
843
  let v = value;
844
- for (let i = path7.length - 1; i >= 0; --i) {
845
- const k = path7[i];
844
+ for (let i = path8.length - 1; i >= 0; --i) {
845
+ const k = path8[i];
846
846
  if (typeof k === "number" && Number.isInteger(k) && k >= 0) {
847
847
  const a = [];
848
848
  a[k] = v;
@@ -861,7 +861,7 @@ var require_Collection = __commonJS({
861
861
  sourceObjects: /* @__PURE__ */ new Map()
862
862
  });
863
863
  }
864
- var isEmptyPath = (path7) => path7 == null || typeof path7 === "object" && !!path7[Symbol.iterator]().next().done;
864
+ var isEmptyPath = (path8) => path8 == null || typeof path8 === "object" && !!path8[Symbol.iterator]().next().done;
865
865
  var Collection = class extends Node.NodeBase {
866
866
  constructor(type, schema) {
867
867
  super(type);
@@ -891,11 +891,11 @@ var require_Collection = __commonJS({
891
891
  * be a Pair instance or a `{ key, value }` object, which may not have a key
892
892
  * that already exists in the map.
893
893
  */
894
- addIn(path7, value) {
895
- if (isEmptyPath(path7))
894
+ addIn(path8, value) {
895
+ if (isEmptyPath(path8))
896
896
  this.add(value);
897
897
  else {
898
- const [key, ...rest] = path7;
898
+ const [key, ...rest] = path8;
899
899
  const node = this.get(key, true);
900
900
  if (identity.isCollection(node))
901
901
  node.addIn(rest, value);
@@ -909,8 +909,8 @@ var require_Collection = __commonJS({
909
909
  * Removes a value from the collection.
910
910
  * @returns `true` if the item was found and removed.
911
911
  */
912
- deleteIn(path7) {
913
- const [key, ...rest] = path7;
912
+ deleteIn(path8) {
913
+ const [key, ...rest] = path8;
914
914
  if (rest.length === 0)
915
915
  return this.delete(key);
916
916
  const node = this.get(key, true);
@@ -924,8 +924,8 @@ var require_Collection = __commonJS({
924
924
  * scalar values from their surrounding node; to disable set `keepScalar` to
925
925
  * `true` (collections are always returned intact).
926
926
  */
927
- getIn(path7, keepScalar) {
928
- const [key, ...rest] = path7;
927
+ getIn(path8, keepScalar) {
928
+ const [key, ...rest] = path8;
929
929
  const node = this.get(key, true);
930
930
  if (rest.length === 0)
931
931
  return !keepScalar && identity.isScalar(node) ? node.value : node;
@@ -943,8 +943,8 @@ var require_Collection = __commonJS({
943
943
  /**
944
944
  * Checks if the collection includes a value with the key `key`.
945
945
  */
946
- hasIn(path7) {
947
- const [key, ...rest] = path7;
946
+ hasIn(path8) {
947
+ const [key, ...rest] = path8;
948
948
  if (rest.length === 0)
949
949
  return this.has(key);
950
950
  const node = this.get(key, true);
@@ -954,8 +954,8 @@ var require_Collection = __commonJS({
954
954
  * Sets a value in this collection. For `!!set`, `value` needs to be a
955
955
  * boolean to add/remove the item from the set.
956
956
  */
957
- setIn(path7, value) {
958
- const [key, ...rest] = path7;
957
+ setIn(path8, value) {
958
+ const [key, ...rest] = path8;
959
959
  if (rest.length === 0) {
960
960
  this.set(key, value);
961
961
  } else {
@@ -3470,9 +3470,9 @@ var require_Document = __commonJS({
3470
3470
  this.contents.add(value);
3471
3471
  }
3472
3472
  /** Adds a value to the document. */
3473
- addIn(path7, value) {
3473
+ addIn(path8, value) {
3474
3474
  if (assertCollection(this.contents))
3475
- this.contents.addIn(path7, value);
3475
+ this.contents.addIn(path8, value);
3476
3476
  }
3477
3477
  /**
3478
3478
  * Create a new `Alias` node, ensuring that the target `node` has the required anchor.
@@ -3547,14 +3547,14 @@ var require_Document = __commonJS({
3547
3547
  * Removes a value from the document.
3548
3548
  * @returns `true` if the item was found and removed.
3549
3549
  */
3550
- deleteIn(path7) {
3551
- if (Collection.isEmptyPath(path7)) {
3550
+ deleteIn(path8) {
3551
+ if (Collection.isEmptyPath(path8)) {
3552
3552
  if (this.contents == null)
3553
3553
  return false;
3554
3554
  this.contents = null;
3555
3555
  return true;
3556
3556
  }
3557
- return assertCollection(this.contents) ? this.contents.deleteIn(path7) : false;
3557
+ return assertCollection(this.contents) ? this.contents.deleteIn(path8) : false;
3558
3558
  }
3559
3559
  /**
3560
3560
  * Returns item at `key`, or `undefined` if not found. By default unwraps
@@ -3569,10 +3569,10 @@ var require_Document = __commonJS({
3569
3569
  * scalar values from their surrounding node; to disable set `keepScalar` to
3570
3570
  * `true` (collections are always returned intact).
3571
3571
  */
3572
- getIn(path7, keepScalar) {
3573
- if (Collection.isEmptyPath(path7))
3572
+ getIn(path8, keepScalar) {
3573
+ if (Collection.isEmptyPath(path8))
3574
3574
  return !keepScalar && identity.isScalar(this.contents) ? this.contents.value : this.contents;
3575
- return identity.isCollection(this.contents) ? this.contents.getIn(path7, keepScalar) : void 0;
3575
+ return identity.isCollection(this.contents) ? this.contents.getIn(path8, keepScalar) : void 0;
3576
3576
  }
3577
3577
  /**
3578
3578
  * Checks if the document includes a value with the key `key`.
@@ -3583,10 +3583,10 @@ var require_Document = __commonJS({
3583
3583
  /**
3584
3584
  * Checks if the document includes a value at `path`.
3585
3585
  */
3586
- hasIn(path7) {
3587
- if (Collection.isEmptyPath(path7))
3586
+ hasIn(path8) {
3587
+ if (Collection.isEmptyPath(path8))
3588
3588
  return this.contents !== void 0;
3589
- return identity.isCollection(this.contents) ? this.contents.hasIn(path7) : false;
3589
+ return identity.isCollection(this.contents) ? this.contents.hasIn(path8) : false;
3590
3590
  }
3591
3591
  /**
3592
3592
  * Sets a value in this document. For `!!set`, `value` needs to be a
@@ -3603,13 +3603,13 @@ var require_Document = __commonJS({
3603
3603
  * Sets a value in this document. For `!!set`, `value` needs to be a
3604
3604
  * boolean to add/remove the item from the set.
3605
3605
  */
3606
- setIn(path7, value) {
3607
- if (Collection.isEmptyPath(path7)) {
3606
+ setIn(path8, value) {
3607
+ if (Collection.isEmptyPath(path8)) {
3608
3608
  this.contents = value;
3609
3609
  } else if (this.contents == null) {
3610
- this.contents = Collection.collectionFromPath(this.schema, Array.from(path7), value);
3610
+ this.contents = Collection.collectionFromPath(this.schema, Array.from(path8), value);
3611
3611
  } else if (assertCollection(this.contents)) {
3612
- this.contents.setIn(path7, value);
3612
+ this.contents.setIn(path8, value);
3613
3613
  }
3614
3614
  }
3615
3615
  /**
@@ -5569,9 +5569,9 @@ var require_cst_visit = __commonJS({
5569
5569
  visit.BREAK = BREAK;
5570
5570
  visit.SKIP = SKIP;
5571
5571
  visit.REMOVE = REMOVE;
5572
- visit.itemAtPath = (cst, path7) => {
5572
+ visit.itemAtPath = (cst, path8) => {
5573
5573
  let item = cst;
5574
- for (const [field2, index] of path7) {
5574
+ for (const [field2, index] of path8) {
5575
5575
  const tok = item?.[field2];
5576
5576
  if (tok && "items" in tok) {
5577
5577
  item = tok.items[index];
@@ -5580,23 +5580,23 @@ var require_cst_visit = __commonJS({
5580
5580
  }
5581
5581
  return item;
5582
5582
  };
5583
- visit.parentCollection = (cst, path7) => {
5584
- const parent = visit.itemAtPath(cst, path7.slice(0, -1));
5585
- const field2 = path7[path7.length - 1][0];
5583
+ visit.parentCollection = (cst, path8) => {
5584
+ const parent = visit.itemAtPath(cst, path8.slice(0, -1));
5585
+ const field2 = path8[path8.length - 1][0];
5586
5586
  const coll = parent?.[field2];
5587
5587
  if (coll && "items" in coll)
5588
5588
  return coll;
5589
5589
  throw new Error("Parent collection not found");
5590
5590
  };
5591
- function _visit(path7, item, visitor) {
5592
- let ctrl = visitor(item, path7);
5591
+ function _visit(path8, item, visitor) {
5592
+ let ctrl = visitor(item, path8);
5593
5593
  if (typeof ctrl === "symbol")
5594
5594
  return ctrl;
5595
5595
  for (const field2 of ["key", "value"]) {
5596
5596
  const token = item[field2];
5597
5597
  if (token && "items" in token) {
5598
5598
  for (let i = 0; i < token.items.length; ++i) {
5599
- const ci = _visit(Object.freeze(path7.concat([[field2, i]])), token.items[i], visitor);
5599
+ const ci = _visit(Object.freeze(path8.concat([[field2, i]])), token.items[i], visitor);
5600
5600
  if (typeof ci === "number")
5601
5601
  i = ci - 1;
5602
5602
  else if (ci === BREAK)
@@ -5607,10 +5607,10 @@ var require_cst_visit = __commonJS({
5607
5607
  }
5608
5608
  }
5609
5609
  if (typeof ctrl === "function" && field2 === "key")
5610
- ctrl = ctrl(item, path7);
5610
+ ctrl = ctrl(item, path8);
5611
5611
  }
5612
5612
  }
5613
- return typeof ctrl === "function" ? ctrl(item, path7) : ctrl;
5613
+ return typeof ctrl === "function" ? ctrl(item, path8) : ctrl;
5614
5614
  }
5615
5615
  exports2.visit = visit;
5616
5616
  }
@@ -6912,14 +6912,14 @@ var require_parser = __commonJS({
6912
6912
  case "scalar":
6913
6913
  case "single-quoted-scalar":
6914
6914
  case "double-quoted-scalar": {
6915
- const fs8 = this.flowScalar(this.type);
6915
+ const fs9 = this.flowScalar(this.type);
6916
6916
  if (atNextItem || it.value) {
6917
- map2.items.push({ start, key: fs8, sep: [] });
6917
+ map2.items.push({ start, key: fs9, sep: [] });
6918
6918
  this.onKeyLine = true;
6919
6919
  } else if (it.sep) {
6920
- this.stack.push(fs8);
6920
+ this.stack.push(fs9);
6921
6921
  } else {
6922
- Object.assign(it, { key: fs8, sep: [] });
6922
+ Object.assign(it, { key: fs9, sep: [] });
6923
6923
  this.onKeyLine = true;
6924
6924
  }
6925
6925
  return;
@@ -7047,13 +7047,13 @@ var require_parser = __commonJS({
7047
7047
  case "scalar":
7048
7048
  case "single-quoted-scalar":
7049
7049
  case "double-quoted-scalar": {
7050
- const fs8 = this.flowScalar(this.type);
7050
+ const fs9 = this.flowScalar(this.type);
7051
7051
  if (!it || it.value)
7052
- fc.items.push({ start: [], key: fs8, sep: [] });
7052
+ fc.items.push({ start: [], key: fs9, sep: [] });
7053
7053
  else if (it.sep)
7054
- this.stack.push(fs8);
7054
+ this.stack.push(fs9);
7055
7055
  else
7056
- Object.assign(it, { key: fs8, sep: [] });
7056
+ Object.assign(it, { key: fs9, sep: [] });
7057
7057
  return;
7058
7058
  }
7059
7059
  case "flow-map-end":
@@ -8577,10 +8577,10 @@ function mergeDefs(...defs) {
8577
8577
  function cloneDef(schema) {
8578
8578
  return mergeDefs(schema._zod.def);
8579
8579
  }
8580
- function getElementAtPath(obj, path7) {
8581
- if (!path7)
8580
+ function getElementAtPath(obj, path8) {
8581
+ if (!path8)
8582
8582
  return obj;
8583
- return path7.reduce((acc, key) => acc?.[key], obj);
8583
+ return path8.reduce((acc, key) => acc?.[key], obj);
8584
8584
  }
8585
8585
  function promiseAllObject(promisesObj) {
8586
8586
  const keys = Object.keys(promisesObj);
@@ -8989,11 +8989,11 @@ function explicitlyAborted(x, startIndex = 0) {
8989
8989
  }
8990
8990
  return false;
8991
8991
  }
8992
- function prefixIssues(path7, issues) {
8992
+ function prefixIssues(path8, issues) {
8993
8993
  return issues.map((iss) => {
8994
8994
  var _a3;
8995
8995
  (_a3 = iss).path ?? (_a3.path = []);
8996
- iss.path.unshift(path7);
8996
+ iss.path.unshift(path8);
8997
8997
  return iss;
8998
8998
  });
8999
8999
  }
@@ -9140,16 +9140,16 @@ function flattenError(error51, mapper = (issue2) => issue2.message) {
9140
9140
  }
9141
9141
  function formatError(error51, mapper = (issue2) => issue2.message) {
9142
9142
  const fieldErrors = { _errors: [] };
9143
- const processError = (error52, path7 = []) => {
9143
+ const processError = (error52, path8 = []) => {
9144
9144
  for (const issue2 of error52.issues) {
9145
9145
  if (issue2.code === "invalid_union" && issue2.errors.length) {
9146
- issue2.errors.map((issues) => processError({ issues }, [...path7, ...issue2.path]));
9146
+ issue2.errors.map((issues) => processError({ issues }, [...path8, ...issue2.path]));
9147
9147
  } else if (issue2.code === "invalid_key") {
9148
- processError({ issues: issue2.issues }, [...path7, ...issue2.path]);
9148
+ processError({ issues: issue2.issues }, [...path8, ...issue2.path]);
9149
9149
  } else if (issue2.code === "invalid_element") {
9150
- processError({ issues: issue2.issues }, [...path7, ...issue2.path]);
9150
+ processError({ issues: issue2.issues }, [...path8, ...issue2.path]);
9151
9151
  } else {
9152
- const fullpath = [...path7, ...issue2.path];
9152
+ const fullpath = [...path8, ...issue2.path];
9153
9153
  if (fullpath.length === 0) {
9154
9154
  fieldErrors._errors.push(mapper(issue2));
9155
9155
  } else {
@@ -9176,17 +9176,17 @@ function formatError(error51, mapper = (issue2) => issue2.message) {
9176
9176
  }
9177
9177
  function treeifyError(error51, mapper = (issue2) => issue2.message) {
9178
9178
  const result = { errors: [] };
9179
- const processError = (error52, path7 = []) => {
9179
+ const processError = (error52, path8 = []) => {
9180
9180
  var _a3, _b;
9181
9181
  for (const issue2 of error52.issues) {
9182
9182
  if (issue2.code === "invalid_union" && issue2.errors.length) {
9183
- issue2.errors.map((issues) => processError({ issues }, [...path7, ...issue2.path]));
9183
+ issue2.errors.map((issues) => processError({ issues }, [...path8, ...issue2.path]));
9184
9184
  } else if (issue2.code === "invalid_key") {
9185
- processError({ issues: issue2.issues }, [...path7, ...issue2.path]);
9185
+ processError({ issues: issue2.issues }, [...path8, ...issue2.path]);
9186
9186
  } else if (issue2.code === "invalid_element") {
9187
- processError({ issues: issue2.issues }, [...path7, ...issue2.path]);
9187
+ processError({ issues: issue2.issues }, [...path8, ...issue2.path]);
9188
9188
  } else {
9189
- const fullpath = [...path7, ...issue2.path];
9189
+ const fullpath = [...path8, ...issue2.path];
9190
9190
  if (fullpath.length === 0) {
9191
9191
  result.errors.push(mapper(issue2));
9192
9192
  continue;
@@ -9218,8 +9218,8 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
9218
9218
  }
9219
9219
  function toDotPath(_path) {
9220
9220
  const segs = [];
9221
- const path7 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
9222
- for (const seg of path7) {
9221
+ const path8 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
9222
+ for (const seg of path8) {
9223
9223
  if (typeof seg === "number")
9224
9224
  segs.push(`[${seg}]`);
9225
9225
  else if (typeof seg === "symbol")
@@ -21911,13 +21911,13 @@ function resolveRef(ref, ctx) {
21911
21911
  if (!ref.startsWith("#")) {
21912
21912
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
21913
21913
  }
21914
- const path7 = ref.slice(1).split("/").filter(Boolean);
21915
- if (path7.length === 0) {
21914
+ const path8 = ref.slice(1).split("/").filter(Boolean);
21915
+ if (path8.length === 0) {
21916
21916
  return ctx.rootSchema;
21917
21917
  }
21918
21918
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
21919
- if (path7[0] === defsKey) {
21920
- const key = path7[1];
21919
+ if (path8[0] === defsKey) {
21920
+ const key = path8[1];
21921
21921
  if (!key || !ctx.defs[key]) {
21922
21922
  throw new Error(`Reference not found: ${ref}`);
21923
21923
  }
@@ -23279,11 +23279,11 @@ function parseShellScript(script, cwd) {
23279
23279
  parseUntil(scope, 0, false);
23280
23280
  return scope.commands;
23281
23281
  }
23282
- function resolveShellPath(dir, path7) {
23283
- if (path7.includes(SHELL_OPAQUE) || path7.startsWith("~")) return null;
23284
- if (!path7.startsWith("/") && dir === null) return null;
23282
+ function resolveShellPath(dir, path8) {
23283
+ if (path8.includes(SHELL_OPAQUE) || path8.startsWith("~")) return null;
23284
+ if (!path8.startsWith("/") && dir === null) return null;
23285
23285
  const parts = [];
23286
- for (const part of (path7.startsWith("/") ? path7 : `${dir}/${path7}`).split("/")) {
23286
+ for (const part of (path8.startsWith("/") ? path8 : `${dir}/${path8}`).split("/")) {
23287
23287
  if (part === "..") parts.pop();
23288
23288
  else if (part && part !== ".") parts.push(part);
23289
23289
  }
@@ -23637,8 +23637,8 @@ function parseSkillName(tool, input) {
23637
23637
  const skillName = parsedInput.skill ?? parsedInput.name;
23638
23638
  return typeof skillName === "string" && skillName ? skillName : null;
23639
23639
  }
23640
- function skillNameFromFilePath(path7) {
23641
- const parts = path7.split(/[\\/]+/).filter(Boolean);
23640
+ function skillNameFromFilePath(path8) {
23641
+ const parts = path8.split(/[\\/]+/).filter(Boolean);
23642
23642
  if (parts.length < 2 || parts[parts.length - 1].toLowerCase() !== "skill.md") return null;
23643
23643
  const skillName = parts[parts.length - 2];
23644
23644
  return /[*?[\]{}…$]/.test(skillName) || ["skills", ".system"].includes(skillName.toLowerCase()) ? null : skillName;
@@ -23648,19 +23648,19 @@ function skillFilePath(tool, input) {
23648
23648
  if (!READ_TOOL_NAMES.has(tool.toLowerCase())) return null;
23649
23649
  const parsedInput = typeof input === "string" ? safeJsonParse(input, null) : input;
23650
23650
  if (!isRecord(parsedInput)) return null;
23651
- const path7 = [parsedInput.path, parsedInput.filePath, parsedInput.file_path].find((value) => typeof value === "string" && value.length > 0);
23652
- return path7 && skillNameFromFilePath(path7) ? path7 : null;
23651
+ const path8 = [parsedInput.path, parsedInput.filePath, parsedInput.file_path].find((value) => typeof value === "string" && value.length > 0);
23652
+ return path8 && skillNameFromFilePath(path8) ? path8 : null;
23653
23653
  }
23654
23654
  function createCallDisplayMessage(message2) {
23655
23655
  if (message2.inputRequest) return { ...message2, type: "tool_call" };
23656
- const path7 = skillFilePath(message2.tool, message2.input);
23657
- const skillName = message2.server.toLowerCase() === "skills" ? message2.tool : path7 ? skillNameFromFilePath(path7) : parseSkillName(message2.tool, message2.input);
23656
+ const path8 = skillFilePath(message2.tool, message2.input);
23657
+ const skillName = message2.server.toLowerCase() === "skills" ? message2.tool : path8 ? skillNameFromFilePath(path8) : parseSkillName(message2.tool, message2.input);
23658
23658
  if (skillName === null) return { ...message2, type: "tool_call" };
23659
23659
  return {
23660
23660
  id: message2.id,
23661
23661
  type: "skill",
23662
23662
  skillName,
23663
- ...path7 ? { path: path7 } : {},
23663
+ ...path8 ? { path: path8 } : {},
23664
23664
  status: message2.status,
23665
23665
  timestamp: message2.timestamp
23666
23666
  };
@@ -23673,11 +23673,11 @@ function insertDisplayMessageByTimestamp(messages, message2) {
23673
23673
  }
23674
23674
 
23675
23675
  // ../shared/src/display-message/format.ts
23676
- function fileContentDiff(path7, content, action) {
23676
+ function fileContentDiff(path8, content, action) {
23677
23677
  const lines = content.replace(/\n$/, "").split("\n");
23678
23678
  return [
23679
- `--- ${action === "add" ? "/dev/null" : `a/${path7}`}`,
23680
- `+++ ${action === "add" ? `b/${path7}` : "/dev/null"}`,
23679
+ `--- ${action === "add" ? "/dev/null" : `a/${path8}`}`,
23680
+ `+++ ${action === "add" ? `b/${path8}` : "/dev/null"}`,
23681
23681
  action === "add" ? `@@ -0,0 +1,${lines.length} @@` : `@@ -1,${lines.length} +0,0 @@`,
23682
23682
  ...lines.map((line) => `${action === "add" ? "+" : "-"}${line}`)
23683
23683
  ].join("\n");
@@ -23704,7 +23704,7 @@ var LINE_COUNT_VALUE_FLAGS = /* @__PURE__ */ new Set([
23704
23704
  ]);
23705
23705
  var SCRIPT_ROOT = "/";
23706
23706
  var commandName = (argv) => argv[0].split("/").pop() ?? argv[0];
23707
- var fileWrites = ({ writes }) => writes.filter(({ path: path7 }) => path7 !== "/dev/null");
23707
+ var fileWrites = ({ writes }) => writes.filter(({ path: path8 }) => path8 !== "/dev/null");
23708
23708
  function shellCommands(command) {
23709
23709
  return parseShellScript(command, SCRIPT_ROOT).filter(({ wrapper }) => !wrapper);
23710
23710
  }
@@ -23716,8 +23716,8 @@ function skillNamesFromCommand(command) {
23716
23716
  if (!SKILL_READ_COMMANDS.has(name) || writesFiles(name, argv) || fileWrites(shellCommand).length > 0) return [];
23717
23717
  const paths = readCommandPaths(name, argv);
23718
23718
  const stdinTarget = shellCommand.stdinTarget?.replaceAll(SHELL_OPAQUE, "\u2026");
23719
- return (paths.length === 0 && stdinTarget ? [stdinTarget] : paths).flatMap((path7) => {
23720
- const resolved = path7.startsWith("~") ? path7 : resolveShellPath(shellCommand.dir, path7)?.replace(SCRIPT_ROOT, "");
23719
+ return (paths.length === 0 && stdinTarget ? [stdinTarget] : paths).flatMap((path8) => {
23720
+ const resolved = path8.startsWith("~") ? path8 : resolveShellPath(shellCommand.dir, path8)?.replace(SCRIPT_ROOT, "");
23721
23721
  const skillName = resolved && skillNameFromFilePath(resolved);
23722
23722
  return skillName ? [skillName] : [];
23723
23723
  });
@@ -24197,7 +24197,7 @@ Starts an ffmpeg screen recorder and returns only after ffmpeg has written the f
24197
24197
  Only one recording at a time. Re-running \`start\` while one is active fails - call \`stop\` first.
24198
24198
 
24199
24199
  ### \`replicas computer record stop\`
24200
- SIGINTs ffmpeg, verifies that the process has finalized the MP4, then prints the output path. A finalization timeout preserves the recording state so \`stop\` can be retried safely. Upload it with \`replicas media upload <path> --name "<name>"\` to share it.
24200
+ SIGINTs ffmpeg, verifies that the process has finalized the MP4, then prints the output path. A finalization timeout preserves the recording state so \`stop\` can be retried safely. To show the interaction in a PR or message, turn it into a short looping GIF with \`replicas media gif <path>\`, then upload with \`replicas media upload <path> --name "<name>"\`.
24201
24201
 
24202
24202
  ## Patterns
24203
24203
 
@@ -24242,9 +24242,10 @@ For tasks the user wants proof of:
24242
24242
  replicas computer record start /tmp/walkthrough.mp4
24243
24243
  # ... your work ...
24244
24244
  replicas computer record stop
24245
- replicas media upload /tmp/walkthrough.mp4 --name "Walkthrough recording"
24245
+ replicas media gif /tmp/walkthrough.mp4 --duration 8
24246
+ replicas media upload /tmp/walkthrough.gif /tmp/walkthrough.mp4 --name "Walkthrough" --name "Walkthrough recording"
24246
24247
  \`\`\`
24247
- Then embed the printed \`![\u2026](\u2026)\` line in your chat reply and follow \`MEDIA.md\` for any external destination.
24248
+ Then embed the printed \`![\u2026](\u2026)\` lines in your chat reply and follow \`MEDIA.md\` for any external destination: the GIF goes inline in PRs, and a grid of extracted frames never does.
24248
24249
 
24249
24250
  ## Failure modes
24250
24251
 
@@ -24886,7 +24887,7 @@ gh pr review 123 --request-changes --body "Changes needed"
24886
24887
 
24887
24888
  ## Media in GitHub workflows
24888
24889
 
24889
- Media delivery is owned by \`references/MEDIA.md\`. Read its GitHub section before adding screenshots, recordings, diagrams, or HTML reports to a pull request, issue, review comment, or commit; it covers the public links every image upload prints, recording previews, organization-only HTML links, and the required **View in Replicas** link.
24890
+ Media delivery is owned by \`references/MEDIA.md\`. Read it before adding screenshots, GIFs, recordings, diagrams, or HTML reports to a pull request, issue, review comment, or commit; it covers when to use a GIF instead of screenshots, the public links every image upload prints, recording previews, organization-only HTML links, and the required **View in Replicas** link.
24890
24891
  `;
24891
24892
  var GITHUB_ABILITY = {
24892
24893
  label: "GitHub",
@@ -25069,24 +25070,74 @@ var LINEAR_ABILITY = {
25069
25070
 
25070
25071
  // ../shared/src/default-skills/replicas-agent/abilities/media.ts
25071
25072
  var SECTION7 = `### Media
25072
- Deliver screenshots, screen recordings, generated diagrams, audio clips, and self-contained HTML pages to the Replicas dashboard and, when applicable, inline in the destination workflow.
25073
+ Deliver screenshots, GIFs, screen recordings, generated diagrams, audio clips, and self-contained HTML pages to the Replicas dashboard and, when applicable, inline in the destination workflow.
25073
25074
 
25074
25075
  **Reference:** \`references/MEDIA.md\`
25075
25076
 
25076
25077
  Use this when:
25077
25078
  - You are about to run \`replicas media upload\`; this reference owns the complete Replicas-first, destination-second workflow
25078
25079
  - You produce a screenshot, recording, generated image, or audio clip the user should see
25080
+ - You need to show motion or an interaction (an animation, a popup that moves, a flow) in a PR or message: make a short looping GIF with \`replicas media gif\`, never a grid of frames
25079
25081
  - You produce a self-contained HTML report that should be linked from a pull request
25080
25082
  - You record video output (browser automation, screen capture) \u2014 including the recommended aspect ratio and FPS
25081
25083
  - You need to embed media in a Slack/Linear/GitHub/GitLab message AND keep a referenceable copy in the Replicas dashboard`;
25082
- var REFERENCE7 = `# Media (Screenshots, Recordings, Audio, HTML)
25084
+ var REFERENCE7 = `# Media (Screenshots, GIFs, Recordings, Audio, HTML)
25083
25085
 
25084
- This is the single source of truth for delivering screenshots, screen recordings, generated diagrams, audio clips, and self-contained HTML pages through Replicas and external surfaces (Slack, Linear, GitHub, GitLab).
25086
+ This is the single source of truth for delivering screenshots, GIFs, screen recordings, generated diagrams, audio clips, and self-contained HTML pages through Replicas and external surfaces (Slack, Linear, GitHub, GitLab).
25085
25087
 
25086
25088
  ## Prerequisites
25087
25089
 
25088
25090
  The \`replicas\` CLI is pre-installed and authenticated in your workspace. No additional setup is needed.
25089
25091
 
25092
+ ## Screenshot, GIF, or recording
25093
+
25094
+ Pick the format from what the reader has to see:
25095
+
25096
+ - **Static UI** (a layout, a new button, an error message): still screenshots.
25097
+ - **Motion or an interaction** (an animation, a popup that moves or flickers, a drag, a short multi-step flow): a short looping GIF. It plays inline in GitHub, Slack, Linear, GitLab, and Replicas chat, with no click and no sign-in.
25098
+ - **A long walkthrough, or anything with sound**: the MP4 recording, shown as a GIF or poster that links to it (see GitHub below).
25099
+
25100
+ Never paste a contact sheet or grid of frames extracted from a recording, and never paste a run of separate frame screenshots. At PR width each frame is a thumbnail, and the reader can't tell what moved. Make a GIF instead.
25101
+
25102
+ ### Make a GIF from a recording
25103
+
25104
+ \`\`\`bash
25105
+ replicas computer record start /tmp/after.mp4 --fps 30
25106
+ # ... perform only the interaction ...
25107
+ replicas computer record stop
25108
+ replicas media gif /tmp/after.mp4 --start 1 --duration 6
25109
+ replicas media upload /tmp/after.gif --name "Language menu after fix"
25110
+ \`\`\`
25111
+
25112
+ \`replicas media gif <recording>\` takes any MP4, WebM, or MOV (desktop, Playwright, CDP screencast, or simulator recordings) and writes a looping GIF next to it (\`-o\` to choose the path). It starts at 12 fps and 960 px wide and lowers both until the file is under 5 MiB. GitHub's image proxy returns 404 for larger images, so a bigger GIF shows as a broken image in the PR.
25113
+
25114
+ - Keep it to the interaction, usually 3\u201310 seconds: \`--start\` and \`--duration\` (default 10, max 30).
25115
+ - When the change is small on a large screen, \`--crop w:h:x:y\` (recording pixels) to the area that changes so text stays readable. Use the same crop for the before and after GIFs.
25116
+ - If it reports the GIF is still too large, show less: a shorter \`--duration\` or a tighter \`--crop\`.
25117
+ - Upload the GIF like any image. Its printed \`![\u2026](\u2026)\` line is public and renders inline everywhere.
25118
+ - Older workspaces may not have \`replicas media gif\` yet (\`replicas media gif --help\` fails). Use the same ffmpeg settings directly; keep \`-ss\` and \`-t\` before \`-i\` (palettegen waits for its input to end, so trimming after \`-i\` never finishes), and if the file is over 5 MiB, lower \`fps\` and the width and run it again:
25119
+
25120
+ \`\`\`bash
25121
+ ffmpeg -v error -y -ss 1 -t 6 -i /tmp/after.mp4 \\
25122
+ -vf "fps=12,scale='min(960,iw)':-1:flags=lanczos,split[a][b];[a]palettegen=stats_mode=diff[p];[b][p]paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle" \\
25123
+ -loop 0 /tmp/after.gif
25124
+ \`\`\`
25125
+
25126
+ ffmpeg is baked into the workspace image. If it's missing, install it with \`sudo apt-get install -y ffmpeg\`.
25127
+
25128
+ ### Before and after
25129
+
25130
+ Record both with the same steps, window size, and crop. Say in one line what to watch for, then put the GIFs side by side in a table, or in labeled sections when they are wide:
25131
+
25132
+ \`\`\`markdown
25133
+ The menu should open on its trigger. Before, it jumps about 95px up in half the openings.
25134
+
25135
+ | Before | After |
25136
+ | --- | --- |
25137
+ | ![Language menu before fix](<before-gif-url>) | ![Language menu after fix](<after-gif-url>) |
25138
+ | [View in Replicas](<before-dashboard-url>) | [View in Replicas](<after-dashboard-url>) |
25139
+ \`\`\`
25140
+
25090
25141
  ## When to upload
25091
25142
 
25092
25143
  Upload to Replicas in these cases \u2014 and **only** these cases:
@@ -25113,7 +25164,7 @@ If none of these apply, don't upload.
25113
25164
  | Slack | \`files.getUploadURLExternal\` \u2192 raw-byte POST \u2192 \`files.completeUploadExternal\` | Uploaded file in the channel or thread |
25114
25165
  | Linear | \`fileUpload\` mutation \u2192 presigned upload \u2192 issue/comment mutation | Returned \`assetUrl\` in Markdown |
25115
25166
  | GitLab | Project \`/uploads\` endpoint with the original bytes | Returned GitLab Markdown |
25116
- | GitHub | Printed public embed for an image or recording poster | Printed embed, linked to the recording when needed |
25167
+ | GitHub | Printed public embed for an image, GIF, or recording poster | Printed embed, linked to the recording when needed |
25117
25168
 
25118
25169
  The dashboard link is the subtitle and jump-back path. It is never a substitute for the destination embed.
25119
25170
 
@@ -25318,7 +25369,7 @@ jq -n \\
25318
25369
  }' | linear_graphql
25319
25370
  \`\`\`
25320
25371
 
25321
- For a recording, upload the MP4 with its own MIME type, then upload a poster frame (\`ffmpeg -i /tmp/demo.mp4 -frames:v 1 /tmp/demo-poster.png\`) the same way as \`image/png\` and link it: \`[![Demo recording](<poster-asset-url>)](<recording-asset-url>)\`.
25372
+ To show motion, upload the GIF from \`replicas media gif\` this way as \`image/gif\` and embed its \`assetUrl\` like a screenshot. For a full recording, upload the MP4 with its own MIME type, then upload a poster frame (\`ffmpeg -i /tmp/demo.mp4 -frames:v 1 /tmp/demo-poster.png\`) the same way as \`image/png\` and link it: \`[![Demo recording](<poster-asset-url>)](<recording-asset-url>)\`.
25322
25373
 
25323
25374
  The same flow works for issue bodies (use \`issueCreate\` / \`issueUpdate\` with the same markdown body).
25324
25375
 
@@ -25343,10 +25394,10 @@ gh pr edit 456 --body-file /tmp/pr-body.md
25343
25394
  gh pr comment 456 --body-file /tmp/comment.md
25344
25395
  \`\`\`
25345
25396
 
25346
- Videos stay private, and GitHub does not play external videos inline. Upload the MP4 normally, generate a poster with \`ffmpeg -i /tmp/demo.mp4 -frames:v 1 /tmp/demo-poster.png\`, upload the poster like any image, and link its printed embed to the recording's **View in Replicas** URL:
25397
+ GitHub does not play external videos inline, so show motion with a GIF (see *Make a GIF from a recording* above); its printed embed animates inline like any image. Videos stay private. When the full recording is also worth watching, upload the MP4 too and link the GIF to the recording's **View in Replicas** URL. When a still is enough, use a poster instead (\`ffmpeg -i /tmp/demo.mp4 -frames:v 1 /tmp/demo-poster.png\`), uploaded like any image:
25347
25398
 
25348
25399
  \`\`\`markdown
25349
- [![Demo recording](<public-poster-url>)](<recording-dashboard-url>)
25400
+ [![Checkout flow](<public-gif-or-poster-url>)](<recording-dashboard-url>)
25350
25401
  \`\`\`
25351
25402
 
25352
25403
  Public links are unauthenticated bearer capabilities. Revoke with \`replicas media revoke <media-id>\` **only** when the user asks or the media should intentionally stop rendering; never as routine post-task cleanup, since revoking breaks every embed already posted on the PR.
@@ -25378,13 +25429,15 @@ const context = await browser.newContext({
25378
25429
 
25379
25430
  For \`ffmpeg\` screen captures, pass \`-r 30\` (or \`-r 60\`) to set the frame rate.
25380
25431
 
25432
+ Start recording just before the interaction and stop right after it, so the GIF made from it stays short.
25433
+
25381
25434
  ## Supported formats
25382
25435
 
25383
25436
  Auto-detected from the filename extension:
25384
25437
 
25385
25438
  | Extension | Kind |
25386
25439
  |---|---|
25387
- | \`png\`, \`jpg\`, \`jpeg\`, \`webp\` | image |
25440
+ | \`png\`, \`jpg\`, \`jpeg\`, \`webp\`, \`gif\` | image |
25388
25441
  | \`mp4\`, \`webm\` | video |
25389
25442
  | \`mp3\`, \`wav\` | audio |
25390
25443
  | \`html\`, \`htm\` | html |
@@ -25407,7 +25460,7 @@ replicas media upload chart-a.svg chart-b.svg --name "Latency chart" --name "Err
25407
25460
  var MEDIA_ABILITY = {
25408
25461
  label: "Media",
25409
25462
  description: "Deliver media to Replicas and destination workflows with inline uploads.",
25410
- bullet: "- Producing or showing the user any media \u2014 screenshots, screen recordings, generated images or diagrams, audio clips, or HTML recap pages \u2014 including in your Replicas chat reply, PR descriptions/comments, and other external platforms. Paste the printed `![\u2026](\u2026)` embeds in your chat reply, never only dashboard links, and upload the file itself to Slack, Linear, or GitLab",
25463
+ bullet: "- Producing or showing the user any media \u2014 screenshots, screen recordings, generated images or diagrams, audio clips, or HTML recap pages \u2014 including in your Replicas chat reply, PR descriptions/comments, and other external platforms. Paste the printed `![\u2026](\u2026)` embeds in your chat reply, never only dashboard links, and upload the file itself to Slack, Linear, or GitLab. To show motion, turn the recording into a short GIF with `replicas media gif` instead of a grid of frames",
25411
25464
  section: SECTION7,
25412
25465
  referenceFile: { name: "MEDIA.md", content: REFERENCE7 }
25413
25466
  };
@@ -26392,7 +26445,7 @@ function stripAttachmentNote(text3) {
26392
26445
  }
26393
26446
  return {
26394
26447
  content,
26395
- files: match ? [...match[1].matchAll(/^- (.+)$/gm)].map(([, path7]) => path7) : [],
26448
+ files: match ? [...match[1].matchAll(/^- (.+)$/gm)].map(([, path8]) => path8) : [],
26396
26449
  walkthroughs
26397
26450
  };
26398
26451
  }
@@ -27278,7 +27331,7 @@ function parseOpencodeEvents(events) {
27278
27331
  upsertDisplayMessage(messages, {
27279
27332
  id,
27280
27333
  type: "file_change",
27281
- changes: files.map((path7) => ({ path: path7, kind: "update" })),
27334
+ changes: files.map((path8) => ({ path: path8, kind: "update" })),
27282
27335
  status: "completed",
27283
27336
  timestamp
27284
27337
  });
@@ -28593,7 +28646,7 @@ var headlessAgentRequestSchema = external_exports.object({
28593
28646
  });
28594
28647
 
28595
28648
  // ../shared/src/cli-version.ts
28596
- var CLI_VERSION = "0.3.73";
28649
+ var CLI_VERSION = "0.3.74";
28597
28650
 
28598
28651
  // ../shared/src/version.ts
28599
28652
  function compareVersions(v1, v2) {
@@ -31074,8 +31127,8 @@ async function claudeAuthCommand(options = {}) {
31074
31127
  var CLIENT_ID = "1031625952748946";
31075
31128
  var AUTH_URL = "https://auth.meta.com";
31076
31129
  var API_KEY_URL = "https://api.meta.ai/muse-code/key";
31077
- async function post(path7, body) {
31078
- return fetch(`${AUTH_URL}${path7}`, {
31130
+ async function post(path8, body) {
31131
+ return fetch(`${AUTH_URL}${path8}`, {
31079
31132
  method: "POST",
31080
31133
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
31081
31134
  body
@@ -31271,7 +31324,7 @@ async function credentialsGetCommand(service, options) {
31271
31324
  });
31272
31325
  }
31273
31326
  async function credentialsSetCommand(service, options) {
31274
- const path7 = credentialsPath(service, options, true);
31327
+ const path8 = credentialsPath(service, options, true);
31275
31328
  const { org: org2, models, ...fields } = options;
31276
31329
  const secretField = service === "bedrock" ? "aws_secret_access_key" : "api_key";
31277
31330
  const body = {
@@ -31279,7 +31332,7 @@ async function credentialsSetCommand(service, options) {
31279
31332
  ...models ? { models: models.split(",").map((model2) => model2.trim()).filter(Boolean) } : {},
31280
31333
  [secretField]: await readSecret(service, secretField === "api_key" ? "API key" : "AWS secret access key")
31281
31334
  };
31282
- const response = await orgAuthenticatedFetch(path7, { method: "POST", body });
31335
+ const response = await orgAuthenticatedFetch(path8, { method: "POST", body });
31283
31336
  output(response, () => console.log(import_chalk17.default.green(`Saved ${scopeLabel(options)} ${service} credentials.`)));
31284
31337
  }
31285
31338
  async function credentialsRemoveCommand(service, options) {
@@ -32563,7 +32616,7 @@ async function canvasPutCommand(id, filename, options) {
32563
32616
 
32564
32617
  // src/commands/chat.ts
32565
32618
  var import_chalk23 = __toESM(require("chalk"));
32566
- var chatPath2 = (id, chatId, path7 = "") => `/v1/replica/${id}/chats/${chatId}${path7}`;
32619
+ var chatPath2 = (id, chatId, path8 = "") => `/v1/replica/${id}/chats/${chatId}${path8}`;
32567
32620
  function printQueue(queue) {
32568
32621
  if (queue.length === 0) {
32569
32622
  console.log("Queue is empty.");
@@ -32648,8 +32701,8 @@ async function chatQueueListCommand(id, chatId) {
32648
32701
  printQueue(queue);
32649
32702
  });
32650
32703
  }
32651
- async function mutateQueue(path7, method, body) {
32652
- output(await orgAuthenticatedFetch(path7, { method, body }), ({ queue }) => {
32704
+ async function mutateQueue(path8, method, body) {
32705
+ output(await orgAuthenticatedFetch(path8, { method, body }), ({ queue }) => {
32653
32706
  if (queue) printQueue(queue);
32654
32707
  else console.log("Done.");
32655
32708
  });
@@ -32662,7 +32715,7 @@ var chatQueueActionCommand = (action) => (id, chatId, messageId) => mutateQueue(
32662
32715
 
32663
32716
  // src/commands/plugins.ts
32664
32717
  var import_chalk24 = __toESM(require("chalk"));
32665
- var pluginsPath = async ({ env }, path7) => env ? `/v1/environments/${await resolveEnvironmentId(env)}/plugins${path7}` : `/v1/plugins${path7}`;
32718
+ var pluginsPath = async ({ env }, path8) => env ? `/v1/environments/${await resolveEnvironmentId(env)}/plugins${path8}` : `/v1/plugins${path8}`;
32666
32719
  async function pluginsListCommand(options) {
32667
32720
  output(await orgAuthenticatedFetch(await pluginsPath(options, "/connections")), ({ plugins: plugins2, environment: environment2 }) => {
32668
32721
  if (!options.all && environment2.length === 0) console.log("No plugins connected. Pass --all to list available plugins.");
@@ -33717,10 +33770,10 @@ var import_chalk27 = __toESM(require("chalk"));
33717
33770
 
33718
33771
  // src/lib/agent-api.ts
33719
33772
  var ENGINE_PORT = process.env.REPLICAS_ENGINE_PORT || "3737";
33720
- async function agentFetch(path7, options) {
33721
- return (await agentRequest(path7, options)).json();
33773
+ async function agentFetch(path8, options) {
33774
+ return (await agentRequest(path8, options)).json();
33722
33775
  }
33723
- async function agentRequest(path7, options) {
33776
+ async function agentRequest(path8, options) {
33724
33777
  const config3 = readAgentConfig();
33725
33778
  if (!config3) {
33726
33779
  throw new Error("Agent mode config not found");
@@ -33734,7 +33787,7 @@ async function agentRequest(path7, options) {
33734
33787
  ...options?.headers || {}
33735
33788
  };
33736
33789
  const body = options?.body === void 0 ? void 0 : isFormData ? options.body : JSON.stringify(options.body);
33737
- const response = await fetch(`${MONOLITH_URL}${path7}`, {
33790
+ const response = await fetch(`${MONOLITH_URL}${path8}`, {
33738
33791
  ...options,
33739
33792
  headers,
33740
33793
  body
@@ -33745,7 +33798,7 @@ async function agentRequest(path7, options) {
33745
33798
  }
33746
33799
  return response;
33747
33800
  }
33748
- async function engineFetch(path7, options) {
33801
+ async function engineFetch(path8, options) {
33749
33802
  const config3 = readAgentConfig();
33750
33803
  if (!config3) {
33751
33804
  throw new Error("Agent mode config not found");
@@ -33755,7 +33808,7 @@ async function engineFetch(path7, options) {
33755
33808
  "Content-Type": "application/json",
33756
33809
  ...options?.headers || {}
33757
33810
  };
33758
- const response = await fetch(`http://localhost:${ENGINE_PORT}${path7}`, {
33811
+ const response = await fetch(`http://localhost:${ENGINE_PORT}${path8}`, {
33759
33812
  ...options,
33760
33813
  headers,
33761
33814
  body: options?.body !== void 0 ? JSON.stringify(options.body) : void 0
@@ -34028,6 +34081,87 @@ async function mediaUrlCommand(mediaId) {
34028
34081
  output(response, ({ url: url2 }) => console.log(url2));
34029
34082
  }
34030
34083
 
34084
+ // src/commands/media-gif.ts
34085
+ var import_child_process4 = require("child_process");
34086
+ var import_fs8 = __toESM(require("fs"));
34087
+ var import_path7 = __toESM(require("path"));
34088
+ var import_util3 = require("util");
34089
+ var GIF_MAX_BYTES = 5 * 1024 * 1024;
34090
+ var GIF_ATTEMPTS = 4;
34091
+ var execFileAsync = (0, import_util3.promisify)(import_child_process4.execFile);
34092
+ function parseOption(value, fallback, flag, min, max) {
34093
+ const parsed = value === void 0 ? fallback : Number(value);
34094
+ if (!Number.isFinite(parsed) || parsed < min || parsed > max) {
34095
+ throw new Error(`${flag} must be between ${min} and ${max}, got '${value}'`);
34096
+ }
34097
+ return parsed;
34098
+ }
34099
+ function gifFilter({ fps, width, crop }) {
34100
+ return [
34101
+ crop && `crop=${crop}`,
34102
+ `fps=${fps}`,
34103
+ `scale='min(${width},iw)':-1:flags=lanczos`,
34104
+ // stats_mode=diff spends the palette on what moves, and diff_mode=rectangle only re-encodes changed areas.
34105
+ "split[a][b];[a]palettegen=stats_mode=diff[p];[b][p]paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle"
34106
+ ].filter(Boolean).join(",");
34107
+ }
34108
+ async function mediaGifCommand(input, options) {
34109
+ const source = import_path7.default.resolve(input);
34110
+ if (!import_fs8.default.existsSync(source) || !import_fs8.default.statSync(source).isFile()) throw new Error(`File not found: ${source}`);
34111
+ const target = import_path7.default.resolve(options.output ?? `${source.replace(/\.[^./]+$/, "")}.gif`);
34112
+ if (import_path7.default.extname(target).toLowerCase() !== ".gif") throw new Error("--output must end in .gif");
34113
+ if (target === source) throw new Error("--output must differ from the input file");
34114
+ if (options.crop && !/^\d+:\d+:\d+:\d+$/.test(options.crop)) {
34115
+ throw new Error(`--crop must be width:height:x:y in pixels of the recording, got '${options.crop}'`);
34116
+ }
34117
+ const start = parseOption(options.start, 0, "--start", 0, 24 * 60 * 60);
34118
+ const duration3 = parseOption(options.duration, 10, "--duration", 1, 30);
34119
+ const maxWidth = parseOption(options.width, 960, "--width", 200, 1280);
34120
+ const maxFps = parseOption(options.fps, 12, "--fps", 5, 20);
34121
+ const temp = import_path7.default.join(import_path7.default.dirname(target), `.${import_path7.default.basename(target, ".gif")}.${process.pid}.tmp.gif`);
34122
+ let fps = maxFps;
34123
+ let width = maxWidth;
34124
+ try {
34125
+ for (let attempt = 0; attempt < GIF_ATTEMPTS; attempt++) {
34126
+ fps = Math.max(5, Math.round(maxFps * 0.8 ** attempt));
34127
+ width = Math.round(maxWidth * 0.8 ** attempt);
34128
+ await execFileAsync("ffmpeg", [
34129
+ "-v",
34130
+ "error",
34131
+ "-y",
34132
+ "-ss",
34133
+ String(start),
34134
+ "-t",
34135
+ String(duration3),
34136
+ "-i",
34137
+ source,
34138
+ "-vf",
34139
+ gifFilter({ fps, width, crop: options.crop }),
34140
+ "-loop",
34141
+ "0",
34142
+ temp
34143
+ ], { timeout: 18e4 }).catch((error51) => {
34144
+ if (error51.code === "ENOENT") throw new Error("ffmpeg is not installed. Install it with `sudo apt-get install -y ffmpeg`.");
34145
+ throw new Error(`ffmpeg failed: ${error51.stderr?.trim() || error51.message}`);
34146
+ });
34147
+ const size = import_fs8.default.statSync(temp).size;
34148
+ if (size <= GIF_MAX_BYTES) {
34149
+ import_fs8.default.renameSync(temp, target);
34150
+ output({ path: target, size, fps, maxWidth: width, start, duration: duration3 }, () => {
34151
+ console.log(`GIF: ${target} (${(size / 1024 / 1024).toFixed(1)} MiB, ${fps} fps, up to ${width}px wide, ${duration3}s from ${start}s, loops)`);
34152
+ console.log(`Next: replicas media upload ${target} --name "<what it shows>", then paste the printed ![\u2026](\u2026) line in your reply and the PR.`);
34153
+ });
34154
+ return;
34155
+ }
34156
+ }
34157
+ } finally {
34158
+ import_fs8.default.rmSync(temp, { force: true });
34159
+ }
34160
+ throw new Error(
34161
+ `The GIF is still over GitHub's 5 MiB limit at ${fps} fps and ${width}px wide. Show less: a shorter --duration with --start at the interaction, or --crop to the area that changes.`
34162
+ );
34163
+ }
34164
+
34031
34165
  // src/commands/history.ts
34032
34166
  var import_node_fs2 = require("fs");
34033
34167
  var import_node_path2 = require("path");
@@ -34556,6 +34690,7 @@ if (isAgentMode()) {
34556
34690
  service.command("stop <name>").description("Stop a service and its whole process group").action(run(serviceStopCommand));
34557
34691
  service.command("list").description("List registered services and whether they are running").action(run(serviceListCommand));
34558
34692
  service.command("logs <name>").description("Print the last lines of a service log").option("-n, --lines <n>", "Number of lines to print (default 50)").action(run(serviceLogsCommand));
34693
+ media.command("gif <recording>").description("Turn a screen recording into a short looping GIF under GitHub's 5 MiB image limit, lowering fps and width until it fits").option("-o, --output <path>", "Output .gif path (default: the recording path with .gif)").option("--start <seconds>", "Where the GIF starts in the recording (default 0)").option("--duration <seconds>", "Length in seconds, 1-30 (default 10)").option("--width <px>", "Maximum width, 200-1280 (default 960)").option("--fps <n>", "Maximum frame rate, 5-20 (default 12)").option("--crop <w:h:x:y>", "Crop to the region that changes, in recording pixels, before scaling").action(run(mediaGifCommand));
34559
34694
  media.command("upload <files...>").description("Upload one or more image, video, audio, or HTML files. Multiple files are uploaded concurrently.").option("-k, --kind <kind>", "Media kind: image, video, audio, or html (inferred from extension if omitted)").option("-s, --session-id <id>", "Session ID to associate the asset with").option(
34560
34695
  "-n, --name <name>",
34561
34696
  "Required human-facing display name shown in the dashboard. Pass one --name per file, in order.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-cli",
3
- "version": "0.3.73",
3
+ "version": "0.3.74",
4
4
  "description": "CLI for managing Replicas workspaces - SSH into cloud dev environments with automatic port forwarding",
5
5
  "main": "dist/index.cjs",
6
6
  "bin": {