sanity-canvas-skill 0.1.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.
- package/LICENSE +21 -0
- package/README.md +124 -0
- package/README.old.md +141 -0
- package/SKILL.md +225 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +493 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/canvasDiff.d.ts +62 -0
- package/dist/lib/canvasDiff.d.ts.map +1 -0
- package/dist/lib/canvasDiff.js +138 -0
- package/dist/lib/canvasDiff.js.map +1 -0
- package/dist/lib/canvasDocFile.d.ts +40 -0
- package/dist/lib/canvasDocFile.d.ts.map +1 -0
- package/dist/lib/canvasDocFile.js +123 -0
- package/dist/lib/canvasDocFile.js.map +1 -0
- package/dist/lib/canvasPayloads.d.ts +55 -0
- package/dist/lib/canvasPayloads.d.ts.map +1 -0
- package/dist/lib/canvasPayloads.js +89 -0
- package/dist/lib/canvasPayloads.js.map +1 -0
- package/dist/lib/canvasToMarkdown.d.ts +25 -0
- package/dist/lib/canvasToMarkdown.d.ts.map +1 -0
- package/dist/lib/canvasToMarkdown.js +114 -0
- package/dist/lib/canvasToMarkdown.js.map +1 -0
- package/dist/lib/diffAdapter.d.ts +38 -0
- package/dist/lib/diffAdapter.d.ts.map +1 -0
- package/dist/lib/diffAdapter.js +176 -0
- package/dist/lib/diffAdapter.js.map +1 -0
- package/dist/lib/diffPush.d.ts +33 -0
- package/dist/lib/diffPush.d.ts.map +1 -0
- package/dist/lib/diffPush.js +233 -0
- package/dist/lib/diffPush.js.map +1 -0
- package/dist/lib/formatOutline.d.ts +18 -0
- package/dist/lib/formatOutline.d.ts.map +1 -0
- package/dist/lib/formatOutline.js +262 -0
- package/dist/lib/formatOutline.js.map +1 -0
- package/dist/lib/formatPull.d.ts +46 -0
- package/dist/lib/formatPull.d.ts.map +1 -0
- package/dist/lib/formatPull.js +196 -0
- package/dist/lib/formatPull.js.map +1 -0
- package/dist/lib/markdownToCanvas.d.ts +30 -0
- package/dist/lib/markdownToCanvas.d.ts.map +1 -0
- package/dist/lib/markdownToCanvas.js +213 -0
- package/dist/lib/markdownToCanvas.js.map +1 -0
- package/dist/lib/pathParser.d.ts +30 -0
- package/dist/lib/pathParser.d.ts.map +1 -0
- package/dist/lib/pathParser.js +66 -0
- package/dist/lib/pathParser.js.map +1 -0
- package/dist/lib/resolveResource.d.ts +9 -0
- package/dist/lib/resolveResource.d.ts.map +1 -0
- package/dist/lib/resolveResource.js +45 -0
- package/dist/lib/resolveResource.js.map +1 -0
- package/dist/lib/targetedPush.d.ts +148 -0
- package/dist/lib/targetedPush.d.ts.map +1 -0
- package/dist/lib/targetedPush.js +467 -0
- package/dist/lib/targetedPush.js.map +1 -0
- package/dist/types.d.ts +268 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Sanity.io
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# sanity-canvas-skill
|
|
2
|
+
|
|
3
|
+
> ### 🤖 Vibe coded by a [Miriad](https://miriad.app) Team
|
|
4
|
+
> This package was built entirely by a team of AI agents collaborating on Miriad — from spec to implementation to tests. 362 tests, zero hand-written lines.
|
|
5
|
+
|
|
6
|
+
Read and write [Sanity Canvas](https://www.sanity.io/canvas) documents programmatically. Markdown ↔ Canvas Portable Text conversion with diff-based push for concurrent editing.
|
|
7
|
+
|
|
8
|
+
> 📖 **Run `npx sanity-canvas-skill@latest --help` for the full command reference.**
|
|
9
|
+
|
|
10
|
+
## Quick Start
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
export SANITY_TOKEN="your-global-sanity-token"
|
|
14
|
+
|
|
15
|
+
# Pull a Canvas doc to markdown (with block keys for roundtrip)
|
|
16
|
+
npx sanity-canvas-skill@latest --org <orgId> pull <docId> > doc.md
|
|
17
|
+
|
|
18
|
+
# Edit doc.md, then push changes back (diff-based, only patches changed blocks)
|
|
19
|
+
cat doc.md | npx sanity-canvas-skill@latest --org <orgId> push
|
|
20
|
+
|
|
21
|
+
# Preview changes without applying
|
|
22
|
+
cat doc.md | npx sanity-canvas-skill@latest --org <orgId> push --dry-run
|
|
23
|
+
|
|
24
|
+
# Force full overwrite (skips diff)
|
|
25
|
+
cat doc.md | npx sanity-canvas-skill@latest --org <orgId> push --force
|
|
26
|
+
|
|
27
|
+
# List docs
|
|
28
|
+
npx sanity-canvas-skill@latest --org <orgId> list
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Getting a Token
|
|
32
|
+
|
|
33
|
+
You need a **global** Sanity auth token (not project-scoped). The easiest way:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx sanity debug --secrets
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
This prints your CLI's auth token, which works as a global token. Alternatively, create one at [manage.sanity.io](https://manage.sanity.io) → API → Tokens.
|
|
40
|
+
|
|
41
|
+
## Programmatic API
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import {
|
|
45
|
+
canvasToMarkdown,
|
|
46
|
+
markdownToCanvas,
|
|
47
|
+
normalizeKeys,
|
|
48
|
+
computeOps,
|
|
49
|
+
diffPush,
|
|
50
|
+
resolveResource,
|
|
51
|
+
} from 'sanity-canvas-skill'
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Convert Canvas blocks to markdown
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
const markdown = canvasToMarkdown(doc.content, {emitKeys: true})
|
|
58
|
+
// Produces markdown with <!-- k:KEY --> comments for roundtrip
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Convert markdown to Canvas blocks
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
const blocks = markdownToCanvas(markdown)
|
|
65
|
+
// Parses key comments, restores block identity
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Diff two block arrays
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
const normalized = normalizeKeys(originalBlocks, editedBlocks)
|
|
72
|
+
const ops = computeOps(originalBlocks, normalized)
|
|
73
|
+
// [{type: 'set', key: 'abc', block: {...}}, {type: 'insert', afterKey: 'def', blocks: [...]}]
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Full push pipeline
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
import {createClient} from '@sanity/client'
|
|
80
|
+
|
|
81
|
+
const resource = await resolveResource(token, orgId)
|
|
82
|
+
const client = createClient({resource: {type: 'canvas', id: resource.id}})
|
|
83
|
+
|
|
84
|
+
const result = await diffPush(client, markdownFileContent)
|
|
85
|
+
// {docId: '...', ops: [...], summary: '1 changed, 2 inserted, 0 deleted', rev: '...'}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## How it works
|
|
89
|
+
|
|
90
|
+
**Pull** serializes Canvas Portable Text blocks to markdown with `<!-- k:KEY -->` HTML comments that preserve block identity.
|
|
91
|
+
|
|
92
|
+
**Push** is diff-based:
|
|
93
|
+
1. Pulls the current doc from Canvas (fresh revision for locking)
|
|
94
|
+
2. Parses edited markdown back to blocks
|
|
95
|
+
3. Matches blocks by `_key` from key comments
|
|
96
|
+
4. Computes minimal SET/INSERT/UNSET operations via `@sanity/diff`
|
|
97
|
+
5. Applies atomically with `ifRevisionId` optimistic locking
|
|
98
|
+
|
|
99
|
+
Only changed blocks are patched. Notes are preserved. Concurrent edits are detected (409 on conflict).
|
|
100
|
+
|
|
101
|
+
Unknown block types survive roundtrip via opaque base64 encoding (`<!-- canvas:TYPE:BASE64 -->`).
|
|
102
|
+
|
|
103
|
+
## Block type support
|
|
104
|
+
|
|
105
|
+
| Markdown | Canvas Type | Roundtrip |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| Text, headings, blockquotes | `block` | ✅ |
|
|
108
|
+
| Bold, italic, code, links | marks/annotations | ✅ |
|
|
109
|
+
| Lists (bullet, numbered) | `block` with `listItem` | ✅ |
|
|
110
|
+
| Code blocks | `canvasCode` | ✅ |
|
|
111
|
+
| Images | `canvasImage` | ✅ |
|
|
112
|
+
| Horizontal rules | `canvasDivider` | ✅ |
|
|
113
|
+
| Unknown types | opaque base64 | ✅ |
|
|
114
|
+
|
|
115
|
+
## Environment variables
|
|
116
|
+
|
|
117
|
+
| Variable | Required | Description |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| `SANITY_TOKEN` | Yes | Global Sanity auth token (get with `npx sanity debug --secrets`) |
|
|
120
|
+
| `SANITY_ORG` | No | Default org ID (alternative to `--org`) |
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
MIT © [Sanity.io](https://www.sanity.io)
|
package/README.old.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Canvas Writer
|
|
2
|
+
|
|
3
|
+
Create and update [Sanity Canvas](https://www.sanity.io/canvas) documents programmatically.
|
|
4
|
+
|
|
5
|
+
Includes a reusable TypeScript library for markdown ↔ Canvas Portable Text conversion, and a CLI tool for reading/writing Canvas documents directly.
|
|
6
|
+
|
|
7
|
+
## How It Works
|
|
8
|
+
|
|
9
|
+
Canvas documents live in a **resource** (not a regular Sanity project/dataset). The CLI uses `@sanity/client` with a special `resource` config to talk directly to the Canvas API.
|
|
10
|
+
|
|
11
|
+
The conversion library handles mapping between standard markdown and Canvas-specific Portable Text block types:
|
|
12
|
+
|
|
13
|
+
| Markdown | Canvas PTE Type |
|
|
14
|
+
|---|---|
|
|
15
|
+
| Paragraphs, headings, blockquotes | `block` (with style) |
|
|
16
|
+
| Bold, italic, code, strikethrough | `span` marks (`strong`, `em`, `code`, `strike-through`) |
|
|
17
|
+
| Bullet/numbered lists | `block` with `listItem` |
|
|
18
|
+
| Links `[text](url)` | `link` annotation with `href` |
|
|
19
|
+
| Code blocks ` ```lang ``` ` | `canvasCode` (lines as nested blocks) |
|
|
20
|
+
| Images `` | `canvasImage` (ephemeral src) |
|
|
21
|
+
| Horizontal rules `---` | `canvasDivider` |
|
|
22
|
+
|
|
23
|
+
## What an Agent Needs From the User
|
|
24
|
+
|
|
25
|
+
To write to Canvas, an agent needs two things:
|
|
26
|
+
|
|
27
|
+
1. **A Sanity auth token** — a global token (not project-scoped). Set as `SANITY_TOKEN` environment variable. The user can generate one at [sanity.io/manage](https://www.sanity.io/manage).
|
|
28
|
+
|
|
29
|
+
2. **A Canvas resource ID** — identifies which Canvas workspace to write to. The user can find this by:
|
|
30
|
+
- Going to `canvas.sanity.io` and looking at the URL path, OR
|
|
31
|
+
- Using the Canvas API: `GET https://api.sanity.io/vX/canvases?organizationId={orgId}` returns resources with an `id` field.
|
|
32
|
+
|
|
33
|
+
That's it. With those two values, the agent can create, read, update, and delete Canvas documents.
|
|
34
|
+
|
|
35
|
+
## Setup
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# Clone and install
|
|
39
|
+
git clone git@github.com:snorrees/canvas-writer-studio.git
|
|
40
|
+
cd canvas-writer-studio
|
|
41
|
+
pnpm install
|
|
42
|
+
|
|
43
|
+
# Set your Sanity token
|
|
44
|
+
export SANITY_TOKEN="your-global-sanity-token"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## CLI Usage
|
|
48
|
+
|
|
49
|
+
All commands require `--resource <id>` to specify the Canvas resource.
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# List all documents
|
|
53
|
+
pnpm canvas --resource <resourceId> list
|
|
54
|
+
pnpm canvas --resource <resourceId> list --search "query"
|
|
55
|
+
|
|
56
|
+
# Read a document
|
|
57
|
+
pnpm canvas --resource <resourceId> read <docId>
|
|
58
|
+
|
|
59
|
+
# Create a new document from markdown
|
|
60
|
+
echo "# Hello World" | pnpm canvas --resource <resourceId> create "My Document"
|
|
61
|
+
|
|
62
|
+
# Set (replace) content from markdown
|
|
63
|
+
cat article.md | pnpm canvas --resource <resourceId> set <docId> content
|
|
64
|
+
|
|
65
|
+
# Set (replace) notes from markdown
|
|
66
|
+
cat notes.md | pnpm canvas --resource <resourceId> set <docId> notes
|
|
67
|
+
|
|
68
|
+
# Set title
|
|
69
|
+
pnpm canvas --resource <resourceId> set <docId> title "New Title"
|
|
70
|
+
|
|
71
|
+
# Append to content
|
|
72
|
+
cat extra.md | pnpm canvas --resource <resourceId> append <docId> content
|
|
73
|
+
|
|
74
|
+
# Append to notes
|
|
75
|
+
cat extra.md | pnpm canvas --resource <resourceId> append <docId> notes
|
|
76
|
+
|
|
77
|
+
# Delete a document
|
|
78
|
+
pnpm canvas --resource <resourceId> delete <docId>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Library API
|
|
82
|
+
|
|
83
|
+
The conversion functions can be imported directly for use in other tools:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
import {markdownToCanvas} from './src/lib/markdownToCanvas'
|
|
87
|
+
import {canvasToMarkdown} from './src/lib/canvasToMarkdown'
|
|
88
|
+
|
|
89
|
+
// Markdown → Canvas Portable Text blocks
|
|
90
|
+
const blocks = markdownToCanvas('# Hello\n\nA paragraph with **bold**.')
|
|
91
|
+
|
|
92
|
+
// Canvas Portable Text blocks → Markdown
|
|
93
|
+
const markdown = canvasToMarkdown(blocks)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Development
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# Run tests (37 tests covering all block types + roundtrip)
|
|
100
|
+
pnpm test
|
|
101
|
+
|
|
102
|
+
# Type check
|
|
103
|
+
pnpm typecheck
|
|
104
|
+
|
|
105
|
+
# Regenerate types after schema changes
|
|
106
|
+
pnpm typegen
|
|
107
|
+
|
|
108
|
+
# Lint
|
|
109
|
+
pnpm lint
|
|
110
|
+
|
|
111
|
+
# Build Studio
|
|
112
|
+
pnpm build
|
|
113
|
+
|
|
114
|
+
# Deploy Studio
|
|
115
|
+
pnpm deploy
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Project Structure
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
src/
|
|
122
|
+
lib/
|
|
123
|
+
markdownToCanvas.ts # Markdown → Canvas PTE conversion
|
|
124
|
+
canvasToMarkdown.ts # Canvas PTE → Markdown conversion
|
|
125
|
+
__tests__/
|
|
126
|
+
canvasMarkdown.test.ts # 37 vitest tests
|
|
127
|
+
scripts/
|
|
128
|
+
canvas-write.ts # CLI tool
|
|
129
|
+
schemaTypes/ # Canvas document schema (mirrors sanity-io/canvas)
|
|
130
|
+
components/
|
|
131
|
+
SyncToCanvasInput.tsx # Studio sync component (browser-side)
|
|
132
|
+
structure.ts # Custom Studio structure
|
|
133
|
+
sanity.types.ts # Generated types (via pnpm typegen)
|
|
134
|
+
sanity.config.ts # Studio config
|
|
135
|
+
sanity.cli.ts # CLI config + typegen config
|
|
136
|
+
schema.json # Extracted schema (for typegen)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Studio
|
|
140
|
+
|
|
141
|
+
This repo also includes a Sanity Studio deployed at [canvas-writer.sanity.studio](https://canvas-writer.sanity.studio/). The Studio mirrors the Canvas schema and includes a sync component that can push documents to Canvas from the browser. The CLI approach is generally more useful for agents.
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: canvas-skill
|
|
3
|
+
description: "Read and write Sanity Canvas documents. Three-verb CLI with path-based access, diff-based push, and notes diffing."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Canvas Skill
|
|
7
|
+
|
|
8
|
+
Read and write [Sanity Canvas](https://www.sanity.io/canvas) documents. Three verbs (`pull`, `push`, `list`), path-based access to content and notes, diff-based push for concurrent editing.
|
|
9
|
+
|
|
10
|
+
## Prerequisites
|
|
11
|
+
|
|
12
|
+
- **Node.js 18+**
|
|
13
|
+
- **SANITY_TOKEN** — a global Sanity auth token (not project-scoped). Get one with `npx sanity debug --secrets`, or from [manage.sanity.io](https://manage.sanity.io) → API → Tokens.
|
|
14
|
+
- **Organization ID** — the Sanity org that owns the Canvas.
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Pull a Canvas doc to markdown
|
|
20
|
+
npx sanity-canvas-skill@latest --org <orgId> pull <docId>
|
|
21
|
+
|
|
22
|
+
# Pull just the content (no notes, no frontmatter)
|
|
23
|
+
npx sanity-canvas-skill@latest --org <orgId> pull <docId> content
|
|
24
|
+
|
|
25
|
+
# Edit, then push changes back (diff-based — only changed blocks are patched)
|
|
26
|
+
cat doc.md | npx sanity-canvas-skill@latest --org <orgId> push
|
|
27
|
+
|
|
28
|
+
# List all Canvas docs
|
|
29
|
+
npx sanity-canvas-skill@latest --org <orgId> list
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Three Verbs
|
|
33
|
+
|
|
34
|
+
| Verb | What it does |
|
|
35
|
+
|------|-------------|
|
|
36
|
+
| `pull` | Read document data → stdout as markdown |
|
|
37
|
+
| `push` | Write markdown from stdin → document (diff-based) |
|
|
38
|
+
| `list` | List documents in a Canvas resource |
|
|
39
|
+
|
|
40
|
+
## Path Model
|
|
41
|
+
|
|
42
|
+
A Canvas document is a tree. The CLI exposes it through **paths**:
|
|
43
|
+
|
|
44
|
+
| Path | What it addresses |
|
|
45
|
+
|------|-------------------|
|
|
46
|
+
| (none) | Entire document (content + notes + title) |
|
|
47
|
+
| `content` | The content block array |
|
|
48
|
+
| `notes` | All notes |
|
|
49
|
+
| `notes[0]` | A single note (by index) |
|
|
50
|
+
| `notes[_key=="k"]` | A single note (by key) |
|
|
51
|
+
| `title` | The document title string |
|
|
52
|
+
|
|
53
|
+
Paths are positional arguments: `npx sanity-canvas-skill@latest pull <docId> <path>`
|
|
54
|
+
|
|
55
|
+
## Pull
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Full document (frontmatter + content + notes)
|
|
59
|
+
npx sanity-canvas-skill@latest --org <orgId> pull <docId>
|
|
60
|
+
|
|
61
|
+
# Content blocks only (with key comments)
|
|
62
|
+
npx sanity-canvas-skill@latest --org <orgId> pull <docId> content
|
|
63
|
+
|
|
64
|
+
# All notes with headers
|
|
65
|
+
npx sanity-canvas-skill@latest --org <orgId> pull <docId> notes
|
|
66
|
+
|
|
67
|
+
# Single note by index or key
|
|
68
|
+
npx sanity-canvas-skill@latest --org <orgId> pull <docId> 'notes[0]'
|
|
69
|
+
npx sanity-canvas-skill@latest --org <orgId> pull <docId> 'notes[_key=="abc"]'
|
|
70
|
+
|
|
71
|
+
# Just the title
|
|
72
|
+
npx sanity-canvas-skill@latest --org <orgId> pull <docId> title
|
|
73
|
+
|
|
74
|
+
# Range reads (content or note body)
|
|
75
|
+
npx sanity-canvas-skill@latest --org <orgId> pull <docId> content --from <key> --to <key>
|
|
76
|
+
npx sanity-canvas-skill@latest --org <orgId> pull <docId> content --from <key>
|
|
77
|
+
npx sanity-canvas-skill@latest --org <orgId> pull <docId> content --range 0-10
|
|
78
|
+
npx sanity-canvas-skill@latest --org <orgId> pull <docId> content --range -5
|
|
79
|
+
|
|
80
|
+
# Outline — structural overview (heading hierarchy + block counts)
|
|
81
|
+
npx sanity-canvas-skill@latest --org <orgId> pull <docId> --outline
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Pull Output Format
|
|
85
|
+
|
|
86
|
+
Full document pull produces:
|
|
87
|
+
|
|
88
|
+
```markdown
|
|
89
|
+
---
|
|
90
|
+
id: n36GqUekRrdauhzK15EtOM
|
|
91
|
+
org: oSyH1iET5
|
|
92
|
+
rev: 2TJuQTedaNK0J2PgZg0gcu
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
<!-- k:abc123 -->
|
|
96
|
+
# Document Title
|
|
97
|
+
|
|
98
|
+
<!-- k:def456 -->
|
|
99
|
+
First paragraph.
|
|
100
|
+
|
|
101
|
+
---notes---
|
|
102
|
+
|
|
103
|
+
## Note: "Research Sources" [fact] <!-- k:note1 -->
|
|
104
|
+
|
|
105
|
+
<!-- k:n1_b0 -->
|
|
106
|
+
Note body here.
|
|
107
|
+
|
|
108
|
+
## Note: "Style Guide" [style] <!-- k:note2 -->
|
|
109
|
+
|
|
110
|
+
<!-- k:n2_b0 -->
|
|
111
|
+
Another note body.
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
- **Frontmatter**: document ID, org, revision (for conflict detection)
|
|
115
|
+
- **`<!-- k:KEY -->`**: block identity preserved through roundtrip
|
|
116
|
+
- **`---notes---`**: delimiter between content and notes
|
|
117
|
+
- **`## Note: "Title" [category]`**: note headers with key comment
|
|
118
|
+
|
|
119
|
+
## Push
|
|
120
|
+
|
|
121
|
+
Push is **diff-based by default** — only changed blocks are patched.
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
# Full document push (diffs both content AND notes)
|
|
125
|
+
cat doc.md | npx sanity-canvas-skill@latest --org <orgId> push
|
|
126
|
+
|
|
127
|
+
# Preview changes without applying
|
|
128
|
+
cat doc.md | npx sanity-canvas-skill@latest --org <orgId> push --dry-run
|
|
129
|
+
|
|
130
|
+
# Force full overwrite (no diff)
|
|
131
|
+
cat doc.md | npx sanity-canvas-skill@latest --org <orgId> push --force
|
|
132
|
+
|
|
133
|
+
# Targeted writes
|
|
134
|
+
npx sanity-canvas-skill@latest --org <orgId> push <docId> content --keys < blocks.md
|
|
135
|
+
npx sanity-canvas-skill@latest --org <orgId> push <docId> content --after <key> < new.md
|
|
136
|
+
npx sanity-canvas-skill@latest --org <orgId> push <docId> content --after-index 3 < new.md
|
|
137
|
+
npx sanity-canvas-skill@latest --org <orgId> push <docId> content --append < new.md
|
|
138
|
+
npx sanity-canvas-skill@latest --org <orgId> push <docId> title "New Title"
|
|
139
|
+
|
|
140
|
+
# Create a new document
|
|
141
|
+
cat content.md | npx sanity-canvas-skill@latest --org <orgId> push --new "Document Title"
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### How Push Works
|
|
145
|
+
|
|
146
|
+
1. Pulls the current document from Canvas (fresh `_rev` for locking)
|
|
147
|
+
2. Parses your edited markdown back to Portable Text blocks
|
|
148
|
+
3. Normalizes keys (restores span-level keys that markdown roundtrip regenerates)
|
|
149
|
+
4. Computes minimal operations via `@sanity/diff`: SET, INSERT, UNSET
|
|
150
|
+
5. Diffs both `content` AND `notes` arrays
|
|
151
|
+
6. Applies all operations in a single atomic transaction with `ifRevisionId`
|
|
152
|
+
|
|
153
|
+
This means:
|
|
154
|
+
- **Only changed blocks are patched** — unchanged content is untouched
|
|
155
|
+
- **Notes are diffed too** — edit a note body, only that note is patched
|
|
156
|
+
- **Concurrent edits are safe** — 409 on conflict, re-pull and try again
|
|
157
|
+
- **New blocks** (without `<!-- k:KEY -->`) are inserted at the correct position
|
|
158
|
+
|
|
159
|
+
## Programmatic API
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
import {
|
|
163
|
+
canvasToMarkdown,
|
|
164
|
+
markdownToCanvas,
|
|
165
|
+
normalizeKeys,
|
|
166
|
+
computeOps,
|
|
167
|
+
diffPush,
|
|
168
|
+
parseCanvasDoc,
|
|
169
|
+
serializeCanvasDoc,
|
|
170
|
+
resolveResourceId,
|
|
171
|
+
} from 'sanity-canvas-skill'
|
|
172
|
+
import {createClient} from '@sanity/client'
|
|
173
|
+
|
|
174
|
+
// Setup
|
|
175
|
+
const resourceId = await resolveResourceId(token, orgId)
|
|
176
|
+
const client = createClient({
|
|
177
|
+
resource: {type: 'canvas', id: resourceId},
|
|
178
|
+
token,
|
|
179
|
+
apiVersion: '2025-10-01',
|
|
180
|
+
useCdn: false,
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
// Pull → markdown
|
|
184
|
+
const doc = await client.fetch('*[_id == $id][0]{ _id, _rev, title, content, notes }', {id: docId})
|
|
185
|
+
const markdown = canvasToMarkdown(doc.content, {emitKeys: true})
|
|
186
|
+
|
|
187
|
+
// Markdown → blocks
|
|
188
|
+
const blocks = markdownToCanvas(markdown)
|
|
189
|
+
|
|
190
|
+
// Diff two block arrays
|
|
191
|
+
const normalized = normalizeKeys(originalBlocks, editedBlocks)
|
|
192
|
+
const ops = computeOps(originalBlocks, normalized)
|
|
193
|
+
|
|
194
|
+
// Full push pipeline (pull + diff + apply atomically)
|
|
195
|
+
const result = await diffPush(client, markdownFileContent)
|
|
196
|
+
// result: {docId, ops, summary, rev}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Block Type Support
|
|
200
|
+
|
|
201
|
+
| Markdown | Canvas Type | Roundtrip |
|
|
202
|
+
|---|---|---|
|
|
203
|
+
| Paragraphs, headings, blockquotes | `block` (with style) | ✅ Lossless |
|
|
204
|
+
| Bold, italic, code, strikethrough | `span` marks | ✅ Lossless |
|
|
205
|
+
| Bullet/numbered lists | `block` with `listItem` | ✅ Lossless |
|
|
206
|
+
| Links `[text](url)` | `link` annotation | ✅ Lossless |
|
|
207
|
+
| Code blocks `` ```lang ``` `` | `canvasCode` | ✅ Lossless |
|
|
208
|
+
| Images `` | `canvasImage` | ✅ Lossless |
|
|
209
|
+
| Horizontal rules `---` | `canvasDivider` | ✅ Lossless |
|
|
210
|
+
| Unknown types | `<!-- canvas:TYPE:BASE64 -->` | ✅ Opaque preservation |
|
|
211
|
+
|
|
212
|
+
## Key Concepts
|
|
213
|
+
|
|
214
|
+
- **Key comments** (`<!-- k:KEY -->`) — preserve block identity. Don't remove them unless you want to delete that block.
|
|
215
|
+
- **Generated keys** — new blocks get `g_` prefixed keys (e.g., `g_0`). Replaced by Canvas-generated keys on push.
|
|
216
|
+
- **Optimistic locking** — push uses `ifRevisionId`. If the doc changed, you get a 409. Re-pull and try again.
|
|
217
|
+
- **Opaque blocks** — unknown types preserved as `<!-- canvas:TYPE:BASE64 -->`. Don't edit these.
|
|
218
|
+
- **Notes diffing** — notes are diffed the same way as content. Edit a note body, only that note is patched.
|
|
219
|
+
|
|
220
|
+
## Environment Variables
|
|
221
|
+
|
|
222
|
+
| Variable | Required | Description |
|
|
223
|
+
|---|---|---|
|
|
224
|
+
| `SANITY_TOKEN` | Yes | Global Sanity auth token (get with `npx sanity debug --secrets`) |
|
|
225
|
+
| `SANITY_ORG` | No | Default organization ID (alternative to `--org`) |
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|