Skip to content

Commit 9a8afc2

Browse files
committed
Merge branch 'main' into stable
2 parents b2cacbe + e455644 commit 9a8afc2

41 files changed

Lines changed: 3128 additions & 641 deletions

Some content is hidden

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

.github/workflows/release-sdk.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,39 @@ jobs:
182182
packages-dir: packages/sdk/langs/python/dist/
183183
skip-existing: true
184184

185+
# -------------------------------------------------------------------
186+
# Sync labs agent with newly released SDK (main/@next only)
187+
# -------------------------------------------------------------------
188+
sync-labs-agent:
189+
needs: auto-release
190+
if: >-
191+
always()
192+
&& github.event_name == 'push'
193+
&& github.ref_name == 'main'
194+
&& needs.auto-release.outputs.version != ''
195+
runs-on: ubuntu-24.04
196+
steps:
197+
- name: Generate token for superdoc-labs
198+
id: labs_token
199+
uses: actions/create-github-app-token@v2
200+
with:
201+
app-id: ${{ secrets.APP_ID }}
202+
private-key: ${{ secrets.APP_PRIVATE_KEY }}
203+
owner: superdoc-dev
204+
repositories: labs
205+
206+
- name: Dispatch labs SDK update
207+
env:
208+
GH_TOKEN: ${{ steps.labs_token.outputs.token }}
209+
run: |
210+
VERSION="${{ needs.auto-release.outputs.version }}"
211+
echo "Dispatching update-agent-sdk.yml on superdoc-dev/labs main for @superdoc-dev/sdk@$VERSION"
212+
gh workflow run update-agent-sdk.yml \
213+
--repo superdoc-dev/labs \
214+
--ref main \
215+
--field sdk_version="$VERSION" \
216+
--field deploy_after_update=true
217+
185218
# -------------------------------------------------------------------
186219
# Manual fallback (workflow_dispatch)
187220
# -------------------------------------------------------------------

apps/cli/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,11 @@
3636
},
3737
"dependencies": {
3838
"@hocuspocus/provider": "catalog:",
39+
"@liveblocks/client": "catalog:",
40+
"@liveblocks/yjs": "catalog:",
3941
"fast-glob": "catalog:",
4042
"happy-dom": "catalog:",
43+
"ws": "catalog:",
4144
"y-websocket": "catalog:",
4245
"yjs": "catalog:"
4346
},
@@ -47,6 +50,7 @@
4750
"@superdoc/super-editor": "workspace:*",
4851
"@types/bun": "catalog:",
4952
"@types/node": "catalog:",
53+
"@types/ws": "^8.5.13",
5054
"superdoc": "workspace:*",
5155
"typescript": "catalog:"
5256
},

apps/cli/scripts/export-sdk-contract.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
} from '../src/cli/operation-set';
3030
import type { CliOnlyOperation } from '../src/cli/types';
3131
import { CLI_ONLY_OPERATION_DEFINITIONS } from '../src/cli/cli-only-operation-definitions';
32+
import { RESPONSE_ENVELOPE_KEY } from '../src/cli/operation-hints';
3233
import { HOST_PROTOCOL_VERSION, HOST_PROTOCOL_FEATURES, HOST_PROTOCOL_NOTIFICATIONS } from '../src/host/protocol';
3334

