bun-docx 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +16 -6
  2. package/dist/index.js +551 -106
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -184,17 +184,19 @@ docx edit FILE --at LOCATOR <content> # LOCATOR = pN | pN:S-
184
184
  docx delete FILE --at LOCATOR # LOCATOR = pN | pN-pM | tN | sN | tN:rRcC:pK (cell paragraph)
185
185
  docx sections FILE [--at LOCATOR] [--columns N] [--type T] [--orientation O] [--size SIZE] [--margins M] # LOCATOR = pN-pM | pN (wrap a range in N columns) | sN (edit one section's columns/type/page geometry). Multi-column layout AND page setup live HERE. PAGE GEOMETRY (margins/orientation/size) with NO --at applies to the WHOLE document (every section); --at sN targets one. Columns/type need --at.
186
186
  docx styles set-default-font FILE "Font Name" [--size N] [--all] # document-wide font: sets styles.xml docDefaults + theme major/minor; --all also repoints styles/runs that pin their own font
187
- docx replace FILE PATTERN REPLACEMENT [--regex] [--ignore-case] [--all] [--limit N] [--current | --baseline] [--exact] [--track] [--dry-run]
187
+ docx replace FILE PATTERN REPLACEMENT [--at pN] [--regex] [--ignore-case] [--all] [--limit N] [--current | --baseline] [--exact] [--track] [--dry-run]
188
188
  # Keeps the run's formatting (bold/font) and any tabs — the no-rebuild way to fill a
189
189
  # formatted/tabbed template line (e.g. "**Org Name**⇥Date"); don't hand-build --runs to refill it.
190
- # A span edit (find edit --at pN:S-E --text NEW) does the same by locator.
190
+ # --at pN (or a cell paragraph tT:rRcC:pN) CONFINES the replace to one paragraph — use it when the
191
+ # SAME placeholder repeats across the doc (a résumé's "City, State" in every entry) and you want THE
192
+ # one in a specific paragraph, instead of find → edit --at pN:S-E span surgery. Batch entries take "at" too.
191
193
 
192
194
  # Batch — apply many changes from ONE read (no re-reading between edits). Keys
193
195
  # on each JSONL line mirror the command's flags; all locators address the doc as
194
196
  # read. insert/edit also accept --batch - to read JSONL from stdin.
195
197
  docx edit FILE --batch fills.jsonl # { at, <one of: text|clear|markdown|runs|code|task>, style?, … }
196
198
  docx insert FILE --batch additions.jsonl # { after|before, <content>, style?, color?, … }
197
- docx replace FILE --batch script.jsonl # { pattern, replacement, regex?, all?, limit?, … } applied in order
199
+ docx replace FILE --batch script.jsonl # { pattern, replacement, at?, regex?, all?, limit?, … } applied in order ("at" scopes that entry to one paragraph)
198
200
  docx delete FILE --batch drop.jsonl # { at } per line — whole blocks (pN/tN/cell), resolved live-first
199
201
 
200
202
  # All four of insert/edit/delete/replace accept --track to record that one
@@ -283,14 +285,22 @@ docx tables unmerge FILE --at tN:rRcC
283
285
  docx tables borders FILE --at tN [--style single|double|none] [--size N] [--color HEX]
284
286
 
285
287
  docx track-changes on|off FILE
288
+ docx track-changes list FILE [--json]
286
289
  docx track-changes accept FILE (--at tcN [--at tcM ...] | --at revN | --all)
287
290
  docx track-changes reject FILE (--at tcN [--at tcM ...] | --at revN | --all)
