Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ ol document get <urlId> --raw # outputs raw markdown
ol document open <urlId> # opens in browser
ol document create --title "Title" --collection <id> --file doc.md --publish
ol document update <urlId> --file updated.md
ol document update <urlId> --publish # publishes a draft
ol document update <urlId> --publish --collection <id> # publishes a draft that has no collection
ol document delete <urlId> --confirm
ol document move <urlId> --collection <target-id>
ol document archive <urlId>
Expand Down
2 changes: 2 additions & 0 deletions skills/outline-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ ol doc create --title "Title" --parent <ref> --text "# Content" # Nest under pa
ol doc create --title "Title" --collection <id> --file ./doc.md
ol doc update <id> --title "New Title"
ol doc update <id> --file ./updated.md
ol doc update <id> --publish # Publish a draft (create makes drafts unless --publish)
ol doc update <id> --publish --collection <ref> # Publish a draft that has no collection (web UI drafts)
ol doc move <id> --collection <target-id> # Move to collection root
ol doc move <id> --parent <ref> # Nest under parent (collection inferred)
ol doc archive <id>
Expand Down
81 changes: 81 additions & 0 deletions src/commands/commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,87 @@ describe('document commands', () => {
mockExit.mockRestore()
})

it('document update --publish publishes without changing title or text', async () => {
const { apiRequest } = await import('../lib/api.js')
;(apiRequest as ReturnType<typeof vi.fn>).mockResolvedValue({
data: {
id: 'doc-id',
title: 'Draft',
urlId: 'abc123',
collectionId: 'c1',
updatedAt: '2024-01-01T00:00:00Z',
},
})

const { registerDocumentCommand } = await import('./document.js')
const program = createTestProgram(registerDocumentCommand)

await program.parseAsync(['node', 'ol', 'document', 'update', 'abc123', '--publish'])

expect(apiRequest).toHaveBeenLastCalledWith('documents.update', {
id: 'doc-id',
publish: true,
})
expect(lines(log)[0]).toContain('Published: Draft')
})

it('document update --publish --collection sends the resolved collectionId', async () => {
const { apiRequest } = await import('../lib/api.js')
const mockApiRequest = apiRequest as ReturnType<typeof vi.fn>
mockApiRequest.mockImplementation((endpoint: string) => {
if (endpoint === 'collections.info') {
return Promise.resolve({ data: { id: 'col-id', name: 'Engineering' } })
}
return Promise.resolve({ data: { id: 'doc-id', title: 'Draft', urlId: 'abc123' } })
})

const { registerDocumentCommand } = await import('./document.js')
const program = createTestProgram(registerDocumentCommand)

await program.parseAsync([
'node',
'ol',
'document',
'update',
'abc123',
'--publish',
'--collection',
'colABC1',
])

expect(mockApiRequest).toHaveBeenLastCalledWith('documents.update', {
id: 'doc-id',
publish: true,
collectionId: 'col-id',
})
})

it('document update --collection without --publish errors before any API call', async () => {
const mockExit = mockProcessExit()
const errorSpy = captureConsole('error')
const { apiRequest } = await import('../lib/api.js')
;(apiRequest as ReturnType<typeof vi.fn>).mockClear()

const { registerDocumentCommand } = await import('./document.js')
const program = createTestProgram(registerDocumentCommand)

await expect(
program.parseAsync([
'node',
'ol',
'document',
'update',
'abc123',
'--collection',
'colABC1',
]),
).rejects.toThrow('process.exit(1)')

expect(lines(errorSpy).join(' ')).toContain('--collection requires --publish')
expect(apiRequest).not.toHaveBeenCalled()
mockExit.mockRestore()
})

it('document move with --parent infers collectionId from parent', async () => {
const { apiRequest } = await import('../lib/api.js')
const mockApiRequest = apiRequest as ReturnType<typeof vi.fn>
Expand Down
17 changes: 16 additions & 1 deletion src/commands/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,18 @@ export function registerDocumentCommand(program: Command): void {
.option('--title <title>', 'New title')
.option('--text <text>', 'New body (markdown)')
.option('--file <path>', 'Read markdown from file')
.option('--publish', 'Publish a draft')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This won't work for drafts that don't belong to a collection, which you can create from the web UI. For those, Outline's authorizeDocumentPublish requires a collectionId in the request and rejects with collectionId is required to publish a draft without collection (source), and there's currently no way to pass one from the CLI. Drafts made with ol doc create always have a collection because it requires --collection or --parent, which is why it works in your testing.

Could you add a --collection <ref> option to doc update that resolves via resolveCollectionId and sends collectionId alongside publish: true? documents.update already accepts collectionId for this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, thanks.

Added in fa1c940:

doc update now takes --collection <ref>, resolved via resolveCollectionId, and sends it as collectionId alongside publish: true. Outline only uses collectionId on update when publishing, so --collection without --publish now errors (pointing to doc move) instead of silently doing nothing. Checked against Outline 1.11.0 with a draft created without a collection: --publish alone gets collectionId is required to publish a draft without collection, and --publish --collection <ref> publishes it into that collection.

.option('--collection <ref>', 'Collection to publish into (drafts without one only)')
.option('--json', 'Output JSON')
.action(async (id: string, opts) => {
if (opts.collection && !opts.publish) {
console.error(
formatError('MISSING_OPTION', '--collection requires --publish.', [
'Use doc move to move a document to another collection',
]),
)
process.exit(1)
}
const resolvedId = await resolveDocumentId(id)
const body: Record<string, unknown> = { id: resolvedId }

Expand All @@ -208,13 +218,18 @@ export function registerDocumentCommand(program: Command): void {
if (rawText) body.text = rawText
if (opts.title) body.title = opts.title
}
if (opts.publish) body.publish = true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: with --publish the output further down still prints Updated: <title>. Printing Published: <title> in that case would make it clearer the publish happened.

if (opts.collection) {
body.collectionId = await resolveCollectionId(opts.collection)
}

const { data } = await apiRequest<Document>('documents.update', body)

if (opts.json) {
outputItem(data, formatDoc, essentialKeys, { json: true })
} else {
console.log(chalk.green(`Updated: ${data.title}`), chalk.dim(data.urlId))
const verb = opts.publish ? 'Published' : 'Updated'
console.log(chalk.green(`${verb}: ${data.title}`), chalk.dim(data.urlId))
}
})

Expand Down
2 changes: 2 additions & 0 deletions src/lib/skills/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ ol doc create --title "Title" --parent <ref> --text "# Content" # Nest under pa
ol doc create --title "Title" --collection <id> --file ./doc.md
ol doc update <id> --title "New Title"
ol doc update <id> --file ./updated.md
ol doc update <id> --publish # Publish a draft (create makes drafts unless --publish)
ol doc update <id> --publish --collection <ref> # Publish a draft that has no collection (web UI drafts)
ol doc move <id> --collection <target-id> # Move to collection root
ol doc move <id> --parent <ref> # Nest under parent (collection inferred)
ol doc archive <id>
Expand Down
Loading