3435
// ---------------------------------------------------------------------------
@@ -105,6 +106,10 @@ function buildSdkContract() {
105106
requiresDocumentContext: cliRequiresDocumentContext(cliOpId),
106107
docRequirement: metadata.docRequirement,
107108

109+
// Response envelope key — tells SDKs which property to unwrap from the CLI response.
110+
// null means result is spread across top-level keys (no unwrapping needed).
111+
responseEnvelopeKey: docApiId ? (RESPONSE_ENVELOPE_KEY[docApiId] ?? null) : null,
112+
108113
// Transport plane
109114
params: metadata.params.map((p) => {
110115
const spec: Record<string, unknown> = {

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

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,5 +101,127 @@ describe('normalizeContextMetadata', () => {
101101
expect(result.sessionType).toBe('local');
102102
expect(result.collaboration).toBeUndefined();
103103
});
104+
105+
test('preserves Liveblocks collab profile with publicApiKey', () => {
106+
const metadata = makeMetadata({
107+
sessionType: 'collab',
108+
collaboration: {
109+
providerType: 'liveblocks',
110+
documentId: 'lb-room-123',
111+
publicApiKey: 'pk_test_xxx',
112+
} as any,
113+
});
114+
const result = normalizeContextMetadata(metadata);
115+
expect(result.sessionType).toBe('collab');
116+
expect(result.collaboration).toEqual({
117+
providerType: 'liveblocks',
118+
documentId: 'lb-room-123',
119+
publicApiKey: 'pk_test_xxx',
120+
});
121+
});
122+
123+
test('preserves Liveblocks collab profile with authEndpoint', () => {
124+
const metadata = makeMetadata({
125+
sessionType: 'collab',
126+
collaboration: {
127+
providerType: 'liveblocks',
128+
documentId: 'lb-room-456',
129+
authEndpoint: 'https://example.com/auth',
130+
authHeadersEnv: 'LB_HEADERS',
131+
} as any,
132+
});
133+
const result = normalizeContextMetadata(metadata);
134+
expect(result.sessionType).toBe('collab');
135+
expect(result.collaboration).toEqual({
136+
providerType: 'liveblocks',
137+
documentId: 'lb-room-456',
138+
authEndpoint: 'https://example.com/auth',
139+
authHeadersEnv: 'LB_HEADERS',
140+
});
141+
});
142+
143+
test('rejects Liveblocks profile with both auth modes', () => {
144+
const metadata = makeMetadata({
145+
sessionType: 'collab',
146+
collaboration: {
147+
providerType: 'liveblocks',
148+
documentId: 'lb-room',
149+
publicApiKey: 'pk_xxx',
150+
authEndpoint: 'https://x',
151+
} as any,
152+
});
153+
const result = normalizeContextMetadata(metadata);
154+
expect(result.sessionType).toBe('local');
155+
expect(result.collaboration).toBeUndefined();
156+
});
157+
158+
test('rejects Liveblocks profile with neither auth mode', () => {
159+
const metadata = makeMetadata({
160+
sessionType: 'collab',
161+
collaboration: {
162+
providerType: 'liveblocks',
163+
documentId: 'lb-room',
164+
} as any,
165+
});
166+
const result = normalizeContextMetadata(metadata);
167+
expect(result.sessionType).toBe('local');
168+
expect(result.collaboration).toBeUndefined();
169+
});
170+
171+
test('rejects malformed Liveblocks profile (missing documentId)', () => {
172+
const metadata = makeMetadata({
173+
sessionType: 'collab',
174+
collaboration: {
175+
providerType: 'liveblocks',
176+
publicApiKey: 'pk_xxx',
177+
} as any,
178+
});
179+
const result = normalizeContextMetadata(metadata);
180+
expect(result.sessionType).toBe('local');
181+
});
182+
183+
test('rejects Liveblocks profile with relative authEndpoint', () => {
184+
const metadata = makeMetadata({
185+
sessionType: 'collab',
186+
collaboration: {
187+
providerType: 'liveblocks',
188+
documentId: 'lb-room',
189+
authEndpoint: '/api/auth',
190+
} as any,
191+
});
192+
const result = normalizeContextMetadata(metadata);
193+
expect(result.sessionType).toBe('local');
194+
expect(result.collaboration).toBeUndefined();
195+
});
196+
197+
test('rejects Liveblocks profile with authHeadersEnv but no authEndpoint', () => {
198+
const metadata = makeMetadata({
199+
sessionType: 'collab',
200+
collaboration: {
201+
providerType: 'liveblocks',
202+
documentId: 'lb-room',
203+
publicApiKey: 'pk_xxx',
204+
authHeadersEnv: 'MY_HEADERS',
205+
} as any,
206+
});
207+
const result = normalizeContextMetadata(metadata);
208+
expect(result.sessionType).toBe('local');
209+
expect(result.collaboration).toBeUndefined();
210+
});
211+
212+
test('rejects Liveblocks profile with invalid authHeadersEnv name', () => {
213+
const metadata = makeMetadata({
214+
sessionType: 'collab',
215+
collaboration: {
216+
providerType: 'liveblocks',
217+
documentId: 'lb-room',
218+
authEndpoint: 'https://example.com/auth',
219+
authHeadersEnv: '123-invalid',
220+
} as any,
221+
});
222+
const result = normalizeContextMetadata(metadata);
223+
expect(result.sessionType).toBe('local');
224+
expect(result.collaboration).toBeUndefined();
225+
});
104226
});
105227
});

apps/cli/src/__tests__/lib/find-query.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,26 @@ describe('resolveFindQuery', () => {
3434
test('treats null --select-json as provided and validates the payload shape', async () => {
3535
await expect(resolveFindQuery(makeParsed({ 'select-json': 'null' }))).rejects.toThrow(CliError);
3636
});
37+
38+
test('normalizes shorthand node selector from --select-json', async () => {
39+
const query = await resolveFindQuery(makeParsed({ 'select-json': JSON.stringify({ type: 'paragraph' }) }));
40+
expect(query).toEqual({
41+
select: { type: 'node', nodeType: 'paragraph' },
42+
});
43+
});
44+
45+
test('passes through canonical node selector from --select-json', async () => {
46+
const query = await resolveFindQuery(
47+
makeParsed({ 'select-json': JSON.stringify({ type: 'node', nodeType: 'heading' }) }),
48+
);
49+
expect(query).toEqual({
50+
select: { type: 'node', nodeType: 'heading' },
51+
});
52+
});
53+
54+
test('rejects invalid shorthand node type from --select-json', async () => {
55+
await expect(resolveFindQuery(makeParsed({ 'select-json': JSON.stringify({ type: 'magic' }) }))).rejects.toThrow(
56+
CliError,
57+
);
58+
});
3759
});

