@superdoc-dev/cli 0.2.0-next.53 → 0.2.0-next.55

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.js +416 -89
  2. package/package.json +7 -7
package/dist/index.js CHANGED
@@ -1210,7 +1210,7 @@ var init_operation_definitions = __esm(() => {
1210
1210
  },
1211
1211
  insert: {
1212
1212
  memberPath: "insert",
1213
- description: "Insert content at a target position. Supports text (default), markdown, and html content types via the `type` field.",
1213
+ description: "Insert content at a target position, or at the end of the document when target is omitted. Supports text (default), markdown, and html content types via the `type` field.",
1214
1214
  expectedResult: "Returns a TextMutationReceipt with applied status; receipt reports NO_OP if the insertion point is invalid or content is empty.",
1215
1215
  requiresDocumentContext: true,
1216
1216
  metadata: mutationOperation({
@@ -1293,7 +1293,7 @@ var init_operation_definitions = __esm(() => {
1293
1293
  ...FORMAT_INLINE_ALIAS_OPERATION_DEFINITIONS,
1294
1294
  "styles.apply": {
1295
1295
  memberPath: "styles.apply",
1296
- description: "Apply document-level default style changes to the stylesheet (word/styles.xml). Targets docDefaults run properties with boolean patch semantics.",
1296
+ description: "Apply document-level default style changes to the stylesheet (word/styles.xml). Targets docDefaults run and paragraph channels with set-style patch semantics.",
1297
1297
  expectedResult: "Returns a StylesApplyReceipt with per-channel success/failure details for each property change.",
1298
1298
  requiresDocumentContext: true,
1299
1299
  metadata: mutationOperation({
@@ -8292,6 +8292,16 @@ function normalizeSessionType(value) {
8292
8292
  return "collab";
8293
8293
  return "local";
8294
8294
  }
8295
+ function normalizeUser(value) {
8296
+ const record = asRecord(value);
8297
+ if (!record)
8298
+ return;
8299
+ if (typeof record.name !== "string" || record.name.length === 0)
8300
+ return;
8301
+ if (typeof record.email !== "string")
8302
+ return;
8303
+ return { name: record.name, email: record.email };
8304
+ }
8295
8305
  function normalizeCollaborationProfile(value) {
8296
8306
  const record = asRecord(value);
8297
8307
  if (!record)
@@ -8301,6 +8311,8 @@ function normalizeCollaborationProfile(value) {
8301
8311
  const documentId = record.documentId;
8302
8312
  const tokenEnv = record.tokenEnv;
8303
8313
  const syncTimeoutMs = record.syncTimeoutMs;
8314
+ const onMissing = record.onMissing;
8315
+ const bootstrapSettlingMs = record.bootstrapSettlingMs;
8304
8316
  if (providerType !== "hocuspocus" && providerType !== "y-websocket")
8305
8317
  return;
8306
8318
  if (typeof url !== "string" || url.length === 0)
@@ -8312,28 +8324,39 @@ function normalizeCollaborationProfile(value) {
8312
8324
  if (syncTimeoutMs != null && (typeof syncTimeoutMs !== "number" || !Number.isFinite(syncTimeoutMs) || syncTimeoutMs <= 0)) {
8313
8325
  return;
8314
8326
  }
8327
+ if (onMissing != null && onMissing !== "seedFromDoc" && onMissing !== "blank" && onMissing !== "error") {
8328
+ return;
8329
+ }
8330
+ if (bootstrapSettlingMs != null && (typeof bootstrapSettlingMs !== "number" || !Number.isFinite(bootstrapSettlingMs) || bootstrapSettlingMs <= 0)) {
8331
+ return;
8332
+ }
8315
8333
  return {
8316
8334
  providerType,
8317
8335
  url,
8318
8336
  documentId,
8319
8337
  tokenEnv: typeof tokenEnv === "string" ? tokenEnv : undefined,
8320
- syncTimeoutMs: typeof syncTimeoutMs === "number" ? syncTimeoutMs : undefined
8338
+ syncTimeoutMs: typeof syncTimeoutMs === "number" ? syncTimeoutMs : undefined,
8339
+ onMissing,
8340
+ bootstrapSettlingMs: typeof bootstrapSettlingMs === "number" ? bootstrapSettlingMs : undefined
8321
8341
  };
8322
8342
  }
8323
8343
  function normalizeContextMetadata(metadata) {
8324
8344
  const sessionType = normalizeSessionType(metadata.sessionType);
8325
8345
  const collaboration = normalizeCollaborationProfile(metadata.collaboration);
8346
+ const user = normalizeUser(metadata.user);
8326
8347
  if (sessionType === "collab" && collaboration) {
8327
8348
  return {
8328
8349
  ...metadata,
8329
8350
  sessionType,
8330
- collaboration
8351
+ collaboration,
8352
+ user
8331
8353
  };
8332
8354
  }
8333
8355
  return {
8334
8356
  ...metadata,
8335
8357
  sessionType: "local",
8336
- collaboration: undefined
8358
+ collaboration: undefined,
8359
+ user
8337
8360
  };
8338
8361
  }
8339
8362
  function sleep(ms) {
@@ -8747,6 +8770,7 @@ function createInitialContextMetadata(io, paths, contextId, input) {
8747
8770
  revision: 0,
8748
8771
  sessionType,
8749
8772
  collaboration: sessionType === "collab" ? input.collaboration : undefined,
8773
+ user: input.user,
8750
8774
  openedAt: timestamp,
8751
8775
  updatedAt: timestamp,
8752
8776
  sourceSnapshot: input.sourceSnapshot
@@ -9196,6 +9220,14 @@ var init_cli_only_operation_definitions = __esm(() => {
9196
9220
  documentId: { type: "string" },
9197
9221
  url: { type: "string" }
9198
9222
  }
9223
+ },
9224
+ bootstrap: {
9225
+ type: "object",
9226
+ properties: {
9227
+ roomState: { type: "string" },
9228
+ bootstrapApplied: { type: "boolean" },
9229
+ bootstrapSource: { type: "string" }
9230
+ }
9199
9231
  }
9200
9232
  },
9201
9233
  required: ["contextId", "sessionType"]
@@ -10153,6 +10185,36 @@ function resolveSegmentPosition(targetOffset, segmentStart, segmentLength, docFr
10153
10185
  }
10154
10186
  return docFrom + (targetOffset - segmentStart);
10155
10187
  }
10188
+ function computeTextContentLength(blockNode) {
10189
+ let length = 0;
10190
+ const walk = (node2) => {
10191
+ if (node2.isText) {
10192
+ length += (node2.text ?? "").length;
10193
+ return;
10194
+ }
10195
+ if (node2.isLeaf) {
10196
+ length += 1;
10197
+ return;
10198
+ }
10199
+ let first2 = true;
10200
+ for (let i = 0;i < node2.childCount; i++) {
10201
+ const child = node2.child(i);
10202
+ if (child.isBlock && !first2)
10203
+ length += 1;
10204
+ walk(child);
10205
+ first2 = false;
10206
+ }
10207
+ };
10208
+ let first = true;
10209
+ for (let i = 0;i < blockNode.childCount; i++) {
10210
+ const child = blockNode.child(i);
10211
+ if (child.isBlock && !first)
10212
+ length += 1;
10213
+ walk(child);
10214
+ first = false;
10215
+ }
10216
+ return length;
10217
+ }
10156
10218
  function resolveTextRangeInBlock(blockNode, blockPos, range) {
10157
10219
  if (range.start < 0 || range.end < range.start)
10158
10220
  return null;
@@ -10210,6 +10272,21 @@ function resolveTextRangeInBlock(blockNode, blockPos, range) {
10210
10272
  return { from: fromPos, to: toPos };
10211
10273
  }
10212
10274
 
10275
+ // ../../packages/super-editor/src/document-api-adapters/helpers/text-mutation-resolution.ts
10276
+ function readTextAtResolvedRange(editor, range) {
10277
+ return editor.state.doc.textBetween(range.from, range.to, `
10278
+ `, OBJECT_REPLACEMENT_CHAR);
10279
+ }
10280
+ function buildTextMutationResolution(input) {
10281
+ return {
10282
+ ...input.requestedTarget ? { requestedTarget: input.requestedTarget } : {},
10283
+ target: input.target,
10284
+ range: { from: input.range.from, to: input.range.to },
10285
+ text: input.text
10286
+ };
10287
+ }
10288
+ var OBJECT_REPLACEMENT_CHAR = "";
10289
+
10213
10290
  // ../../packages/super-editor/src/document-api-adapters/helpers/adapter-utils.ts
10214
10291
  function findTextBlockCandidates(index, blockId) {
10215
10292
  return index.candidates.filter((candidate) => candidate.nodeId === blockId && isTextBlockCandidate(candidate));
@@ -10236,22 +10313,100 @@ function resolveTextTarget(editor, target) {
10236
10313
  return null;
10237
10314
  return resolveTextRangeInBlock(block.node, block.pos, target.range);
10238
10315
  }
10316
+ function collectTopLevelPositions(doc) {
10317
+ const positions = new Set;
10318
+ let offset = 0;
10319
+ for (let i = 0;i < doc.childCount; i++) {
10320
+ positions.add(offset);
10321
+ offset += doc.child(i).nodeSize;
10322
+ }
10323
+ return positions;
10324
+ }
10239
10325
  function resolveDefaultInsertTarget(editor) {
10240
10326
  const index = getBlockIndex(editor);
10241
- const firstParagraph = index.candidates.find((candidate) => candidate.nodeType === "paragraph" && isTextBlockCandidate(candidate));
10242
- const firstTextBlock = firstParagraph ?? index.candidates.find((candidate) => isTextBlockCandidate(candidate));
10243
- if (!firstTextBlock)
10327
+ const doc = editor.state.doc;
10328
+ const topLevelPositions = collectTopLevelPositions(doc);
10329
+ for (let i = index.candidates.length - 1;i >= 0; i--) {
10330
+ const candidate = index.candidates[i];
10331
+ if (topLevelPositions.has(candidate.pos) && isTextBlockCandidate(candidate)) {
10332
+ const textLength = computeTextContentLength(candidate.node);
10333
+ const range = resolveTextRangeInBlock(candidate.node, candidate.pos, { start: textLength, end: textLength });
10334
+ if (!range)
10335
+ continue;
10336
+ return {
10337
+ kind: "text-block",
10338
+ target: {
10339
+ kind: "text",
10340
+ blockId: candidate.nodeId,
10341
+ range: { start: textLength, end: textLength }
10342
+ },
10343
+ range
10344
+ };
10345
+ }
10346
+ }
10347
+ if (doc.content.size > 0) {
10348
+ return { kind: "structural-end", insertPos: doc.content.size };
10349
+ }
10350
+ return null;
10351
+ }
10352
+ function insertParagraphAtEnd(editor, pos, text, applyMeta) {
10353
+ const schema = editor.state.schema;
10354
+ const textNode = schema.text(text);
10355
+ const paragraph = schema.nodes.paragraph.create(null, textNode);
10356
+ const tr = editor.state.tr;
10357
+ tr.insert(pos, paragraph);
10358
+ if (applyMeta)
10359
+ applyMeta(tr);
10360
+ editor.dispatch(tr);
10361
+ }
10362
+ function resolveWriteTarget(editor, request) {
10363
+ const requestedTarget = request.target;
10364
+ if (request.kind === "insert" && !request.target) {
10365
+ const fallback = resolveDefaultInsertTarget(editor);
10366
+ if (!fallback)
10367
+ return null;
10368
+ if (fallback.kind === "structural-end") {
10369
+ const pos = fallback.insertPos;
10370
+ const syntheticRange = { from: pos, to: pos };
10371
+ const syntheticTarget = { kind: "text", blockId: "", range: { start: 0, end: 0 } };
10372
+ return {
10373
+ requestedTarget,
10374
+ effectiveTarget: syntheticTarget,
10375
+ range: syntheticRange,
10376
+ resolution: buildTextMutationResolution({
10377
+ requestedTarget,
10378
+ target: syntheticTarget,
10379
+ range: syntheticRange,
10380
+ text: ""
10381
+ }),
10382
+ structuralEnd: true
10383
+ };
10384
+ }
10385
+ const text2 = readTextAtResolvedRange(editor, fallback.range);
10386
+ return {
10387
+ requestedTarget,
10388
+ effectiveTarget: fallback.target,
10389
+ range: fallback.range,
10390
+ resolution: buildTextMutationResolution({
10391
+ requestedTarget,
10392
+ target: fallback.target,
10393
+ range: fallback.range,
10394
+ text: text2
10395
+ })
10396
+ };
10397
+ }
10398
+ const target = request.target;
10399
+ if (!target)
10244
10400
  return null;
10245
- const range = resolveTextRangeInBlock(firstTextBlock.node, firstTextBlock.pos, { start: 0, end: 0 });
10401
+ const range = resolveTextTarget(editor, target);
10246
10402
  if (!range)
10247
10403
  return null;
10404
+ const text = readTextAtResolvedRange(editor, range);
10248
10405
  return {
10249
- target: {
10250
- kind: "text",
10251
- blockId: firstTextBlock.nodeId,
10252
- range: { start: 0, end: 0 }
10253
- },
10254
- range
10406
+ requestedTarget,
10407
+ effectiveTarget: target,
10408
+ range,
10409
+ resolution: buildTextMutationResolution({ requestedTarget, target, range, text })
10255
10410
  };
10256
10411
  }
10257
10412
  function addDiagnostic(diagnostics, message) {
@@ -24670,21 +24825,6 @@ var init_executor = __esm(() => {
24670
24825
  TEXT_STYLE_KEYS = ["color", "fontSize", "letterSpacing", "vertAlign", "position"];
24671
24826
  });
24672
24827
 
24673
- // ../../packages/super-editor/src/document-api-adapters/helpers/text-mutation-resolution.ts
24674
- function readTextAtResolvedRange(editor, range) {
24675
- return editor.state.doc.textBetween(range.from, range.to, `
24676
- `, OBJECT_REPLACEMENT_CHAR);
24677
- }
24678
- function buildTextMutationResolution(input) {
24679
- return {
24680
- ...input.requestedTarget ? { requestedTarget: input.requestedTarget } : {},
24681
- target: input.target,
24682
- range: { from: input.range.from, to: input.range.to },
24683
- text: input.text
24684
- };
24685
- }
24686
- var OBJECT_REPLACEMENT_CHAR = "";
24687
-
24688
24828
  // ../../node_modules/.pnpm/bail@2.0.2/node_modules/bail/index.js
24689
24829
  function bail(error) {
24690
24830
  if (error) {
@@ -70722,39 +70862,6 @@ function normalizeFormatLocator(input) {
70722
70862
  };
70723
70863
  return { target };
70724
70864
  }
70725
- function resolveWriteTarget(editor, request) {
70726
- const requestedTarget = request.target;
70727
- if (request.kind === "insert" && !request.target) {
70728
- const fallback = resolveDefaultInsertTarget(editor);
70729
- if (!fallback)
70730
- return null;
70731
- const text6 = readTextAtResolvedRange(editor, fallback.range);
70732
- return {
70733
- requestedTarget,
70734
- effectiveTarget: fallback.target,
70735
- range: fallback.range,
70736
- resolution: buildTextMutationResolution({
70737
- requestedTarget,
70738
- target: fallback.target,
70739
- range: fallback.range,
70740
- text: text6
70741
- })
70742
- };
70743
- }
70744
- const target = request.target;
70745
- if (!target)
70746
- return null;
70747
- const range = resolveTextTarget(editor, target);
70748
- if (!range)
70749
- return null;
70750
- const text5 = readTextAtResolvedRange(editor, range);
70751
- return {
70752
- requestedTarget,
70753
- effectiveTarget: target,
70754
- range,
70755
- resolution: buildTextMutationResolution({ requestedTarget, target, range, text: text5 })
70756
- };
70757
- }
70758
70865
  function mapPlanReceiptToTextReceipt(_receipt, resolution) {
70759
70866
  return { success: true, resolution };
70760
70867
  }
@@ -70829,6 +70936,16 @@ function writeWrapper(editor, request, options) {
70829
70936
  if (options?.dryRun) {
70830
70937
  return { success: true, resolution: resolved.resolution };
70831
70938
  }
70939
+ if (resolved.structuralEnd && normalizedRequest.kind === "insert") {
70940
+ const insertPos = resolved.range.from;
70941
+ const text5 = normalizedRequest.text ?? "";
70942
+ const receipt3 = executeDomainCommand(editor, () => {
70943
+ const meta = mode === "tracked" ? applyTrackedMutationMeta : applyDirectMutationMeta;
70944
+ insertParagraphAtEnd(editor, insertPos, text5, meta);
70945
+ return true;
70946
+ }, { expectedRevision: options?.expectedRevision });
70947
+ return mapPlanReceiptToTextReceipt(receipt3, resolved.resolution);
70948
+ }
70832
70949
  const stepId = v4();
70833
70950
  let op;
70834
70951
  let stepDef;
@@ -70989,8 +71106,14 @@ function insertStructuredWrapper(editor, input, options) {
70989
71106
  if (!fallback) {
70990
71107
  throw new DocumentApiAdapterError("TARGET_NOT_FOUND", "No default insertion point available.");
70991
71108
  }
70992
- resolvedRange = fallback.range;
70993
- effectiveTarget = fallback.target;
71109
+ if (fallback.kind === "structural-end") {
71110
+ const pos = fallback.insertPos;
71111
+ resolvedRange = { from: pos, to: pos };
71112
+ effectiveTarget = { kind: "text", blockId: "", range: { start: 0, end: 0 } };
71113
+ } else {
71114
+ resolvedRange = fallback.range;
71115
+ effectiveTarget = fallback.target;
71116
+ }
70994
71117
  }
70995
71118
  const resolution = buildTextMutationResolution({
70996
71119
  requestedTarget: target,
@@ -83665,18 +83788,35 @@ function parseCollaborationInput(value) {
83665
83788
  if ("params" in value) {
83666
83789
  throw new CliError("VALIDATION_ERROR", "collaboration.params is not supported in v1.");
83667
83790
  }
83668
- const allowedKeys = new Set(["providerType", "url", "documentId", "tokenEnv", "syncTimeoutMs"]);
83791
+ const allowedKeys = new Set([
83792
+ "providerType",
83793
+ "url",
83794
+ "documentId",
83795
+ "tokenEnv",
83796
+ "syncTimeoutMs",
83797
+ "onMissing",
83798
+ "bootstrapSettlingMs"
83799
+ ]);
83669
83800
  for (const key of Object.keys(value)) {
83670
83801
  if (!allowedKeys.has(key)) {
83671
83802
  throw new CliError("VALIDATION_ERROR", `collaboration.${key} is not supported.`);
83672
83803
  }
83673
83804
  }
83805
+ let onMissing;
83806
+ if (value.onMissing != null) {
83807
+ if (value.onMissing !== "seedFromDoc" && value.onMissing !== "blank" && value.onMissing !== "error") {
83808
+ throw new CliError("VALIDATION_ERROR", 'collaboration.onMissing must be "seedFromDoc", "blank", or "error".');
83809
+ }
83810
+ onMissing = value.onMissing;
83811
+ }
83674
83812
  return {
83675
83813
  providerType: normalizeProviderType(value.providerType, "collaboration.providerType"),
83676
83814
  url: expectNonEmptyString(value.url, "collaboration.url").trim(),
83677
83815
  documentId: value.documentId != null ? expectNonEmptyString(value.documentId, "collaboration.documentId") : undefined,
83678
83816
  tokenEnv: expectOptionalEnvVarName(value.tokenEnv, "collaboration.tokenEnv"),
83679
- syncTimeoutMs: expectOptionalPositiveNumber(value.syncTimeoutMs, "collaboration.syncTimeoutMs")
83817
+ syncTimeoutMs: expectOptionalPositiveNumber(value.syncTimeoutMs, "collaboration.syncTimeoutMs"),
83818
+ onMissing,
83819
+ bootstrapSettlingMs: expectOptionalPositiveNumber(value.bootstrapSettlingMs, "collaboration.bootstrapSettlingMs")
83680
83820
  };
83681
83821
  }
83682
83822
  function resolveCollaborationProfile(input, sessionId) {
@@ -83686,7 +83826,9 @@ function resolveCollaborationProfile(input, sessionId) {
83686
83826
  url: input.url,
83687
83827
  documentId,
83688
83828
  tokenEnv: input.tokenEnv,
83689
- syncTimeoutMs: input.syncTimeoutMs
83829
+ syncTimeoutMs: input.syncTimeoutMs,
83830
+ onMissing: input.onMissing,
83831
+ bootstrapSettlingMs: input.bootstrapSettlingMs
83690
83832
  };
83691
83833
  }
83692
83834
  function resolveCollaborationToken(profile) {
@@ -83786,6 +83928,119 @@ var init_collaboration = __esm(() => {
83786
83928
  ENV_VAR_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
83787
83929
  });
83788
83930
 
83931
+ // src/lib/bootstrap.ts
83932
+ function detectRoomState(ydoc) {
83933
+ const fragment = ydoc.getXmlFragment("supereditor");
83934
+ if (fragment.length > 0)
83935
+ return "populated";
83936
+ const metaMap = ydoc.getMap("meta");
83937
+ for (const [key, value] of metaMap.entries()) {
83938
+ if (key === "bootstrap") {
83939
+ const marker = value;
83940
+ if (marker && marker.source !== "pending")
83941
+ return "populated";
83942
+ continue;
83943
+ }
83944
+ return "populated";
83945
+ }
83946
+ return "empty";
83947
+ }
83948
+ function resolveBootstrapDecision(roomState, onMissing, hasDoc) {
83949
+ if (roomState === "populated")
83950
+ return { action: "join" };
83951
+ switch (onMissing) {
83952
+ case "seedFromDoc":
83953
+ return { action: "seed", source: hasDoc ? "doc" : "blank" };
83954
+ case "blank":
83955
+ return { action: "seed", source: "blank" };
83956
+ case "error":
83957
+ return { action: "error", reason: 'Collaboration room is empty and onMissing is set to "error".' };
83958
+ }
83959
+ }
83960
+ function writeBootstrapMarker(ydoc, source) {
83961
+ const metaMap = ydoc.getMap("meta");
83962
+ const marker = {
83963
+ version: 1,
83964
+ clientId: ydoc.clientID,
83965
+ seededAt: new Date().toISOString(),
83966
+ source
83967
+ };
83968
+ metaMap.set("bootstrap", marker);
83969
+ }
83970
+ function readBootstrapMarker(ydoc) {
83971
+ return ydoc.getMap("meta").get("bootstrap");
83972
+ }
83973
+ function snapshotCompetitor(marker) {
83974
+ return {
83975
+ observedOtherClientId: marker.clientId,
83976
+ observedSource: marker.source,
83977
+ observedAt: new Date().toISOString()
83978
+ };
83979
+ }
83980
+ function observeCompetitor(ydoc) {
83981
+ const myClientId = ydoc.clientID;
83982
+ const metaMap = ydoc.getMap("meta");
83983
+ let competitor = null;
83984
+ const handler2 = (event) => {
83985
+ if (!event.keysChanged.has("bootstrap"))
83986
+ return;
83987
+ const marker = metaMap.get("bootstrap");
83988
+ if (marker && marker.clientId !== myClientId && !competitor) {
83989
+ competitor = snapshotCompetitor(marker);
83990
+ }
83991
+ };
83992
+ metaMap.observe(handler2);
83993
+ return {
83994
+ getCompetitor: () => competitor,
83995
+ dispose: () => metaMap.unobserve(handler2)
83996
+ };
83997
+ }
83998
+ function sleep2(ms) {
83999
+ if (ms <= 0)
84000
+ return Promise.resolve();
84001
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
84002
+ }
84003
+ async function claimBootstrap(ydoc, settlingMs, jitterMs = DEFAULT_BOOTSTRAP_JITTER_MS) {
84004
+ await sleep2(Math.floor(Math.random() * jitterMs));
84005
+ const metaMap = ydoc.getMap("meta");
84006
+ metaMap.set("bootstrap", {
84007
+ version: 1,
84008
+ clientId: ydoc.clientID,
84009
+ seededAt: new Date().toISOString(),
84010
+ source: "pending"
84011
+ });
84012
+ const observer = observeCompetitor(ydoc);
84013
+ try {
84014
+ await sleep2(settlingMs);
84015
+ const competitor = observer.getCompetitor();
84016
+ if (competitor)
84017
+ return { granted: false, competitor };
84018
+ const marker = readBootstrapMarker(ydoc);
84019
+ if (marker?.clientId === ydoc.clientID) {
84020
+ return { granted: true };
84021
+ }
84022
+ return {
84023
+ granted: false,
84024
+ competitor: marker ? snapshotCompetitor(marker) : { observedOtherClientId: 0, observedSource: "unknown", observedAt: new Date().toISOString() }
84025
+ };
84026
+ } finally {
84027
+ observer.dispose();
84028
+ }
84029
+ }
84030
+ async function detectBootstrapRace(ydoc, observeMs = POST_SEED_OBSERVE_MS) {
84031
+ const observer = observeCompetitor(ydoc);
84032
+ try {
84033
+ await sleep2(observeMs);
84034
+ const competitor = observer.getCompetitor();
84035
+ if (competitor)
84036
+ return { raceSuspected: true, competitor };
84037
+ return { raceSuspected: false };
84038
+ } finally {
84039
+ observer.dispose();
84040
+ }
84041
+ }
84042
+ var DEFAULT_BOOTSTRAP_SETTLING_MS = 1500, DEFAULT_BOOTSTRAP_JITTER_MS = 150, POST_SEED_OBSERVE_MS = 200;
84043
+
83789
84044
  // src/lib/document.ts
83790
84045
  import { readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
83791
84046
  import { createHash as createHash2 } from "node:crypto";
@@ -83879,10 +84134,11 @@ async function openDocument(doc, io, options = {}) {
83879
84134
  const isTest = false;
83880
84135
  editor = await EditorRuntime.open(Buffer.from(source), {
83881
84136
  documentId: options.documentId ?? meta.path ?? "blank.docx",
83882
- user: { id: "cli", name: "CLI" },
84137
+ user: options.user ? { name: options.user.name, email: options.user.email, image: null } : { id: "cli", name: "CLI" },
83883
84138
  ...isTest ? { telemetry: { enabled: false } } : {},
83884
84139
  ydoc: options.ydoc,
83885
84140
  ...options.collaborationProvider != null ? { collaborationProvider: options.collaborationProvider } : {},
84141
+ ...options.isNewFile != null ? { isNewFile: options.isNewFile } : {},
83886
84142
  ...passThroughEditorOpts
83887
84143
  });
83888
84144
  } catch (error) {
@@ -83939,18 +84195,48 @@ async function openDocument(doc, io, options = {}) {
83939
84195
  }
83940
84196
  };
83941
84197
  }
83942
- async function openCollaborativeDocument(doc, io, profile) {
84198
+ async function openCollaborativeDocument(doc, io, profile, options = {}) {
83943
84199
  const runtime = createCollaborationRuntime(profile);
83944
84200
  try {
83945
84201
  await runtime.waitForSync();
83946
- const opened = await openDocument(doc, io, {
84202
+ const onMissing = profile.onMissing ?? "seedFromDoc";
84203
+ let finalRoomState = detectRoomState(runtime.ydoc);
84204
+ let decision = resolveBootstrapDecision(finalRoomState, onMissing, doc != null);
84205
+ if (decision.action === "seed") {
84206
+ const claim = await claimBootstrap(runtime.ydoc, profile.bootstrapSettlingMs ?? DEFAULT_BOOTSTRAP_SETTLING_MS);
84207
+ if (!claim.granted) {
84208
+ finalRoomState = detectRoomState(runtime.ydoc);
84209
+ decision = { action: "join" };
84210
+ }
84211
+ }
84212
+ if (decision.action === "error") {
84213
+ throw new CliError("COLLABORATION_ROOM_EMPTY", decision.reason);
84214
+ }
84215
+ const shouldSeed = decision.action === "seed";
84216
+ const docForEditor = shouldSeed ? doc : undefined;
84217
+ const opened = await openDocument(docForEditor, io, {
83947
84218
  documentId: profile.documentId,
83948
84219
  ydoc: runtime.ydoc,
83949
- collaborationProvider: runtime.provider
84220
+ collaborationProvider: runtime.provider,
84221
+ isNewFile: shouldSeed,
84222
+ user: options.user
83950
84223
  });
84224
+ let raceDetection;
84225
+ if (shouldSeed) {
84226
+ writeBootstrapMarker(runtime.ydoc, decision.source);
84227
+ raceDetection = await detectBootstrapRace(runtime.ydoc);
84228
+ }
84229
+ const bootstrap = {
84230
+ roomState: finalRoomState,
84231
+ bootstrapApplied: shouldSeed,
84232
+ bootstrapSource: shouldSeed ? decision.source : undefined,
84233
+ raceSuspected: raceDetection?.raceSuspected,
84234
+ raceCompetitor: raceDetection?.raceSuspected ? raceDetection.competitor : undefined
84235
+ };
83951
84236
  return {
83952
84237
  editor: opened.editor,
83953
84238
  meta: opened.meta,
84239
+ bootstrap,
83954
84240
  dispose() {
83955
84241
  try {
83956
84242
  opened.dispose();
@@ -83966,7 +84252,7 @@ async function openCollaborativeDocument(doc, io, profile) {
83966
84252
  }
83967
84253
  async function openSessionDocument(doc, io, metadata, options = {}) {
83968
84254
  if (metadata.sessionType !== "collab") {
83969
- return openDocument(doc, io);
84255
+ return openDocument(doc, io, { user: metadata.user });
83970
84256
  }
83971
84257
  if (!metadata.collaboration) {
83972
84258
  throw new CliError("COMMAND_FAILED", "Session is marked as collaborative but has no collaboration profile.");
@@ -83981,11 +84267,12 @@ async function openSessionDocument(doc, io, metadata, options = {}) {
83981
84267
  sessionType: metadata.sessionType,
83982
84268
  collaboration: metadata.collaboration,
83983
84269
  sourcePath: metadata.sourcePath,
83984
- workingDocPath: metadata.workingDocPath
84270
+ workingDocPath: metadata.workingDocPath,
84271
+ user: metadata.user
83985
84272
  };
83986
84273
  return options.collabSessionPool.acquire(sessionId, doc, metadataForPool, io);
83987
84274
  }
83988
- return openCollaborativeDocument(doc, io, metadata.collaboration);
84275
+ return openCollaborativeDocument(doc, io, metadata.collaboration, { user: metadata.user });
83989
84276
  }
83990
84277
  async function getFileChecksum(path2) {
83991
84278
  let bytes;
@@ -85445,7 +85732,7 @@ async function executeReadOperation(request) {
85445
85732
  opened2.dispose();
85446
85733
  }
85447
85734
  }
85448
- const opened = await openDocument(paths.workingDocPath, context.io);
85735
+ const opened = await openDocument(paths.workingDocPath, context.io, { user: metadata.user });
85449
85736
  try {
85450
85737
  const result = invokeOperation(opened.editor, operationId, input);
85451
85738
  const document4 = {
@@ -85635,7 +85922,7 @@ async function executeMutationOperation(request) {
85635
85922
  opened2.dispose();
85636
85923
  }
85637
85924
  }
85638
- const opened = await openDocument(paths.workingDocPath, context.io);
85925
+ const opened = await openDocument(paths.workingDocPath, context.io, { user: metadata.user });
85639
85926
  try {
85640
85927
  const result = invokeOperation2(opened.editor, operationId, input, invokeOptions);
85641
85928
  const document4 = {
@@ -86344,7 +86631,7 @@ function deriveOptionSpecs(params3) {
86344
86631
  }
86345
86632
  return specs;
86346
86633
  }
86347
- var DOC_PARAM, SESSION_PARAM, OUT_PARAM, FORCE_PARAM, DRY_RUN_PARAM, CHANGE_MODE_PARAM, EXPECTED_REVISION_PARAM, AGENT_HIDDEN_PARAM_NAMES, OPERATION_CONSTRAINTS, PARAM_FLAG_OVERRIDES, PARAM_SCHEMA_OVERRIDES, PARAM_EXCLUSIONS, TEXT_TARGET_FLAT_PARAMS, INSERT_FLAT_PARAMS, LIST_TARGET_FLAT_PARAMS, FORMAT_OPERATION_IDS, EXTRA_CLI_PARAMS, CLI_ONLY_METADATA, CLI_OPERATION_METADATA, CLI_OPERATION_OPTION_SPECS;
86634
+ var DOC_PARAM, SESSION_PARAM, OUT_PARAM, FORCE_PARAM, DRY_RUN_PARAM, CHANGE_MODE_PARAM, EXPECTED_REVISION_PARAM, USER_NAME_PARAM, USER_EMAIL_PARAM, AGENT_HIDDEN_PARAM_NAMES, OPERATION_CONSTRAINTS, PARAM_FLAG_OVERRIDES, PARAM_SCHEMA_OVERRIDES, PARAM_EXCLUSIONS, TEXT_TARGET_FLAT_PARAMS, INSERT_FLAT_PARAMS, LIST_TARGET_FLAT_PARAMS, FORMAT_OPERATION_IDS, EXTRA_CLI_PARAMS, CLI_ONLY_METADATA, CLI_OPERATION_METADATA, CLI_OPERATION_OPTION_SPECS;
86348
86635
  var init_operation_params = __esm(() => {
86349
86636
  init_src();
86350
86637
  init_operation_set();
@@ -86375,6 +86662,18 @@ var init_operation_params = __esm(() => {
86375
86662
  type: "number",
86376
86663
  agentVisible: false
86377
86664
  };
86665
+ USER_NAME_PARAM = {
86666
+ name: "userName",
86667
+ kind: "flag",
86668
+ flag: "user-name",
86669
+ type: "string"
86670
+ };
86671
+ USER_EMAIL_PARAM = {
86672
+ name: "userEmail",
86673
+ kind: "flag",
86674
+ flag: "user-email",
86675
+ type: "string"
86676
+ };
86378
86677
  AGENT_HIDDEN_PARAM_NAMES = new Set(["out", "expectedRevision", "changeMode", "dryRun"]);
86379
86678
  OPERATION_CONSTRAINTS = {
86380
86679
  "doc.find": {
@@ -86501,7 +86800,11 @@ var init_operation_params = __esm(() => {
86501
86800
  { name: "collabDocumentId", kind: "flag", flag: "collab-document-id", type: "string" },
86502
86801
  { name: "collabUrl", kind: "flag", flag: "collab-url", type: "string" },
86503
86802
  { name: "contentOverride", kind: "flag", flag: "content-override", type: "string" },
86504
- { name: "overrideType", kind: "flag", flag: "override-type", type: "string" }
86803
+ { name: "overrideType", kind: "flag", flag: "override-type", type: "string" },
86804
+ { name: "onMissing", kind: "flag", flag: "on-missing", type: "string" },
86805
+ { name: "bootstrapSettlingMs", kind: "flag", flag: "bootstrap-settling-ms", type: "number" },
86806
+ USER_NAME_PARAM,
86807
+ USER_EMAIL_PARAM
86505
86808
  ],
86506
86809
  constraints: null
86507
86810
  },
@@ -87800,6 +88103,10 @@ async function runOpen(tokens, context) {
87800
88103
  const collabDocumentId = getStringOption(parsed, "collab-document-id");
87801
88104
  const contentOverride = getStringOption(parsed, "content-override");
87802
88105
  const overrideType = getStringOption(parsed, "override-type");
88106
+ const onMissing = getStringOption(parsed, "on-missing");
88107
+ const bootstrapSettlingMs = getNumberOption(parsed, "bootstrap-settling-ms");
88108
+ const userName = getStringOption(parsed, "user-name");
88109
+ const userEmail = getStringOption(parsed, "user-email");
87803
88110
  if (contentOverride != null && !overrideType) {
87804
88111
  throw new CliError("INVALID_ARGUMENT", "open: --content-override requires --override-type.");
87805
88112
  }
@@ -87809,6 +88116,9 @@ async function runOpen(tokens, context) {
87809
88116
  if (overrideType && !VALID_OVERRIDE_TYPES.has(overrideType)) {
87810
88117
  throw new CliError("INVALID_ARGUMENT", `open: --override-type must be one of: markdown, html, text. Got "${overrideType}".`);
87811
88118
  }
88119
+ if (onMissing != null && !VALID_ON_MISSING.has(onMissing)) {
88120
+ throw new CliError("INVALID_ARGUMENT", `open: --on-missing must be one of: seedFromDoc, blank, error. Got "${onMissing}".`);
88121
+ }
87812
88122
  if (collaborationPayload != null && (collabUrl || collabDocumentId)) {
87813
88123
  throw new CliError("INVALID_ARGUMENT", "open: do not combine --collaboration-json with --collab-url / --collab-document-id.");
87814
88124
  }
@@ -87817,18 +88127,32 @@ async function runOpen(tokens, context) {
87817
88127
  }
87818
88128
  let collaborationInput;
87819
88129
  if (collaborationPayload != null) {
87820
- collaborationInput = parseCollaborationInput(collaborationPayload);
88130
+ if (typeof collaborationPayload !== "object" || Array.isArray(collaborationPayload)) {
88131
+ throw new CliError("VALIDATION_ERROR", "open: --collaboration-json must be a JSON object.");
88132
+ }
88133
+ const payload = collaborationPayload;
88134
+ if (onMissing != null && !("onMissing" in payload))
88135
+ payload.onMissing = onMissing;
88136
+ if (bootstrapSettlingMs != null && !("bootstrapSettlingMs" in payload))
88137
+ payload.bootstrapSettlingMs = bootstrapSettlingMs;
88138
+ collaborationInput = parseCollaborationInput(payload);
87821
88139
  } else if (collabUrl) {
87822
88140
  collaborationInput = parseCollaborationInput({
87823
88141
  providerType: "hocuspocus",
87824
88142
  url: collabUrl,
87825
- documentId: collabDocumentId
88143
+ documentId: collabDocumentId,
88144
+ ...onMissing != null ? { onMissing } : {},
88145
+ ...bootstrapSettlingMs != null ? { bootstrapSettlingMs } : {}
87826
88146
  });
87827
88147
  } else if (collabDocumentId) {
87828
88148
  throw new CliError("MISSING_REQUIRED", "open: --collab-document-id requires --collab-url.");
87829
88149
  }
87830
88150
  const collaboration = collaborationInput ? resolveCollaborationProfile(collaborationInput, sessionId) : undefined;
87831
88151
  const sessionType = collaboration ? "collab" : "local";
88152
+ if (!collaboration && (onMissing != null || bootstrapSettlingMs != null)) {
88153
+ throw new CliError("INVALID_ARGUMENT", "open: --on-missing and --bootstrap-settling-ms require collaboration mode (--collaboration-json or --collab-url).");
88154
+ }
88155
+ const user = userName != null || userEmail != null ? { name: userName ?? "CLI", email: userEmail ?? "" } : undefined;
87832
88156
  const editorOpenOptions = {};
87833
88157
  if (contentOverride != null && overrideType) {
87834
88158
  if (overrideType === "markdown") {
@@ -87854,10 +88178,8 @@ async function runOpen(tokens, context) {
87854
88178
  revision: existing.revision
87855
88179
  });
87856
88180
  }
87857
- if (collaboration && doc == null) {
87858
- throw new CliError("MISSING_REQUIRED", "open: a document path is required when using collaboration.");
87859
- }
87860
- const opened = collaboration ? await openCollaborativeDocument(doc, context.io, collaboration) : await openDocument(doc, context.io, { editorOpenOptions });
88181
+ const opened = collaboration ? await openCollaborativeDocument(doc, context.io, collaboration, { user }) : await openDocument(doc, context.io, { editorOpenOptions, user });
88182
+ const bootstrap = "bootstrap" in opened ? opened.bootstrap : undefined;
87861
88183
  let adoptedToHostPool = false;
87862
88184
  try {
87863
88185
  const output = await exportToPath(opened.editor, paths.workingDocPath, true);
@@ -87868,7 +88190,8 @@ async function runOpen(tokens, context) {
87868
88190
  sourcePath,
87869
88191
  sourceSnapshot,
87870
88192
  sessionType,
87871
- collaboration
88193
+ collaboration,
88194
+ user
87872
88195
  });
87873
88196
  await writeContextMetadata(paths, metadata);
87874
88197
  await setActiveSessionId(metadata.contextId);
@@ -87890,6 +88213,7 @@ async function runOpen(tokens, context) {
87890
88213
  dirty: metadata.dirty,
87891
88214
  sessionType: metadata.sessionType,
87892
88215
  collaboration: metadata.collaboration,
88216
+ bootstrap,
87893
88217
  openedAt: metadata.openedAt,
87894
88218
  updatedAt: metadata.updatedAt
87895
88219
  },
@@ -87902,7 +88226,7 @@ async function runOpen(tokens, context) {
87902
88226
  }
87903
88227
  }, undefined, sessionId);
87904
88228
  }
87905
- var VALID_OVERRIDE_TYPES;
88229
+ var VALID_OVERRIDE_TYPES, VALID_ON_MISSING;
87906
88230
  var init_open = __esm(() => {
87907
88231
  init_args();
87908
88232
  init_collaboration();
@@ -87912,6 +88236,7 @@ var init_open = __esm(() => {
87912
88236
  init_operation_args();
87913
88237
  init_session();
87914
88238
  VALID_OVERRIDE_TYPES = new Set(["markdown", "html", "text"]);
88239
+ VALID_ON_MISSING = new Set(["seedFromDoc", "blank", "error"]);
87915
88240
  });
87916
88241
 
87917
88242
  // src/commands/save.ts
@@ -94374,7 +94699,9 @@ function profileToKey(profile) {
94374
94699
  url: profile.url,
94375
94700
  documentId: profile.documentId,
94376
94701
  tokenEnv: profile.tokenEnv ?? null,
94377
- syncTimeoutMs: profile.syncTimeoutMs ?? null
94702
+ syncTimeoutMs: profile.syncTimeoutMs ?? null,
94703
+ onMissing: profile.onMissing ?? null,
94704
+ bootstrapSettlingMs: profile.bootstrapSettlingMs ?? null
94378
94705
  });
94379
94706
  }
94380
94707
  function buildFingerprint(metadata) {
@@ -94417,7 +94744,7 @@ class InMemoryCollaborationSessionPool {
94417
94744
  await this.disposeSession(sessionId);
94418
94745
  }
94419
94746
  const profile = metadata.collaboration;
94420
- const opened = await this.openCollaborative(docPath, io, profile);
94747
+ const opened = await this.openCollaborative(docPath, io, profile, { user: metadata.user });
94421
94748
  const created = {
94422
94749
  opened,
94423
94750
  fingerprint,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@superdoc-dev/cli",
3
- "version": "0.2.0-next.53",
3
+ "version": "0.2.0-next.55",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "superdoc": "./dist/index.js"
@@ -20,8 +20,8 @@
20
20
  "@types/node": "22.19.2",
21
21
  "typescript": "^5.9.2",
22
22
  "@superdoc/document-api": "0.0.1",
23
- "superdoc": "1.16.0",
24
23
  "@superdoc/pm-adapter": "0.0.0",
24
+ "superdoc": "1.16.0",
25
25
  "@superdoc/super-editor": "0.0.1"
26
26
  },
27
27
  "module": "src/index.ts",
@@ -29,11 +29,11 @@
29
29
  "access": "public"
30
30
  },
31
31
  "optionalDependencies": {
32
- "@superdoc-dev/cli-darwin-arm64": "0.2.0-next.53",
33
- "@superdoc-dev/cli-darwin-x64": "0.2.0-next.53",
34
- "@superdoc-dev/cli-linux-x64": "0.2.0-next.53",
35
- "@superdoc-dev/cli-windows-x64": "0.2.0-next.53",
36
- "@superdoc-dev/cli-linux-arm64": "0.2.0-next.53"
32
+ "@superdoc-dev/cli-darwin-arm64": "0.2.0-next.55",
33
+ "@superdoc-dev/cli-darwin-x64": "0.2.0-next.55",
34
+ "@superdoc-dev/cli-linux-x64": "0.2.0-next.55",
35
+ "@superdoc-dev/cli-linux-arm64": "0.2.0-next.55",
36
+ "@superdoc-dev/cli-windows-x64": "0.2.0-next.55"
37
37
  },
38
38
  "scripts": {
39
39
  "dev": "bun run src/index.ts",