288
- # A del+ins REPLACE pair shares a "group": "revN" in `list`; `--at revN` accepts/rejects
289
- # both halves in one call (no re-list between them tcN ids renumber after each accept).
291
+ docx track-changes apply FILE [--accept H ...] [--reject H ...]
292
+ # `list` defaults to a text table, one LOGICAL change per line (revN collapses a del+ins
293
+ # pair onto one line); `--json` for the raw array. A del+ins REPLACE pair shares a
294
+ # "group": "revN"; `--at revN` accepts/rejects both halves in one call.
295
+ # To FINALIZE a review (accept some, reject the rest), use `apply` — it takes both decision
296
+ # lists in ONE call, resolved against the original ids, so nothing renumbers mid-operation
297
+ # and the file is never left half-finalized. Doing it as separate accept then reject calls
298
+ # renumbers the ids between them. After a subset accept/reject/apply, the confirmation
299
+ # re-lists what remains with its renumbered handles.
290
300
  ```
291
301
 
292
302
  > **One rule to memorize: addressing an existing thing is always `--at`.**
293
- > `comments reply/resolve/delete`, `footnotes/endnotes edit/delete`, `images extract/replace/delete`, `hyperlinks replace/delete`, `tables *`, `track-changes accept/reject`, `edit`, and `delete` all take `--at LOCATOR`. The exceptions are positional or directional by nature: `insert` uses `--after`/`--before LOCATOR` (or `--at-start`/`--at-end` for the document boundaries, no locator); `read` slices with `--from`/`--to LOCATOR`; `wc` takes a positional `[LOCATOR]`; `find`/`replace` take a positional `QUERY`/`PATTERN`. `images extract --to DIR` is an *output directory*, not a locator.
303
+ > `comments reply/resolve/delete`, `footnotes/endnotes edit/delete`, `images extract/replace/delete`, `hyperlinks replace/delete`, `tables *`, `track-changes accept/reject`, `edit`, and `delete` all take `--at LOCATOR`. The exceptions are positional or directional by nature: `insert` uses `--after`/`--before LOCATOR` (or `--at-start`/`--at-end` for the document boundaries, no locator); `read` slices with `--from`/`--to LOCATOR`; `wc` takes a positional `[LOCATOR]`; `find`/`replace` take a positional `QUERY`/`PATTERN` (and `replace` accepts an optional `--at pN` to *confine* the substitution to one paragraph). `images extract --to DIR` is an *output directory*, not a locator.
294
304
 
295
305
  ## Output contract
296
306
 
package/dist/index.js CHANGED
@@ -18088,9 +18088,25 @@ function previewTrackedChanges(document2, target, verb) {
18088
18088
  }
18089
18089
  function applyTrackedChanges(document2, target, verb) {
18090
18090
  const targets = resolveTargets(document2, target);
18091
- const records = targets.map((found) => recordFor(found, verb));
18092
- const affectedNotes = collectAffectedNotes(targets);
18093
- for (const found of [...targets].reverse()) {
18091
+ return applyResolvedTargets(document2, targets.map((found) => ({ found, verb })));
18092
+ }
18093
+ function applyTrackedDecisions(document2, accepts, rejects) {
18094
+ const acceptTargets = resolveTargets(document2, accepts).map((found) => ({
18095
+ found,
18096
+ verb: "accept"
18097
+ }));
18098
+ const rejectTargets = resolveTargets(document2, rejects).map((found) => ({
18099
+ found,
18100
+ verb: "reject"
18101
+ }));
18102
+ return applyResolvedTargets(document2, [...acceptTargets, ...rejectTargets]);
18103
+ }
18104
+ function applyResolvedTargets(document2, decisions) {
18105
+ const order = collectTrackedChanges(document2);
18106
+ const sorted = [...decisions].sort((left, right) => indexOfFound(order, left.found) - indexOfFound(order, right.found));
18107
+ const records = sorted.map(({ found, verb }) => recordFor(found, verb));
18108
+ const affectedNotes = collectAffectedNotes(sorted.map(({ found }) => found));
18109
+ for (const { found, verb } of [...sorted].reverse()) {
18094
18110
  if (verb === "accept")
18095
18111
  applyAccept(found);
18096
18112
  else
@@ -18100,6 +18116,9 @@ function applyTrackedChanges(document2, target, verb) {
18100
18116
  applyNotePairing(document2, affectedNotes);
18101
18117
  return records;
18102
18118
  }
18119
+ function indexOfFound(inventory, found) {
18120
+ return inventory.findIndex((change) => change.node === found.node);
18121
+ }
18103
18122
  function resolveTargets(document2, target) {
18104
18123
  const allChanges = collectTrackedChanges(document2);
18105
18124
  if (target === "all")
@@ -18602,6 +18621,9 @@ class TrackChanges {
18602
18621
  reject(target) {
18603
18622
  return applyTrackedChanges(this.document, target, "reject");
18604
18623
  }
18624
+ apply(accepts, rejects) {
18625
+ return applyTrackedDecisions(this.document, accepts, rejects);
18626
+ }
18605
18627
  applyInsertion(paragraph, authorFlag, allocator) {
18606
18628
  const mintMeta = this.metaMinter(authorFlag, allocator);
18607
18629
  paragraph.children = wrapContiguousTrackable(paragraph.children, (runs) => /* @__PURE__ */ jsxDEV(Ins, {
@@ -33807,6 +33829,13 @@ function NormalStyle() {
33807
33829
  ]
33808
33830
  }, undefined, true, undefined, this);
33809
33831
  }
33832
+ function HeadingThemeFonts() {
33833
+ return /* @__PURE__ */ jsxDEV(w.rFonts, {
33834
+ "w-asciiTheme": "majorHAnsi",
33835
+ "w-hAnsiTheme": "majorHAnsi",
33836
+ "w-cstheme": "majorBidi"
33837
+ }, undefined, false, undefined, this);
33838
+ }
33810
33839
  function TitleStyle() {
33811
33840
  return /* @__PURE__ */ jsxDEV(w.style, {
33812
33841
  "w-type": "paragraph",
@@ -33829,10 +33858,7 @@ function TitleStyle() {
33829
33858
  }, undefined, false, undefined, this),
33830
33859
  /* @__PURE__ */ jsxDEV(w.rPr, {
33831
33860
  children: [
33832
- /* @__PURE__ */ jsxDEV(w.rFonts, {
33833
- "w-ascii": "Calibri Light",
33834
- "w-hAnsi": "Calibri Light"
33835
- }, undefined, false, undefined, this),
33861
+ /* @__PURE__ */ jsxDEV(HeadingThemeFonts, {}, undefined, false, undefined, this),
33836
33862
  /* @__PURE__ */ jsxDEV(w.color, {
33837
33863
  "w-val": "1F3864"
33838
33864
  }, undefined, false, undefined, this),
@@ -33866,10 +33892,7 @@ function SubtitleStyle() {
33866
33892
  }, undefined, false, undefined, this),
33867
33893
  /* @__PURE__ */ jsxDEV(w.rPr, {
33868
33894
  children: [
33869
- /* @__PURE__ */ jsxDEV(w.rFonts, {
33870
- "w-ascii": "Calibri Light",
33871
- "w-hAnsi": "Calibri Light"
33872
- }, undefined, false, undefined, this),
33895
+ /* @__PURE__ */ jsxDEV(HeadingThemeFonts, {}, undefined, false, undefined, this),
33873
33896
  /* @__PURE__ */ jsxDEV(w.color, {
33874
33897
  "w-val": "5A5A5A"
33875
33898
  }, undefined, false, undefined, this),
@@ -33919,10 +33942,7 @@ function HeadingStyle({
33919
33942
  }, undefined, true, undefined, this),
33920
33943
  /* @__PURE__ */ jsxDEV(w.rPr, {
33921
33944
  children: [
33922
- /* @__PURE__ */ jsxDEV(w.rFonts, {
33923
- "w-ascii": "Calibri Light",
33924
- "w-hAnsi": "Calibri Light"
33925
- }, undefined, false, undefined, this),
33945
+ /* @__PURE__ */ jsxDEV(HeadingThemeFonts, {}, undefined, false, undefined, this),
33926
33946
  bold2 && /* @__PURE__ */ jsxDEV(w.b, {}, undefined, false, undefined, this),
33927
33947
  italic && /* @__PURE__ */ jsxDEV(w.i, {}, undefined, false, undefined, this),
33928
33948
  color && /* @__PURE__ */ jsxDEV(w.color, {
@@ -90938,8 +90958,7 @@ function computeSectionStarts(blocks) {
90938
90958
  continue;
90939
90959
  const entry = {
90940
90960
  block: blocks[index2],
90941
- breakIndex: index2,
90942
- isTrailing: index2 === blocks.length - 1
90961
+ breakIndex: index2
90943
90962
  };
90944
90963
  const group = starts.get(sectionStart) ?? [];
90945
90964
  group.push(entry);
@@ -90949,12 +90968,7 @@ function computeSectionStarts(blocks) {
90949
90968
  return starts;
90950
90969
  }
90951
90970
  function renderSectionStart(entry, blocks, ctx) {
90952
- const marginalLines = ctx.marginalsBySection.get(entry.block.id);
90953
- if (entry.isTrailing) {
90954
- return marginalLines && marginalLines.length > 0 ? marginalLines.join(`
90955
- `) : null;
90956
- }
90957
- return renderSectionBreak(entry.block, governedRange(blocks, entry.breakIndex), marginalLines);
90971
+ return renderSectionBreak(entry.block, governedRange(blocks, entry.breakIndex), ctx.marginalsBySection.get(entry.block.id));
90958
90972
  }
90959
90973
  function computeGoverningColumns(blocks) {
90960
90974
  const map3 = new Map;
@@ -92148,6 +92162,55 @@ var init_render2 = __esm(() => {
92148
92162
  };
92149
92163
  });
92150
92164
 
92165
+ // src/cli/replace/scope.ts
92166
+ function validateScopeShape(at) {
92167
+ let parsed;
92168
+ try {
92169
+ parsed = parseLocator(at);
92170
+ } catch (error) {
92171
+ if (error instanceof LocatorParseError) {
92172
+ throw new ScopeError("INVALID_LOCATOR", error.message);
92173
+ }
92174
+ throw error;
92175
+ }
92176
+ if (!isParagraphLocator(parsed)) {
92177
+ throw new ScopeError("INVALID_LOCATOR", `--at scope must be a single paragraph (pN) or cell paragraph (tT:rRcC:pN), got "${at}" \u2014 replace targets text within one paragraph`);
92178
+ }
92179
+ return at;
92180
+ }
92181
+ function isParagraphLocator(parsed) {
92182
+ if (parsed.kind === "block")
92183
+ return parsed.blockId[0] === "p";
92184
+ let inner2 = parsed;
92185
+ while (inner2?.kind === "cell")
92186
+ inner2 = inner2.inner;
92187
+ return inner2?.kind === "block" && inner2.blockId[0] === "p";
92188
+ }
92189
+ function resolveReplaceScope(document4, at) {
92190
+ const blockId = validateScopeShape(at);
92191
+ try {
92192
+ document4.body.resolveBlock(at);
92193
+ } catch (error) {
92194
+ throw new ScopeError("BLOCK_NOT_FOUND", error.message);
92195
+ }
92196
+ return blockId;
92197
+ }
92198
+ function matchesInScope(matches, blockId) {
92199
+ return matches.filter((match) => match.blockId === blockId);
92200
+ }
92201
+ var ScopeError;
92202
+ var init_scope = __esm(() => {
92203
+ init_core2();
92204
+ ScopeError = class ScopeError extends Error {
92205
+ code;
92206
+ constructor(code3, message) {
92207
+ super(message);
92208
+ this.code = code3;
92209
+ this.name = "ScopeError";
92210
+ }
92211
+ };
92212
+ });
92213
+
92151
92214
  // src/cli/replace/batch.ts
92152
92215
  async function runReplaceBatch(filePath, batchSource, values2) {
92153
92216
  const authorFlag = values2.author;
@@ -92181,6 +92244,13 @@ async function runReplaceBatch(filePath, batchSource, values2) {
92181
92244
  const spec = specs[index2];
92182
92245
  if (!spec)
92183
92246
  continue;
92247
+ if (spec.at !== undefined) {
92248
+ try {
92249
+ document4.body.resolveBlock(spec.at);
92250
+ } catch {
92251
+ return fail("BLOCK_NOT_FOUND", `entry ${index2}: scope "${spec.at}" not found`);
92252
+ }
92253
+ }
92184
92254
  let findResult;
92185
92255
  try {
92186
92256
  findResult = findTextSpans(document4.body, spec.pattern, {
@@ -92193,7 +92263,7 @@ async function runReplaceBatch(filePath, batchSource, values2) {
92193
92263
  const message = matcherError instanceof Error ? matcherError.message : String(matcherError);
92194
92264
  return fail("USAGE", `entry ${index2}: invalid pattern: ${message}`);
92195
92265
  }
92196
- const all2 = findResult.matches;
92266
+ const all2 = spec.at !== undefined ? matchesInScope(findResult.matches, spec.at) : findResult.matches;
92197
92267
  const selected = spec.limit !== undefined ? all2.slice(0, spec.limit) : spec.all ? all2 : all2.slice(0, 1);
92198
92268
  const tracked = allocator ? {
92199
92269
  meta: {
@@ -92265,6 +92335,20 @@ function validateSpec(raw, index2) {
92265
92335
  }
92266
92336
  limit = value;
92267
92337
  }
92338
+ let at;
92339
+ if (raw.at !== undefined) {
92340
+ if (typeof raw.at !== "string") {
92341
+ throw new EntryError3("USAGE", `entry ${index2}: "at" must be a string`);
92342
+ }
92343
+ try {
92344
+ at = validateScopeShape(raw.at);
92345
+ } catch (error) {
92346
+ if (error instanceof ScopeError) {
92347
+ throw new EntryError3(error.code, `entry ${index2}: ${error.message}`);
92348
+ }
92349
+ throw error;
92350
+ }
92351
+ }
92268
92352
  return {
92269
92353
  pattern: raw.pattern,
92270
92354
  replacement: raw.replacement,
@@ -92274,7 +92358,8 @@ function validateSpec(raw, index2) {
92274
92358
  all: Boolean(raw.all),
92275
92359
  ...limit !== undefined ? { limit } : {},
92276
92360
  view: wantCurrent ? "current" : wantBaseline ? "baseline" : "accepted",
92277
- ...typeof raw.author === "string" ? { author: raw.author } : {}
92361
+ ...typeof raw.author === "string" ? { author: raw.author } : {},
92362
+ ...at !== undefined ? { at } : {}
92278
92363
  };
92279
92364
  }
92280
92365
  var EntryError3;
@@ -92283,6 +92368,7 @@ var init_batch4 = __esm(() => {
92283
92368
  init_find();
92284
92369
  init_parse_helpers();
92285
92370
  init_respond();
92371
+ init_scope();
92286
92372
  EntryError3 = class EntryError3 extends Error {
92287
92373
  code;
92288
92374
  hint;
@@ -92303,6 +92389,7 @@ __export(exports_replace3, {
92303
92389
  async function run37(args) {
92304
92390
  const parsed = await tryParseArgs(args, {
92305
92391
  batch: { type: "string" },
92392
+ at: { type: "string" },
92306
92393
  regex: { type: "boolean" },
92307
92394
  "ignore-case": { type: "boolean" },
92308
92395
  all: { type: "boolean" },
@@ -92329,6 +92416,9 @@ async function run37(args) {
92329
92416
  if (parsed.positionals.length > 1) {
92330
92417
  return fail("USAGE", "--batch reads pattern/replacement from the JSONL file; don't also pass them as positionals", HELP33);
92331
92418
  }
92419
+ if (parsed.values.at !== undefined) {
92420
+ return fail("USAGE", '--at is per-entry in batch mode: put "at" on each JSONL line, not on the command', HELP33);
92421
+ }
92332
92422
  return runReplaceBatch(path2, batchInput, parsed.values);
92333
92423
  }
92334
92424
  const pattern = parsed.positionals[1];
@@ -92366,7 +92456,19 @@ async function run37(args) {
92366
92456
  const message = matcherError instanceof Error ? matcherError.message : String(matcherError);
92367
92457
  return fail("USAGE", `Invalid pattern: ${message}`);
92368
92458
  }
92369
- const allMatches = findResult.matches;
92459
+ const atScope = parsed.values.at;
92460
+ let allMatches = findResult.matches;
92461
+ if (atScope !== undefined) {
92462
+ try {
92463
+ const blockId = resolveReplaceScope(document4, atScope);
92464
+ allMatches = matchesInScope(allMatches, blockId);
92465
+ } catch (scopeError) {
92466
+ if (scopeError instanceof ScopeError) {
92467
+ return fail(scopeError.code, scopeError.message);
92468
+ }
92469
+ throw scopeError;
92470
+ }
92471
+ }
92370
92472
  const normalizationFields = findResult.normalizedQuery !== undefined ? {
92371
92473
  normalizedPattern: findResult.normalizedQuery,
92372
92474
  normalizationApplied: findResult.normalizationApplied
@@ -92397,6 +92499,7 @@ async function run37(args) {
92397
92499
  regex: useRegex,
92398
92500
  ignoreCase,
92399
92501
  view: findView,
92502
+ ...atScope ? { at: atScope } : {},
92400
92503
  totalMatches: allMatches.length,
92401
92504
  replaced: selected.length,
92402
92505
  matches: matchesPayload,
@@ -92415,6 +92518,7 @@ async function run37(args) {
92415
92518
  regex: useRegex,
92416
92519
  ignoreCase,
92417
92520
  view: findView,
92521
+ ...atScope ? { at: atScope } : {},
92418
92522
  totalMatches: 0,
92419
92523
  replaced: 0,
92420
92524
  matches: [],
@@ -92451,6 +92555,7 @@ async function run37(args) {
92451
92555
  regex: useRegex,
92452
92556
  ignoreCase,
92453
92557
  view: findView,
92558
+ ...atScope ? { at: atScope } : {},
92454
92559
  totalMatches: allMatches.length,
92455
92560
  replaced: selected.length,
92456
92561
  matches: matchesPayload,
@@ -92466,9 +92571,16 @@ Usage:
92466
92571
  docx replace FILE --batch - [options] # read JSONL from stdin
92467
92572
 
92468
92573
  Options:
92574
+ --at LOCATOR confine the replace to ONE paragraph: a body pN or a cell
92575
+ paragraph tT:rRcC:pN. Use this when the same placeholder
92576
+ repeats across the document (a r\xE9sum\xE9's "City, State" /
92577
+ "Position Title" in every entry) and you want THE one in a
92578
+ specific paragraph \u2014 no find + offset-span edit needed:
92579
+ docx replace doc.docx --at p20 "City, State" "San Francisco, CA"
92469
92580
  --regex treat PATTERN as a JavaScript regular expression
92470
92581
  --ignore-case case-insensitive match
92471
- --all replace every match (default: just the first)
92582
+ --all replace every match (default: just the first; with --at,
92583
+ scoped to that paragraph)
92472
92584
  --limit N replace at most N matches (in document order)
92473
92585
  --author NAME author for tracked changes (default: $DOCX_AUTHOR)
92474
92586
  --track record substitutions as tracked changes even when the
@@ -92539,6 +92651,7 @@ var init_replace4 = __esm(() => {
92539
92651
  init_parse_helpers();
92540
92652
  init_respond();
92541
92653
  init_batch4();
92654
+ init_scope();
92542
92655
  });
92543
92656
 
92544
92657
  // src/cli/sections/index.tsx
@@ -93663,6 +93776,11 @@ theme-following headings adopt the font. Styles/runs that pin their OWN font
93663
93776
  (e.g. a code block's monospace, a deliberately-Arial run) are preserved; pass
93664
93777
  --all to repoint those too.
93665
93778
 
93779
+ Theme-following headings only adopt the font if the document HAS a theme part
93780
+ (every doc this CLI creates does; Word docs do too). The ack's "themeUpdated"
93781
+ is false for the rare theme-less doc \u2014 there the body changes but headings keep
93782
+ their fallback font; run with --all to force the headings onto the new font.
93783
+
93666
93784
  Options:
93667
93785
  --size N Also set the default font size, in points (e.g. 12).
93668
93786
  --all Repoint EVERY explicit font \u2014 styles, body runs, and notes \u2014
@@ -95232,42 +95350,23 @@ var init_groups = __esm(() => {
95232
95350
  };
95233
95351
  });
95234
95352
 
95235
- // src/cli/track-changes/list.ts
95236
- var exports_list5 = {};
95237
- __export(exports_list5, {
95238
- run: () => run49
95239
- });
95240
- async function run49(args) {
95241
- const parsed = await tryParseArgs(args, {
95242
- help: { type: "boolean", short: "h" }
95243
- }, HELP45);
95244
- if (typeof parsed === "number")
95245
- return parsed;
95246
- if (parsed.values.help) {
95247
- await writeStdout(HELP45);
95248
- return EXIT2.OK;
95249
- }
95250
- const path2 = parsed.positionals[0];
95251
- if (!path2)
95252
- return fail("USAGE", "Missing FILE argument", HELP45);
95253
- const document4 = await openOrFail(path2);
95254
- if (typeof document4 === "number")
95255
- return document4;
95353
+ // src/cli/track-changes/list-view.ts
95354
+ function collectTrackedChangeRecords(document4) {
95256
95355
  const byId = new Map;
95257
95356
  for (const paragraph2 of flattenParagraphs(document4.body.blocks)) {
95258
- for (const run50 of paragraph2.runs) {
95259
- if (run50.type !== "text" || !run50.trackedChange)
95357
+ for (const run49 of paragraph2.runs) {
95358
+ if (run49.type !== "text" || !run49.trackedChange)
95260
95359
  continue;
95261
- const change = run50.trackedChange;
95360
+ const change = run49.trackedChange;
95262
95361
  const existing = byId.get(change.id);
95263
95362
  if (existing) {
95264
- existing.text += run50.text;
95363
+ existing.text += run49.text;
95265
95364
  continue;
95266
95365
  }
95267
95366
  byId.set(change.id, {
95268
95367
  ...change,
95269
95368
  blockId: paragraph2.id,
95270
- text: run50.text
95369
+ text: run49.text
95271
95370
  });
95272
95371
  }
95273
95372
  }
@@ -95304,8 +95403,154 @@ async function run49(args) {
95304
95403
  if (group)
95305
95404
  record.group = group;
95306
95405
  }
95307
- await respond(sorted);
95308
- return EXIT2.OK;
95406
+ return sorted;
95407
+ }
95408
+ function renderTrackedChangeTable(records) {
95409
+ if (records.length === 0)
95410
+ return `no tracked changes
95411
+ `;
95412
+ const byGroup = new Map;
95413
+ for (const record of records) {
95414
+ if (!record.group)
95415
+ continue;
95416
+ const members = byGroup.get(record.group) ?? [];
95417
+ members.push(record);
95418
+ byGroup.set(record.group, members);
95419
+ }
95420
+ const rows = [];
95421
+ const consumed = new Set;
95422
+ for (const record of records) {
95423
+ if (record.group) {
95424
+ if (consumed.has(record.group))
95425
+ continue;
95426
+ consumed.add(record.group);
95427
+ const members = byGroup.get(record.group) ?? [record];
95428
+ const del = members.find((member) => member.kind === "del");
95429
+ const ins = members.find((member) => member.kind === "ins");
95430
+ rows.push({
95431
+ handle: record.group,
95432
+ verb: "replace",
95433
+ block: record.blockId,
95434
+ desc: `${quote(del?.text ?? "")} \u2192 ${quote(ins?.text ?? "")}`
95435
+ });
95436
+ continue;
95437
+ }
95438
+ rows.push({
95439
+ handle: record.id,
95440
+ verb: verbLabel(record.kind),
95441
+ block: record.blockId,
95442
+ desc: describeSolo(record)
95443
+ });
95444
+ }
95445
+ const handleWidth = Math.max(...rows.map((row) => row.handle.length));
95446
+ const verbWidth = Math.max(...rows.map((row) => row.verb.length));
95447
+ const blockWidth = Math.max(...rows.map((row) => row.block.length));
95448
+ const body = rows.map((row) => ` ${row.handle.padEnd(handleWidth)} ${row.verb.padEnd(verbWidth)} ${row.block.padEnd(blockWidth)} ${row.desc}`).join(`
95449
+ `);
95450
+ const total = records.length;
95451
+ const header = `${total} tracked change${total === 1 ? "" : "s"}${rows.length === total ? "" : ` (${rows.length} logical)`}:`;
95452
+ const footer = `Address by the leftmost handle: accept/reject --at <handle> (repeatable).
95453
+ ` + "A revN handle resolves both halves of a replace in one call; --json for full detail.";
95454
+ return `${header}
95455
+
95456
+ ${body}
95457
+
95458
+ ${footer}
95459
+ `;
95460
+ }
95461
+ async function remainingTrackedChangesBlock(path2, verb) {
95462
+ let document4;
95463
+ try {
95464
+ document4 = await Document.open(path2);
95465
+ } catch {
95466
+ return "";
95467
+ }
95468
+ const remaining = collectTrackedChangeRecords(document4);
95469
+ if (remaining.length === 0)
95470
+ return "";
95471
+ return `
95472
+ Remaining (ids renumbered after this ${verb}):
95473
+
95474
+ ${renderTrackedChangeTable(remaining)}`;
95475
+ }
95476
+ function quote(text6, max = 44) {
95477
+ const collapsed = text6.replace(/\s+/g, " ").trim();
95478
+ if (collapsed === "")
95479
+ return "\xB6";
95480
+ const clipped = collapsed.length > max ? `${collapsed.slice(0, max - 1)}\u2026` : collapsed;
95481
+ return `"${clipped}"`;
95482
+ }
95483
+ function verbLabel(kind) {
95484
+ const labels = {
95485
+ ins: "insert",
95486
+ del: "delete",
95487
+ moveFrom: "move-from",
95488
+ moveTo: "move-to",
95489
+ sectPrChange: "section",
95490
+ pPrChange: "format",
95491
+ rowIns: "row-insert",
95492
+ rowDel: "row-delete",
95493
+ cellIns: "cell-insert",
95494
+ cellDel: "cell-delete",
95495
+ tblGridChange: "grid",
95496
+ tblPrChange: "table-fmt",
95497
+ tcPrChange: "cell-fmt",
95498
+ checkboxToggle: "checkbox"
95499
+ };
95500
+ return labels[kind] ?? kind;
95501
+ }
95502
+ function describeSolo(record) {
95503
+ if (record.kind === "ins" || record.kind === "del" || record.kind === "moveFrom" || record.kind === "moveTo") {
95504
+ return record.text === "" ? "\xB6 paragraph mark" : quote(record.text);
95505
+ }
95506
+ if (record.kind === "pPrChange" || record.kind === "sectPrChange") {
95507
+ const diff2 = propDiff(record.prior, record.current);
95508
+ return diff2 || (record.kind === "pPrChange" ? "paragraph format" : "layout");
95509
+ }
95510
+ const notes = {
95511
+ rowIns: "row inserted",
95512
+ rowDel: "row deleted",
95513
+ cellIns: "cell inserted",
95514
+ cellDel: "cell deleted",
95515
+ tblGridChange: "grid widths",
95516
+ tblPrChange: "table properties",
95517
+ tcPrChange: "cell properties",
95518
+ checkboxToggle: "checkbox toggled"
95519
+ };
95520
+ return notes[record.kind] ?? "";
95521
+ }
95522
+ function propDiff(prior, current) {
95523
+ const before = flattenProps(prior);
95524
+ const after = flattenProps(current);
95525
+ const keys = new Set([...before.keys(), ...after.keys()]);
95526
+ const parts = [];
95527
+ for (const key of keys) {
95528
+ const from = before.get(key);
95529
+ const to = after.get(key);
95530
+ if (from === to)
95531
+ continue;
95532
+ parts.push(`${key} ${from ?? "\xB7"}\u2192${to ?? "\xB7"}`);
95533
+ }
95534
+ if (parts.length === 0)
95535
+ return "";
95536
+ if (parts.length > 3)
95537
+ return `${parts.slice(0, 3).join(", ")}, \u2026`;
95538
+ return parts.join(", ");
95539
+ }
95540
+ function flattenProps(obj, prefix = "") {
95541
+ const out = new Map;
95542
+ if (obj === null || typeof obj !== "object")
95543
+ return out;
95544
+ for (const [key, value] of Object.entries(obj)) {
95545
+ const path2 = prefix ? `${prefix}.${key}` : key;
95546
+ if (value !== null && typeof value === "object") {
95547
+ for (const [k, v] of flattenProps(value, path2))
95548
+ out.set(k, v);
95549
+ } else if (typeof value === "string" || typeof value === "number") {
95550
+ out.set(path2, value);
95551
+ }
95552
+ }
95553
+ return out;
95309
95554
  }
95310
95555
  function trackedChangeIndex(id) {
95311
95556
  const match = id.match(/^tc(\d+)$/);
@@ -95355,12 +95600,50 @@ function readParagraphPropsSummary(children) {
95355
95600
  }
95356
95601
  return out;
95357
95602
  }
95603
+ var init_list_view = __esm(() => {
95604
+ init_core2();
95605
+ init_track_changes();
95606
+ init_groups();
95607
+ });
95608
+
95609
+ // src/cli/track-changes/list.ts
95610
+ var exports_list5 = {};
95611
+ __export(exports_list5, {
95612
+ run: () => run49
95613
+ });
95614
+ async function run49(args) {
95615
+ const parsed = await tryParseArgs(args, {
95616
+ help: { type: "boolean", short: "h" },
95617
+ json: { type: "boolean" }
95618
+ }, HELP45);
95619
+ if (typeof parsed === "number")
95620
+ return parsed;
95621
+ if (parsed.values.help) {
95622
+ await writeStdout(HELP45);
95623
+ return EXIT2.OK;
95624
+ }
95625
+ const path2 = parsed.positionals[0];
95626
+ if (!path2)
95627
+ return fail("USAGE", "Missing FILE argument", HELP45);
95628
+ const document4 = await openOrFail(path2);
95629
+ if (typeof document4 === "number")
95630
+ return document4;
95631
+ const records = collectTrackedChangeRecords(document4);
95632
+ if (parsed.values.json) {
95633
+ await respond(records);
95634
+ return EXIT2.OK;
95635
+ }
95636
+ await writeStdout(renderTrackedChangeTable(records));
95637
+ return EXIT2.OK;
95638
+ }
95358
95639
  var HELP45 = `docx track-changes list \u2014 inventory every revision wrapper