apps/cli/src/__tests__/lib/validate-type-spec.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, expect, test } from 'bun:test';
22
import { validateValueAgainstTypeSpec } from '../../lib/operation-args';
33
import { CliError } from '../../lib/errors';
44
import type { CliTypeSpec } from '../../cli/types';
5+
import { CLI_OPERATION_METADATA } from '../../cli/operation-params';
56

67
describe('validateValueAgainstTypeSpec – oneOf const enumeration', () => {
78
const schema: CliTypeSpec = {
@@ -91,3 +92,53 @@ describe('validateValueAgainstTypeSpec – enum branch', () => {
9192
}
9293
});
9394
});
95+
96+
// ---------------------------------------------------------------------------
97+
// doc.find select schema override
98+
// ---------------------------------------------------------------------------
99+
100+
describe('doc.find select schema — accepts canonical and shorthand forms', () => {
101+
const metadata = CLI_OPERATION_METADATA['doc.find'];
102+
const selectParam = metadata.params.find((p) => p.name === 'select');
103+
const schema = selectParam?.schema;
104+
105+
if (!schema) throw new Error('doc.find metadata missing select param with schema');
106+
107+
test('accepts canonical text selector', () => {
108+
expect(() => validateValueAgainstTypeSpec({ type: 'text', pattern: 'hello' }, schema, 'select')).not.toThrow();
109+
});
110+
111+
test('accepts canonical text selector with all optional fields', () => {
112+
expect(() =>
113+
validateValueAgainstTypeSpec(
114+
{ type: 'text', pattern: 'hello', mode: 'regex', caseSensitive: true },
115+
schema,
116+
'select',
117+
),
118+
).not.toThrow();
119+
});
120+
121+
test('accepts canonical node selector', () => {
122+
expect(() => validateValueAgainstTypeSpec({ type: 'node', nodeType: 'heading' }, schema, 'select')).not.toThrow();
123+
});
124+
125+
test('accepts canonical node selector with kind', () => {
126+
expect(() => validateValueAgainstTypeSpec({ type: 'node', kind: 'block' }, schema, 'select')).not.toThrow();
127+
});
128+
129+
test('accepts shorthand node selector', () => {
130+
expect(() => validateValueAgainstTypeSpec({ type: 'paragraph' }, schema, 'select')).not.toThrow();
131+
});
132+
133+
test('accepts shorthand for inline node type', () => {
134+
expect(() => validateValueAgainstTypeSpec({ type: 'hyperlink' }, schema, 'select')).not.toThrow();
135+
});
136+
137+
test('rejects invalid shorthand node type', () => {
138+
expect(() => validateValueAgainstTypeSpec({ type: 'magic' }, schema, 'select')).toThrow(CliError);
139+
});
140+
141+
test('rejects text selector missing required pattern', () => {
142+
expect(() => validateValueAgainstTypeSpec({ type: 'text' }, schema, 'select')).toThrow(CliError);
143+
});
144+
});

apps/cli/src/cli/cli-only-operation-definitions.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,13 @@ export const CLI_ONLY_OPERATION_DEFINITIONS: Record<CliOnlyOperation, CliOnlyOpe
6363
dirty: { type: 'boolean' },
6464
collaboration: {
6565
type: 'object',
66+
description: 'Collaboration summary (auth config redacted).',
6667
properties: {
68+
providerType: { type: 'string', enum: ['y-websocket', 'hocuspocus', 'liveblocks'] },
6769
documentId: { type: 'string' },
68-
url: { type: 'string' },
70+
url: { type: 'string', description: 'WebSocket URL (websocket providers only).' },
6971
},
72+
required: ['providerType', 'documentId'],
7073
},
7174
bootstrap: {
7275
type: 'object',
@@ -173,10 +176,13 @@ export const CLI_ONLY_OPERATION_DEFINITIONS: Record<CliOnlyOperation, CliOnlyOpe
173176
},
174177
collaboration: {
175178
type: 'object',
179+
description: 'Collaboration summary (auth config redacted).',
176180
properties: {
181+
providerType: { type: 'string', enum: ['y-websocket', 'hocuspocus', 'liveblocks'] },
177182
documentId: { type: 'string' },
178-
url: { type: 'string' },
183+
url: { type: 'string', description: 'WebSocket URL (websocket providers only).' },
179184
},
185+
required: ['providerType', 'documentId'],
180186
},
181187
openedAt: { type: 'string' },
182188
updatedAt: { type: 'string' },
@@ -249,6 +255,16 @@ export const CLI_ONLY_OPERATION_DEFINITIONS: Record<CliOnlyOperation, CliOnlyOpe
249255
sessionType: { type: 'string' },
250256
dirty: { type: 'boolean' },
251257
revision: { type: 'number' },
258+
collaboration: {
259+
type: 'object',
260+
description: 'Collaboration summary (auth config redacted).',
261+
properties: {
262+
providerType: { type: 'string', enum: ['y-websocket', 'hocuspocus', 'liveblocks'] },
263+
documentId: { type: 'string' },
264+
url: { type: 'string', description: 'WebSocket URL (websocket providers only).' },
265+
},
266+
required: ['providerType', 'documentId'],
267+
},
252268
},
253269
},
254270
},

0 commit comments

Comments
 (0)