Skip to content

Commit 1731905

Browse files
committed
Merge branch 'main' into stable
# Conflicts: # packages/superdoc/scripts/ensure-types.cjs
2 parents 22f8a7e + f609371 commit 1731905

309 files changed

Lines changed: 16199 additions & 3572 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/cli/README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -311,10 +311,11 @@ superdoc info ./contract.docx --pretty
311311

312312
## Input payload flags
313313

314-
- `--query-json`, `--query-file`
315-
- `--address-json`, `--address-file`
316-
- `--target-json`, `--target-file`
317-
- `--at-json`, `--at-file` (for `create paragraph`)
314+
- `--query-json`, `--query-file` (`find`, `lists list`)
315+
- `--address-json`, `--address-file` (`get-node`, `lists get`)
316+
- `--target-json` (mutation commands — no `--target-file` counterpart; use flat flags `--block-id`/`--start`/`--end` as alternative)
317+
- `--input-json`, `--input-file` (`call`, `create paragraph`)
318+
- `--at-json`, `--at-file` (`create paragraph`)
318319

319320
## Stdin support
320321

apps/cli/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@
1010
"skill"
1111
],
1212
"scripts": {
13+
"predev": "node scripts/ensure-superdoc-build.js",
1314
"dev": "bun run src/index.ts",
15+
"prebuild": "node scripts/ensure-superdoc-build.js",
1416
"build": "bun build src/index.ts --outdir dist --target node --format esm",
17+
"prebuild:native": "node scripts/ensure-superdoc-build.js",
1518
"build:native": "bun build src/index.ts --compile --outfile dist/superdoc",
1619
"build:native:all": "node scripts/build-native-cli.js --all",
1720
"build:native:host": "node scripts/build-native-cli.js",
@@ -20,10 +23,12 @@
2023
"build:prepublish": "node scripts/build-and-stage.js",
2124
"publish:platforms": "node scripts/publish.js --tag latest",
2225
"publish:platforms:dry": "node scripts/publish.js --tag latest --dry-run",
26+
"pretest": "node scripts/ensure-superdoc-build.js",
2327
"test": "NODE_ENV=test bun test",
2428
"lint": "eslint .",
2529
"lint:fix": "eslint --fix .",
2630
"format": "prettier --write .",
31+
"pretypecheck": "node scripts/ensure-superdoc-build.js --types",
2732
"typecheck": "tsc --noEmit -p tsconfig.check.json",
2833
"prepublishOnly": "pnpm run build",
2934
"release": "pnpx semantic-release",

apps/cli/scripts/build-native-cli.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { existsSync, mkdirSync, readFileSync } from 'node:fs';
44
import path from 'node:path';
55
import { spawnSync } from 'node:child_process';
66
import { cliRoot, ensureNoUnknownFlags, getOptionalFlagValue, isDirectExecution, repoRoot } from './utils.js';
7+
import { ensureSuperdocBuild } from './ensure-superdoc-build.js';
78

89
const cliEntry = path.join(cliRoot, 'src/index.ts');
910
const cliPackagePath = path.join(cliRoot, 'package.json');
@@ -155,6 +156,7 @@ async function writeManifest(entries) {
155156
*/
156157
export async function main(argv = process.argv.slice(2)) {
157158
ensureNoUnknownFlags(argv, allowedFlags);
159+
ensureSuperdocBuild();
158160
const targets = resolveRequestedTargets(argv);
159161
const hostTarget = resolveHostTargetId();
160162

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import path from 'node:path';
2+
import { ensureNoUnknownFlags, isDirectExecution, repoRoot, runCommand } from './utils.js';
3+
4+
const allowedFlags = new Set(['--types']);
5+
const superdocRoot = path.join(repoRoot, 'packages/superdoc');
6+
7+
/**
8+
* Ensures the packaged `superdoc` runtime exists for CLI entrypoints that now
9+
* consume `superdoc/super-editor` instead of raw `@superdoc/super-editor/*` source.
10+
*
11+
* `--types` performs the full published build so package type exports exist.
12+
* Without it, a faster runtime-only build is sufficient for Bun execution.
13+
*
14+
* @param {{ includeTypes?: boolean }} [options]
15+
* @returns {void}
16+
*/
17+
export function ensureSuperdocBuild(options = {}) {
18+
const includeTypes = options.includeTypes === true;
19+
const scriptName = includeTypes ? 'build:es' : 'build:dev';
20+
const label = includeTypes ? 'Build packaged SuperDoc runtime and types' : 'Build packaged SuperDoc runtime';
21+
22+
runCommand('pnpm', ['--prefix', superdocRoot, 'run', scriptName], label);
23+
}
24+
25+
/**
26+
* CLI wrapper around {@link ensureSuperdocBuild}.
27+
*
28+
* @param {string[]} [argv=process.argv.slice(2)]
29+
* @returns {void}
30+
*/
31+
export function main(argv = process.argv.slice(2)) {
32+
ensureNoUnknownFlags(argv, allowedFlags);
33+
ensureSuperdocBuild({ includeTypes: argv.includes('--types') });
34+
}
35+
36+
if (isDirectExecution(import.meta.url)) {
37+
try {
38+
main();
39+
} catch (error) {
40+
console.error(error instanceof Error ? error.message : error);
41+
process.exitCode = 1;
42+
}
43+
}

apps/cli/skill/SKILL.md

Lines changed: 66 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description: Edit, query, and transform Word documents with the SuperDoc CLI v1
55

66
# SuperDoc CLI (v1)
77

8-
Use SuperDoc CLI for DOCX work. Prefer canonical v1 commands.
8+
Use SuperDoc CLI for DOCX work. Use v1 commands (canonical operations and their helper wrappers).
99
Do not default to legacy commands unless explicitly needed for v0-style bulk workflows.
1010

1111
Use `superdoc` if installed, or `npx @superdoc-dev/cli@latest` as a fallback.
@@ -28,12 +28,13 @@ Use `describe command` for per-command args and constraints.
2828

2929
```bash
3030
superdoc open ./contract.docx
31-
superdoc find --type text --pattern "termination"
31+
superdoc query match --select-json '{"type":"text","pattern":"termination"}' --require exactlyOne
3232
superdoc replace --target-json '{"kind":"text","blockId":"p1","range":{"start":0,"end":11}}' --text "expiration"
3333
superdoc save --in-place
3434
superdoc close
3535
```
3636

37+
- Always use `query match` (not `find`) to discover mutation targets — it returns exact addresses with cardinality guarantees.
3738
- After `open`, commands run against the active/default session when `<doc>` is omitted.
3839
- Use `superdoc session list|set-default|save|close` for explicit session control.
3940
- `close` on dirty state requires `--discard` or a prior `save`.
@@ -57,28 +58,80 @@ superdoc replace ./proposal.docx \
5758

5859
- In stateless mode (`<doc>` provided), mutating commands require `--out` unless using `--dry-run`.
5960

61+
### Safety: preview before apply
62+
63+
- Use `--dry-run` to preview any mutation without applying it.
64+
- Use `--expected-revision <n>` with stateful mutations for optimistic concurrency checks.
65+
6066
## Common v1 Commands
6167

62-
- Search text/nodes: `find --type text --pattern "..."` or `find --query-json '{...}'`
63-
- Replace text: `replace --target-json '{...}' --text "..."`
64-
- Add/edit comments: `comments add|reply|edit|resolve|remove`
65-
- Review tracked changes: `track-changes list|accept|reject|accept-all|reject-all`
68+
### Query & inspect
69+
70+
- Search/browse content: `find --type text --pattern "..."` or `find --query-json '{...}'`
71+
- Find mutation target: `query match --select-json '{...}' --require exactlyOne`
72+
- Inspect blocks: `blocks list`, `get-node`, `get-node-by-id`
6673
- Extract content: `get-text`, `get-markdown`, `get-html`
67-
- Low-level direct invoke: `call <operationId> --input-json '{...}'`
74+
75+
### Mutate
76+
77+
- Replace text: `replace --target-json '{...}' --text "..."`
78+
- Insert inline text: `insert --block-id <id> --offset <n> --value "..."`
79+
- Delete text/node: `delete --target-json '{...}'`
80+
- Delete blocks: `blocks delete`, `blocks delete-range`
81+
- Batch mutations: `mutations apply --steps-json '[...]' --atomic true --change-mode direct`
82+
- Create paragraph: `create paragraph --text "..."` (with optional `--at-json`)
83+
- Create heading: `create heading --input-json '{"level":<n>,"text":"..."}'`
84+
85+
### Format
86+
87+
- Apply formatting: `format apply --block-id <id> --start <n> --end <n> --inline-json '{"bold":true}'`
88+
- Shortcuts: `format bold`, `format italic`, `format underline`, `format strikethrough`
89+
90+
### Lists
91+
92+
- List items: `lists list`, `lists get`
93+
- Insert list item: `lists insert --node-id <id> --position after --text "..."`
94+
- Modify: `lists indent`, `lists outdent`, `lists set-level`, `lists set-type`, `lists convert-to-text`
95+
96+
### Comments
97+
98+
- Add/reply: `comments add`, `comments reply`
99+
- Read: `comments get`, `comments list`
100+
- Edit/resolve/move: `comments edit`, `comments resolve`, `comments move`, `comments set-internal`
101+
- Delete: `comments delete` (canonical) or `comments remove` (alias)
102+
103+
### Track changes
104+
105+
- List: `track-changes list`, `track-changes get`
106+
- Decide: `track-changes accept`, `track-changes reject`, `track-changes accept-all`, `track-changes reject-all`
107+
108+
### History
109+
110+
- `history get`, `history undo`, `history redo`
111+
112+
### Low-level
113+
114+
- Direct invoke: `call <operationId> --input-json '{...}'` (JSON output only — `--pretty` is not supported)
68115

69116
## JSON/File Payload Flags
70117

71-
Use one of each pair (not both):
118+
Not all `--*-file` variants are available on every command. Use `describe command <name>` to check.
119+
120+
Always supported alongside their `-json` counterpart (use one, not both):
121+
122+
| Flag pair | Available on |
123+
|-----------|-------------|
124+
| `--query-json` / `--query-file` | `find`, `lists list` |
125+
| `--address-json` / `--address-file` | `get-node`, `lists get` |
126+
| `--input-json` / `--input-file` | `call`, `create paragraph` |
127+
| `--at-json` / `--at-file` | `create paragraph` |
72128

73-
- `--query-json` or `--query-file`
74-
- `--target-json` or `--target-file`
75-
- `--address-json` or `--address-file`
76-
- `--input-json` or `--input-file` (for `call`)
129+
`--target-json` is widely available on mutation commands but has **no** `--target-file` counterpart. Use flat flags (`--block-id`, `--start`, `--end`) as an alternative to `--target-json`.
77130

78131
## Output and Global Flags
79132

80133
- Default output is JSON envelope.
81-
- Use `--pretty` for human-readable output.
134+
- Use `--pretty` for human-readable output (not supported by `call`).
82135
- Global flags: `--output <json|pretty>`, `--session <id>`, `--timeout-ms <n>`.
83136
- `<doc>` can be `-` to read DOCX bytes from stdin.
84137

apps/cli/src/__tests__/cli.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { access, copyFile, mkdir, readFile, rm, writeFile } from 'node:fs/promis
33
import { join } from 'node:path';
44
import { run } from '../index';
55
import { resolveListDocFixture, resolveSourceDocFixture } from './fixtures';
6+
import { writeListDocWithoutParaIds } from './unstable-list-fixture';
67

78
type RunResult = {
89
code: number;
@@ -1218,6 +1219,55 @@ describe('superdoc CLI', () => {
12181219
expect(getEnvelope.data.item.address.nodeId).toBe(address.nodeId);
12191220
});
12201221

1222+
test('lists list/get keep list item addresses stable for docs without paraIds in stateless mode', async () => {
1223+
const source = join(TEST_DIR, 'lists-no-paraids-stateless.docx');
1224+
await writeListDocWithoutParaIds(source);
1225+
1226+
const address = await firstListItemAddress(['lists', 'list', source, '--limit', '1']);
1227+
1228+
const getResult = await runCli(['lists', 'get', source, '--address-json', JSON.stringify(address)]);
1229+
expect(getResult.code).toBe(0);
1230+
1231+
const getEnvelope = parseJsonOutput<
1232+
SuccessEnvelope<{
1233+
address: ListItemAddress;
1234+
item: { address: ListItemAddress };
1235+
}>
1236+
>(getResult);
1237+
expect(getEnvelope.data.item.address.nodeId).toBe(address.nodeId);
1238+
1239+
const secondAddress = await firstListItemAddress(['lists', 'list', source, '--limit', '1']);
1240+
expect(secondAddress.nodeId).toBe(address.nodeId);
1241+
});
1242+
1243+
test('lists list/get keep list item addresses stable for docs without paraIds in stateful mode', async () => {
1244+
const source = join(TEST_DIR, 'lists-no-paraids-stateful.docx');
1245+
await writeListDocWithoutParaIds(source);
1246+
1247+
try {
1248+
const openResult = await runCli(['open', source]);
1249+
expect(openResult.code).toBe(0);
1250+
1251+
const address = await firstListItemAddress(['lists', 'list', '--limit', '1']);
1252+
1253+
const getResult = await runCli(['lists', 'get', '--address-json', JSON.stringify(address)]);
1254+
expect(getResult.code).toBe(0);
1255+
1256+
const getEnvelope = parseJsonOutput<
1257+
SuccessEnvelope<{
1258+
address: ListItemAddress;
1259+
item: { address: ListItemAddress };
1260+
}>
1261+
>(getResult);
1262+
expect(getEnvelope.data.item.address.nodeId).toBe(address.nodeId);
1263+
1264+
const secondAddress = await firstListItemAddress(['lists', 'list', '--limit', '1']);
1265+
expect(secondAddress.nodeId).toBe(address.nodeId);
1266+
} finally {
1267+
await runCli(['close', '--discard']);
1268+
}
1269+
});
1270+
12211271
test('lists list pretty prints list rows', async () => {
12221272
const result = await runCli(['lists', 'list', LIST_SAMPLE_DOC, '--limit', '2', '--output', 'pretty']);
12231273
expect(result.code).toBe(0);

0 commit comments

Comments
 (0)