95359
95640
 
95360
95641
  Usage:
95361
95642
  docx track-changes list FILE [options]
95362
95643
 
95363
95644
  Options:
95645
+ --json Emit the full revision objects as a JSON array (default: a
95646
+ text table, one LOGICAL change per line)
95364
95647
  -h, --help Show this help
95365
95648
 
95366
95649
  Lists every revision wrapper with stable tcN ids \u2014 run-level <w:ins>, <w:del>,
@@ -95370,25 +95653,35 @@ paragraph-mark <w:ins>/<w:del> markers (a self-closing element inside
95370
95653
  of the same logical move appear as separate entries; their kind tells them
95371
95654
  apart.
95372
95655
 
95373
- Output: a bare JSON array of { id, kind, author, date, revisionId, blockId,
95374
- text } sorted by id (document order). Each item's "id" (e.g. tc0) is its
95375
- addressable handle \u2014 pass it to \`accept\`/\`reject --at tcN\`. When a del and an
95376
- ins are an adjacent REPLACE pair on the same paragraph, BOTH carry a shared
95377
- "group": "revN" \u2014 accept/reject the whole logical change in one call with
95378
- \`--at revN\` instead of accepting each half separately (tcN ids renumber after
95379
- each single accept, so the revN handle avoids the re-list ping-pong). Errors print
95380
- {code, error, hint?} with a nonzero exit. kind is one of: "ins", "del", "moveFrom",
95381
- "moveTo", "sectPrChange", "pPrChange", "rowIns", "rowDel", "cellIns", "cellDel",
95382
- "tblGridChange", "tblPrChange", "tcPrChange", "checkboxToggle". Paragraph-mark
95383
- entries have kind "ins"/"del" with text "" \u2014 their blockId is the owning
95384
- paragraph's pN. Table-structural entries (rowIns/rowDel/cellIns/cellDel and
95385
- the property revisions tblGridChange/tblPrChange/tcPrChange) have text "" and
95386
- blockId set to the owning table's tN. checkboxToggle entries surface a Word
95387
- checkbox content control's tracked toggle (\u2610\u2194\u2612): metadata comes from the
95388
- inner <w:ins> (the new glyph); reject restores the prior glyph and flips
95389
- the w14:checked attribute back. Structural inserts/deletes of a checkbox
95390
- (Word emits <w:customXmlDel/InsRangeStart/End> around the SDT) round-trip
95391
- through the XmlNode tree but aren't yet enumerated as a dedicated kind.
95656
+ Output: a text table, ONE LOGICAL CHANGE per line \u2014
95657
+
95658
+ rev0 replace p7 "Net 90 from the Company" \u2192 "Net 30 from the Company"
95659
+ tc4 format p22 spacing.line \u2192360
95660
+ tc7 delete p27 "Personal Guarantee."
95661
+
95662
+ The leftmost token is the handle you pass to \`accept\`/\`reject --at\` (repeatable,
95663
+ e.g. \`--at rev0 --at tc4\`). A del+ins REPLACE pair on the same paragraph is
95664
+ collapsed onto ONE line under its shared revN handle, so accepting/rejecting the
95665
+ whole logical change is a single call \u2014 addressing the two tcN halves separately
95666
+ forces a re-list, because tcN ids renumber after each accept. All --at targets in
95667
+ one call resolve against the pre-mutation tree, so the renumbering never bites a
95668
+ batch.
95669
+
95670
+ --json gives a bare JSON array of { id, kind, author, date, revisionId, blockId,
95671
+ text } sorted by id (document order). Each item's "id" (e.g. tc0) is its granular
95672
+ handle; paired halves additionally carry "group": "revN". kind is one of: "ins",
95673
+ "del", "moveFrom", "moveTo", "sectPrChange", "pPrChange", "rowIns", "rowDel",
95674
+ "cellIns", "cellDel", "tblGridChange", "tblPrChange", "tcPrChange",
95675
+ "checkboxToggle". Paragraph-mark entries have kind "ins"/"del" with text "" \u2014
95676
+ their blockId is the owning paragraph's pN. Table-structural entries
95677
+ (rowIns/rowDel/cellIns/cellDel and the property revisions
95678
+ tblGridChange/tblPrChange/tcPrChange) have text "" and blockId set to the owning
95679
+ table's tN. checkboxToggle entries surface a Word checkbox content control's
95680
+ tracked toggle (\u2610\u2194\u2612): metadata comes from the inner <w:ins> (the new glyph);
95681
+ reject restores the prior glyph and flips the w14:checked attribute back.
95682
+ Structural inserts/deletes of a checkbox (Word emits
95683
+ <w:customXmlDel/InsRangeStart/End> around the SDT) round-trip through the
95684
+ XmlNode tree but aren't yet enumerated as a dedicated kind.
95392
95685
 
95393
95686
  Property revisions (kind="sectPrChange" or "pPrChange") additionally include
95394
95687
  { prior, current } objects from before and after the tracked edit, so agents
@@ -95398,19 +95691,17 @@ style/alignment/spacing/indent.
95398
95691
 
95399
95692
  Examples:
95400
95693
  docx track-changes list doc.docx
95401
- docx track-changes list doc.docx | jq '.[] | select(.kind == "del")'
95402
- docx track-changes list doc.docx | jq '.[] | select(.kind | test("move"))'
95403
- docx track-changes list doc.docx | jq '.[] | select(.kind == "sectPrChange") | .prior, .current'
95404
- docx track-changes list doc.docx | jq '.[] | select(.kind == "pPrChange") | .prior, .current'
95694
+ docx track-changes accept doc.docx --at rev0 --at tc4
95695
+ docx track-changes list doc.docx --json | jq '.[] | select(.kind == "del")'
95696
+ docx track-changes list doc.docx --json | jq '.[] | select(.kind | test("move"))'
95697
+ docx track-changes list doc.docx --json | jq '.[] | select(.kind == "pPrChange") | .prior, .current'
95405
95698
  `;
95406
95699
  var init_list8 = __esm(() => {
95407
- init_core2();
95408
- init_track_changes();
95409
95700
  init_respond();
95410
- init_groups();
95701
+ init_list_view();
95411
95702
  });
95412
95703
 
95413
- // src/cli/track-changes/apply.ts
95704
+ // src/cli/track-changes/run-apply.ts
95414
95705
  async function runApply(args, verb, help) {
95415
95706
  const parsed = await tryParseArgs(args, {
95416
95707
  at: { type: "string", multiple: true },
@@ -95472,6 +95763,11 @@ async function runApply(args, verb, help) {
95472
95763
  path: outputPath ?? path2,
95473
95764
  applied
95474
95765
  });
95766
+ if (!all2 && !parsed.values.verbose) {
95767
+ const remaining = await remainingTrackedChangesBlock(outputPath ?? path2, verb);
95768
+ if (remaining)
95769
+ await writeStdout(remaining);
95770
+ }
95475
95771
  return EXIT2.OK;
95476
95772
  } catch (error) {
95477
95773
  if (error instanceof TrackedChangeNotFoundError) {
@@ -95480,10 +95776,11 @@ async function runApply(args, verb, help) {
95480
95776
  throw error;
95481
95777
  }
95482
95778
  }
95483
- var init_apply2 = __esm(() => {
95779
+ var init_run_apply = __esm(() => {
95484
95780
  init_track_changes();
95485
95781
  init_respond();
95486
95782
  init_groups();
95783
+ init_list_view();
95487
95784
  });
95488
95785
 
95489
95786
  // src/cli/track-changes/accept.ts
@@ -95497,7 +95794,7 @@ async function run50(args) {
95497
95794
  var AT_FORMS14, HELP46;
95498
95795
  var init_accept = __esm(() => {
95499
95796
  init_core2();
95500
- init_apply2();
95797
+ init_run_apply();
95501
95798
  AT_FORMS14 = describeForms(["trackedChange"], " ");
95502
95799
  HELP46 = `docx track-changes accept \u2014 accept tracked changes (incorporate into the doc)
95503
95800
 
@@ -95568,7 +95865,7 @@ async function run51(args) {
95568
95865
  var AT_FORMS15, HELP47;
95569
95866
  var init_reject = __esm(() => {
95570
95867
  init_core2();
95571
- init_apply2();
95868
+ init_run_apply();
95572
95869
  AT_FORMS15 = describeForms(["trackedChange"], " ");
95573
95870
  HELP47 = `docx track-changes reject \u2014 reject tracked changes (revert to pre-change state)
95574
95871
 
@@ -95636,13 +95933,15 @@ Examples:
95636
95933
  `;
95637
95934
  });
95638
95935
 
95639
- // src/cli/track-changes/toggle.tsx
95640
- var exports_toggle = {};
95641
- __export(exports_toggle, {
95936
+ // src/cli/track-changes/apply.ts
95937
+ var exports_apply = {};
95938
+ __export(exports_apply, {
95642
95939
  run: () => run52
95643
95940
  });
95644
95941
  async function run52(args) {
95645
95942
  const parsed = await tryParseArgs(args, {
95943
+ accept: { type: "string", multiple: true },
95944
+ reject: { type: "string", multiple: true },
95646
95945
  ...SAVE_FLAGS
95647
95946
  }, HELP48);
95648
95947
  if (typeof parsed === "number")
@@ -95652,15 +95951,151 @@ async function run52(args) {
95652
95951
  return EXIT2.OK;
95653
95952
  }
95654
95953
  setVerboseAck(Boolean(parsed.values.verbose));
95954
+ const path2 = parsed.positionals[0];
95955
+ if (!path2)
95956
+ return fail("USAGE", "Missing FILE argument", HELP48);
95957
+ const acceptRaw = parsed.values.accept ?? [];
95958
+ const rejectRaw = parsed.values.reject ?? [];
95959
+ if (acceptRaw.length === 0 && rejectRaw.length === 0) {
95960
+ return fail("USAGE", "Specify --accept and/or --reject (handle)", HELP48);
95961
+ }
95962
+ const document4 = await openOrFail(path2);
95963
+ if (typeof document4 === "number")
95964
+ return document4;
95965
+ const trackChanges = new TrackChanges(document4);
95966
+ const groups = revisionGroups(trackChanges.list());
95967
+ let accepts;
95968
+ let rejects;
95969
+ try {
95970
+ accepts = expandRevisionTargets(acceptRaw, groups);
95971
+ rejects = expandRevisionTargets(rejectRaw, groups);
95972
+ } catch (error) {
95973
+ if (error instanceof UnknownRevisionError) {
95974
+ return fail("TRACKED_CHANGE_NOT_FOUND", error.message, "Run 'docx track-changes list FILE' \u2014 revN handles appear as the `group` field on paired changes.");
95975
+ }
95976
+ throw error;
95977
+ }
95978
+ const rejectSet = new Set(rejects);
95979
+ const conflict = accepts.find((id) => rejectSet.has(id));
95980
+ if (conflict) {
95981
+ return fail("USAGE", `${conflict} is named in both --accept and --reject`, HELP48);
95982
+ }
95983
+ const outputPath = parsed.values.output;
95984
+ try {
95985
+ if (parsed.values["dry-run"]) {
95986
+ const previewApplied = [
95987
+ ...trackChanges.preview(accepts, "accept"),
95988
+ ...trackChanges.preview(rejects, "reject")
95989
+ ].sort((a2, b) => trackedChangeIndex2(a2.id) - trackedChangeIndex2(b.id));
95990
+ await respond({
95991
+ operation: "track-changes.apply",
95992
+ dryRun: true,
95993
+ path: path2,
95994
+ ...outputPath ? { output: outputPath } : {},
95995
+ applied: previewApplied
95996
+ });
95997
+ return EXIT2.OK;
95998
+ }
95999
+ const applied = trackChanges.apply(accepts, rejects);
96000
+ await document4.save(outputPath);
96001
+ await respondAck({
96002
+ ok: true,
96003
+ operation: "track-changes.apply",
96004
+ path: outputPath ?? path2,
96005
+ applied
96006
+ });
96007
+ if (!parsed.values.verbose) {
96008
+ const remaining = await remainingTrackedChangesBlock(outputPath ?? path2, "apply");
96009
+ if (remaining)
96010
+ await writeStdout(remaining);
96011
+ }
96012
+ return EXIT2.OK;
96013
+ } catch (error) {
96014
+ if (error instanceof TrackedChangeNotFoundError) {
96015
+ return fail("TRACKED_CHANGE_NOT_FOUND", error.message, "Run 'docx track-changes list FILE' to see available handles.");
96016
+ }
96017
+ throw error;
96018
+ }
96019
+ }
96020
+ function trackedChangeIndex2(id) {
96021
+ const match = id.match(/^tc(\d+)$/);
96022
+ return match?.[1] ? Number(match[1]) : 0;
96023
+ }
96024
+ var HELP48 = `docx track-changes apply \u2014 finalize: accept AND reject in one atomic call
96025
+
96026
+ Usage:
96027
+ docx track-changes apply FILE (--accept H ... | --reject H ...) [options]
96028
+
96029
+ A document review ends in a finalize: accept the changes you want, reject the
96030
+ rest. Doing that as separate \`accept\` and \`reject\` calls is a trap \u2014 tcN/revN
96031
+ ids renumber after every accept/reject, so the SECOND command addresses a
96032
+ moved target ("tc5 not found", or worse, silently the wrong change). \`apply\`
96033
+ takes BOTH decisions at once and resolves EVERY handle against the original
96034
+ pre-mutation tree, so nothing renumbers mid-operation and the file is never
96035
+ left half-finalized (there is no undo).
96036
+
96037
+ Handles are the same ones \`track-changes list\` prints: a tcN, or a revN that
96038
+ covers both halves of a del+ins replace pair. Both flags repeat.
96039
+
96040
+ Targets (at least one required):
96041
+ --accept H A handle (tcN or revN) to accept. Repeat for several
96042
+ (--accept rev0 --accept rev1 --accept tc4).
96043
+ --reject H A handle (tcN or revN) to reject. Repeat for several.
96044
+
96045
+ A handle may not appear in both lists. Unknown handles error before anything is
96046
+ written. Leftover changes you name in neither list stay tracked (apply finalizes
96047
+ only what you address); the confirmation re-lists them so you can see what's left.
96048
+
96049
+ Options:
96050
+ -o, --output PATH Write to PATH instead of overwriting FILE
96051
+ --dry-run Print what would change; do not write the file
96052
+ -v, --verbose Print the success ack JSON (default: a one-line confirmation)
96053
+ -h, --help Show this help
96054
+
96055
+ Output:
96056
+ Prints a one-line confirmation on success (exit 0); when changes remain it
96057
+ also re-lists them with their renumbered handles. --verbose prints
96058
+ {ok:true, operation, path, applied}. --dry-run previews. Errors print
96059
+ {code, error, hint?} with a nonzero exit. Discover handles with
96060
+ \`docx track-changes list FILE\`.
96061
+
96062
+ Examples:
96063
+ docx track-changes apply doc.docx --accept rev0 --accept rev1 --accept tc4 \\
96064
+ --reject rev2 --reject tc7
96065
+ docx track-changes apply doc.docx --reject rev0 --dry-run
96066
+ `;
96067
+ var init_apply2 = __esm(() => {
96068
+ init_track_changes();
96069
+ init_respond();
96070
+ init_groups();
96071
+ init_list_view();
96072
+ });
96073
+
96074
+ // src/cli/track-changes/toggle.tsx
96075
+ var exports_toggle = {};
96076
+ __export(exports_toggle, {
96077
+ run: () => run53
96078
+ });
96079
+ async function run53(args) {
96080
+ const parsed = await tryParseArgs(args, {
96081
+ ...SAVE_FLAGS
96082
+ }, HELP49);
96083
+ if (typeof parsed === "number")
96084
+ return parsed;
96085
+ if (parsed.values.help) {
96086
+ await writeStdout(HELP49);
96087
+ return EXIT2.OK;
96088
+ }
96089
+ setVerboseAck(Boolean(parsed.values.verbose));
95655
96090
  const [first, second] = parsed.positionals;
95656
96091
  const modeFirst = first === "on" || first === "off";
95657
96092
  const mode = modeFirst ? first : second;
95658
96093
  const path2 = modeFirst ? second : first;
95659
96094
  if (mode !== "on" && mode !== "off") {
95660
- return fail("USAGE", `Expected "on" or "off" (e.g. \`docx track-changes on FILE\`), got: ${parsed.positionals.join(" ") || "<nothing>"}`, HELP48);
96095
+ return fail("USAGE", `Expected "on" or "off" (e.g. \`docx track-changes on FILE\`), got: ${parsed.positionals.join(" ") || "<nothing>"}`, HELP49);
95661
96096
  }
95662
96097
  if (!path2)
95663
- return fail("USAGE", "Missing FILE argument", HELP48);
96098
+ return fail("USAGE", "Missing FILE argument", HELP49);
95664
96099
  const document4 = await openOrFail(path2);
95665
96100
  if (typeof document4 === "number")
95666
96101
  return document4;
@@ -95690,7 +96125,7 @@ async function run52(args) {
95690
96125
  });
95691
96126
  return EXIT2.OK;
95692
96127
  }
95693
- var HELP48 = `docx track-changes \u2014 toggle the document's tracked-changes mode
96128
+ var HELP49 = `docx track-changes \u2014 toggle the document's tracked-changes mode
95694
96129
 
95695
96130
  Usage:
95696
96131
  docx track-changes on|off FILE [options]
@@ -95728,16 +96163,16 @@ var init_toggle2 = __esm(() => {
95728
96163
  // src/cli/track-changes/index.ts
95729
96164
  var exports_track_changes = {};
95730
96165
  __export(exports_track_changes, {
95731
- run: () => run53
96166
+ run: () => run54
95732
96167
  });
95733
- async function run53(args) {
96168
+ async function run54(args) {
95734
96169
  const first = args[0];
95735
96170
  if (first === "--help" || first === "-h" || first === "help") {
95736
- await writeStdout(HELP49);
96171
+ await writeStdout(HELP50);
95737
96172
  return 0;
95738
96173
  }
95739
96174
  if (!first) {
95740
- return fail("USAGE", "Missing arguments", HELP49);
96175
+ return fail("USAGE", "Missing arguments", HELP50);
95741
96176
  }
95742
96177
  if (first === "list") {
95743
96178
  const module_2 = await Promise.resolve().then(() => (init_list8(), exports_list5));
@@ -95751,16 +96186,21 @@ async function run53(args) {
95751
96186
  const module_2 = await Promise.resolve().then(() => (init_reject(), exports_reject));
95752
96187
  return module_2.run(args.slice(1));
95753
96188
  }
96189
+ if (first === "apply") {
96190
+ const module_2 = await Promise.resolve().then(() => (init_apply2(), exports_apply));
96191
+ return module_2.run(args.slice(1));
96192
+ }
95754
96193
  const module_ = await Promise.resolve().then(() => (init_toggle2(), exports_toggle));
95755
96194
  return module_.run(args);
95756
96195
  }
95757
- var HELP49 = `docx track-changes \u2014 manage tracked-changes
96196
+ var HELP50 = `docx track-changes \u2014 manage tracked-changes
95758
96197
 
95759
96198
  Usage:
95760
96199
  docx track-changes on|off FILE [options]
95761
96200
  docx track-changes list FILE [options]
95762
96201
  docx track-changes accept FILE (--at tcN | --all) [options]
95763
96202
  docx track-changes reject FILE (--at tcN | --all) [options]
96203
+ docx track-changes apply FILE (--accept H ... | --reject H ...) [options]
95764
96204
 
95765
96205
  Verbs:
95766
96206
  on Set <w:trackChanges/> in word/settings.xml
@@ -95775,9 +96215,14 @@ Verbs:
95775
96215
  paragraph-mark ins the entire paragraph is removed); del/moveFrom
95776
96216
  unwrap (with <w:delText> \u2192 <w:t> rename); sectPrChange restores
95777
96217
  its snapshot
96218
+ apply Finalize: accept AND reject in ONE atomic call, every handle
96219
+ resolved against the original tree \u2014 the safe way to apply a
96220
+ review, since separate accept/reject calls renumber ids between
96221
+ them
95778
96222
 
95779
96223
  Exact-change addressing is always --at tcN (repeatable); --all targets every
95780
- change. Discover ids with "docx track-changes list FILE".
96224
+ change; \`apply\` takes --accept/--reject handle lists. Discover ids with
96225
+ "docx track-changes list FILE".
95781
96226
 
95782
96227
  When tracking is on, the SUBSEQUENT insert/edit/delete/replace commands emit
95783
96228
  <w:ins>/<w:del> markers (attributed via --author or $DOCX_AUTHOR on those
@@ -95894,29 +96339,29 @@ var init_count = __esm(() => {
95894
96339
  // src/cli/wc/index.ts
95895
96340
  var exports_wc = {};
95896
96341
  __export(exports_wc, {
95897
- run: () => run54
96342
+ run: () => run55
95898
96343
  });
95899
- async function run54(args) {
96344
+ async function run55(args) {
95900
96345
  const parsed = await tryParseArgs(args, {
95901
96346
  accepted: { type: "boolean" },
95902
96347
  baseline: { type: "boolean" },
95903
96348
  current: { type: "boolean" },
95904
96349
  json: { type: "boolean" },
95905
96350
  help: { type: "boolean", short: "h" }
95906
- }, HELP50);
96351
+ }, HELP51);
95907
96352
  if (typeof parsed === "number")
95908
96353
  return parsed;
95909
96354
  if (parsed.values.help) {
95910
- await writeStdout(HELP50);
96355
+ await writeStdout(HELP51);
95911
96356
  return EXIT2.OK;
95912
96357
  }
95913
96358
  const path2 = parsed.positionals[0];
95914
96359
  const locatorInput = parsed.positionals[1];
95915
96360
  if (!path2)
95916
- return fail("USAGE", "Missing FILE argument", HELP50);
96361
+ return fail("USAGE", "Missing FILE argument", HELP51);
95917
96362
  const view = resolveView(parsed.values);
95918
96363
  if (!view) {
95919
- return fail("USAGE", "--accepted, --baseline, and --current are mutually exclusive", HELP50);
96364
+ return fail("USAGE", "--accepted, --baseline, and --current are mutually exclusive", HELP51);
95920
96365
  }
95921
96366
  const json2 = Boolean(parsed.values.json);
95922
96367
  const pickText = paragraphTextFor(view);
@@ -96094,13 +96539,13 @@ function paragraphTextFor(view) {
96094
96539
  return paragraphTextBaseline;
96095
96540
  return paragraphText2;
96096
96541
  }
96097
- var HELP50;
96542
+ var HELP51;
96098
96543
  var init_wc = __esm(() => {
96099
96544
  init_core2();
96100
96545
  init_parse_helpers();
96101
96546
  init_respond();
96102
96547
  init_count();
96103
- HELP50 = `docx wc \u2014 count words in a document or a locator-addressed slice
96548
+ HELP51 = `docx wc \u2014 count words in a document or a locator-addressed slice
96104
96549
 
96105
96550
  Usage:
96106
96551
  docx wc FILE [LOCATOR] [options]
@@ -96164,7 +96609,7 @@ Examples:
96164
96609
  // package.json
96165
96610
  var package_default = {
96166
96611
  name: "bun-docx",
96167
- version: "0.16.0",
96612
+ version: "0.17.0",
96168
96613
  description: "CLI for AI agents (Claude, Codex) to read, edit, and comment on .docx files with full format fidelity.",
96169
96614
  keywords: [
96170
96615
  "docx",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bun-docx",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "CLI for AI agents (Claude, Codex) to read, edit, and comment on .docx files with full format fidelity.",
5
5
  "keywords": [
6
6
  "docx",