@notionhq/custom-blocks 0.0.78 → 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.
Files changed (36) hide show
  1. package/HOST.md +9 -2
  2. package/README.md +4 -2
  3. package/dist/bridge/SandboxBridge.d.ts +1 -1
  4. package/dist/bridge/SandboxBridge.js +1 -1
  5. package/dist/bridge/messages/connect.d.ts +1 -1
  6. package/dist/bridge/messages/connect.d.ts.map +1 -1
  7. package/dist/bridge/messages/connect.js +2 -8
  8. package/dist/bridge/messages/sandboxToHost.d.ts +1 -1
  9. package/dist/host/createCustomBlockHost.d.ts +1 -1
  10. package/dist/host/createCustomBlockHost.d.ts.map +1 -1
  11. package/dist/host/createCustomBlockHost.js +17 -4
  12. package/dist/host/lifecycle/{ready.d.ts → protocolVersion.d.ts} +1 -1
  13. package/dist/host/lifecycle/protocolVersion.d.ts.map +1 -0
  14. package/dist/version.js +1 -1
  15. package/docs/data-sources.md +7 -5
  16. package/docs/vite-plugin.md +46 -0
  17. package/package.json +1 -2
  18. package/src/bridge/SandboxBridge.ts +1 -1
  19. package/src/bridge/messages/connect.ts +2 -9
  20. package/src/host/createCustomBlockHost.ts +25 -5
  21. package/bin/cli/attach.js +0 -180
  22. package/bin/cli/cli.js +0 -248
  23. package/bin/cli/create.js +0 -106
  24. package/bin/cli/datasources.js +0 -255
  25. package/bin/cli/deploy.js +0 -109
  26. package/bin/cli/ids.js +0 -13
  27. package/bin/cli/ntn.js +0 -95
  28. package/bin/cli/pullData.js +0 -76
  29. package/bin/cli/pullManifest.js +0 -158
  30. package/bin/cli/target.js +0 -95
  31. package/bin/src/bridge/dataSources/propertySchema.js +0 -148
  32. package/bin/src/bridge/manifest.js +0 -40
  33. package/dist/host/lifecycle/ready.d.ts.map +0 -1
  34. package/docs/manifest.md +0 -42
  35. /package/dist/host/lifecycle/{ready.js → protocolVersion.js} +0 -0
  36. /package/src/host/lifecycle/{ready.ts → protocolVersion.ts} +0 -0
package/bin/cli/cli.js DELETED
@@ -1,248 +0,0 @@
1
- #!/usr/bin/env node
2
- import { attach } from "./attach.js";
3
- import { create } from "./create.js";
4
- import { connect, getDataSources } from "./datasources.js";
5
- import { deploy } from "./deploy.js";
6
- import { pullData } from "./pullData.js";
7
- import { pullManifest } from "./pullManifest.js";
8
- const HELP = `
9
- ncblock — Notion Custom Block SDK CLI
10
-
11
- Usage:
12
- ncblock connect <database-url-or-id> [options]
13
- ncblock connect [--block <id>] [--bind <key>=<ds-id>] [options]
14
- ncblock data_sources get <block-id-or-url>
15
- ncblock deploy <dist-path> [--block <id>] [options]
16
- ncblock pull_data <id-or-url> [--limit <n>] [--out <path>]
17
- ncblock attach <block-id-or-url> [--capability <key>]
18
- ncblock create [--parent <id-or-url>] [--capability <key>]
19
-
20
- Commands:
21
- connect Pull the data source schema and PATCH the block(s) in .notion/target.json
22
- data_sources get Read data source bindings from a custom block
23
- deploy Run \`ntn custom deploy\` for each block; appends --block IDs into target.json on success
24
- pull_data Resolve an ID/URL to a data source; print its schema + N rows as JSON to stdout
25
- attach Point a custom block at the worker's definition (from \`ntn workers capabilities\`)
26
- create Create a custom block (private page parent by default) and attach it
27
-
28
- Options:
29
- --block <id> Block ID/URL (repeatable; accumulates into target.json's block_id[])
30
- --bind <key>=<id> Bind a manifest key to a data source ID (repeatable)
31
- --capability <key> customBlock capability key to attach (required if the worker has more than one)
32
- --parent <id> Parent ID/URL for \`create\` (a page hosts the block; omit for a new private page)
33
- --limit <n> Rows to pull for \`pull_data\` (default: 25, max: 100)
34
- --env <name> Notion env (defaults to target.json's env, or "production")
35
- --key <key> Data source key in manifest (default: "default") — for connect <id> only
36
- --out <path> Output path — manifest for connect, JSON file for pull_data
37
- --manifest <path> Path to custom_blocks.json (default: "./custom_blocks.json")
38
- --dry-run Print the intended action without performing it
39
- --json Emit machine-parseable JSON output
40
- --help, -h Show this help message
41
-
42
- Examples:
43
- ncblock connect <db-url>
44
- ncblock connect --block <block-id> --bind default=<db-id>
45
- ncblock data_sources get <block-id>
46
- ncblock deploy dist/
47
- ncblock pull_data <db-url> --limit 50 --out sample.json
48
- ncblock attach <block-url> --capability hello
49
- ncblock create
50
- `;
51
- function parseArgs(argv) {
52
- const flags = {};
53
- const bindings = {};
54
- const blocks = [];
55
- const positional = [];
56
- let i = 2;
57
- while (i < argv.length) {
58
- const arg = argv[i];
59
- if (arg === "--key") {
60
- flags.key = argv[++i];
61
- }
62
- else if (arg === "--out") {
63
- flags.out = argv[++i];
64
- }
65
- else if (arg === "--dry-run") {
66
- flags.dryRun = true;
67
- }
68
- else if (arg === "--json") {
69
- flags.jsonOutput = true;
70
- }
71
- else if (arg === "--quiet") {
72
- flags.quiet = true;
73
- }
74
- else if (arg === "--manifest") {
75
- flags.manifest = argv[++i];
76
- }
77
- else if (arg === "--capability") {
78
- flags.capability = argv[++i];
79
- }
80
- else if (arg === "--parent") {
81
- flags.parent = argv[++i];
82
- }
83
- else if (arg === "--limit") {
84
- flags.limit = argv[++i];
85
- }
86
- else if (arg === "--env") {
87
- flags.env = argv[++i];
88
- }
89
- else if (arg === "--block") {
90
- const value = argv[++i];
91
- if (!value) {
92
- console.error("Error: --block requires a value.\n");
93
- console.error("Usage: --block <id-or-url>");
94
- process.exit(1);
95
- }
96
- blocks.push(value);
97
- }
98
- else if (arg === "--bind") {
99
- const pair = argv[++i];
100
- const eq = pair?.indexOf("=");
101
- if (!pair || eq === undefined || eq < 1) {
102
- console.error(`Error: invalid --bind value: ${pair}\n`);
103
- console.error("Expected format: --bind <key>=<data-source-id>");
104
- process.exit(1);
105
- }
106
- bindings[pair.slice(0, eq)] = pair.slice(eq + 1);
107
- }
108
- else if (arg === "--help" || arg === "-h") {
109
- flags.help = true;
110
- }
111
- else if (!arg.startsWith("-")) {
112
- positional.push(arg);
113
- }
114
- i++;
115
- }
116
- return { flags, bindings, blocks, positional };
117
- }
118
- function main() {
119
- const { flags, bindings, blocks, positional } = parseArgs(process.argv);
120
- if (flags.help || positional.length === 0) {
121
- console.log(HELP);
122
- process.exit(0);
123
- }
124
- const [command, subcommand, ...rest] = positional;
125
- // `manifest pull` is kept as an internal/advanced command — not advertised
126
- // in HELP, but still callable for users who want pull without the PATCH
127
- // step. Prefer `ncblock connect <id>`.
128
- if (command === "manifest" && subcommand === "pull") {
129
- const idOrUrl = rest[0];
130
- if (!idOrUrl) {
131
- console.error("Error: missing <id-or-url> argument.\n");
132
- console.error("Usage: ncblock manifest pull <id-or-url> [options]");
133
- process.exit(1);
134
- }
135
- pullManifest({
136
- idOrUrl,
137
- key: flags.key,
138
- out: flags.out,
139
- dryRun: flags.dryRun,
140
- quiet: flags.quiet,
141
- });
142
- return;
143
- }
144
- if (command === "connect") {
145
- // `ncblock connect <database-url-or-id>` pulls the schema first then
146
- // runs the binding/PATCH flow against target.json. The positional
147
- // must point at a database (or a data source); ID-kind classification
148
- // (block vs view vs wrong-workspace) happens upstream in `init.ts`.
149
- connect({
150
- idOrUrl: subcommand,
151
- blockIds: blocks,
152
- bindings,
153
- key: flags.key,
154
- out: flags.out,
155
- manifest: flags.manifest,
156
- env: flags.env,
157
- dryRun: flags.dryRun,
158
- jsonOutput: flags.jsonOutput,
159
- quiet: flags.quiet,
160
- });
161
- return;
162
- }
163
- if (command === "data_sources" && subcommand === "get") {
164
- const idOrUrl = rest[0];
165
- if (!idOrUrl) {
166
- console.error("Error: missing <block-id-or-url> argument.\n");
167
- console.error("Usage: ncblock data_sources get <block-id-or-url>");
168
- process.exit(1);
169
- }
170
- getDataSources(idOrUrl);
171
- return;
172
- }
173
- if (command === "deploy") {
174
- const distPath = subcommand;
175
- if (!distPath) {
176
- console.error("Error: missing <dist-path> argument.\n");
177
- console.error("Usage: ncblock deploy <dist-path> [--block <id>...] [--env <name>]");
178
- process.exit(1);
179
- }
180
- deploy({
181
- distPath,
182
- blockIds: blocks,
183
- env: flags.env,
184
- dryRun: flags.dryRun,
185
- jsonOutput: flags.jsonOutput,
186
- });
187
- return;
188
- }
189
- // The commands below route their API calls through ntn.ts, which resolves
190
- // the env from `process.env.ENV` (falling back to target.json). Honor an
191
- // explicit --env by setting it here so every ntn call inherits it.
192
- if (flags.env) {
193
- process.env.ENV = flags.env;
194
- }
195
- if (command === "pull_data") {
196
- const idOrUrl = subcommand;
197
- if (!idOrUrl) {
198
- console.error("Error: missing <id-or-url> argument.\n");
199
- console.error("Usage: ncblock pull_data <id-or-url> [--limit <n>] [--out <path>]");
200
- process.exit(1);
201
- }
202
- const limitRaw = flags.limit;
203
- const limit = limitRaw === undefined ? undefined : Number(limitRaw);
204
- if (limit !== undefined && !Number.isFinite(limit)) {
205
- console.error(`Error: --limit must be a number, got '${limitRaw}'.`);
206
- process.exit(1);
207
- }
208
- pullData({
209
- idOrUrl,
210
- limit,
211
- out: flags.out,
212
- });
213
- return;
214
- }
215
- if (command === "attach") {
216
- const idOrUrl = subcommand;
217
- if (!idOrUrl) {
218
- console.error("Error: missing <block-id-or-url> argument.\n");
219
- console.error("Usage: ncblock attach <block-id-or-url> [--capability <key>]");
220
- process.exit(1);
221
- }
222
- attach({
223
- idOrUrl,
224
- capability: flags.capability,
225
- jsonOutput: flags.jsonOutput,
226
- });
227
- return;
228
- }
229
- if (command === "create") {
230
- create({
231
- parent: flags.parent,
232
- capability: flags.capability,
233
- jsonOutput: flags.jsonOutput,
234
- });
235
- return;
236
- }
237
- console.error(`Unknown command: ${positional.join(" ")}\n`);
238
- console.log(HELP);
239
- process.exit(1);
240
- }
241
- try {
242
- main();
243
- }
244
- catch (error) {
245
- const message = error instanceof Error ? error.message : String(error);
246
- console.error(`\nError: ${message}\n`);
247
- process.exit(1);
248
- }
package/bin/cli/create.js DELETED
@@ -1,106 +0,0 @@
1
- import { attach } from "./attach.js";
2
- import { extractId, formatUuid } from "./ids.js";
3
- import { ntnApi, ntnApiPatch, ntnApiPost } from "./ntn.js";
4
- const DEFAULT_PAGE_TITLE = "Custom block";
5
- function is404(error) {
6
- return error instanceof Error && error.message.includes("404");
7
- }
8
- /**
9
- * Classify a `--parent` ID/URL. A database or data source means the user wants
10
- * a custom *view* under a collection (the parental-nesting flow), which the
11
- * public API can't express today — see `create` below. Anything else is
12
- * treated as a page/block to host the new block.
13
- */
14
- function classifyParent(uuid) {
15
- try {
16
- const db = ntnApi(`/v1/databases/${uuid}`);
17
- if (db.object === "database") {
18
- return "database";
19
- }
20
- }
21
- catch (error) {
22
- if (!is404(error)) {
23
- throw error;
24
- }
25
- }
26
- try {
27
- const ds = ntnApi(`/v1/data_sources/${uuid}`);
28
- if (ds.object === "data_source") {
29
- return "data_source";
30
- }
31
- }
32
- catch (error) {
33
- if (!is404(error)) {
34
- throw error;
35
- }
36
- }
37
- return "page";
38
- }
39
- /** Create a private (workspace-level) page to host the new block. */
40
- function createPrivatePage() {
41
- console.log("\nCreating a private page to host the block…");
42
- const page = ntnApiPost("/v1/pages", {
43
- parent: { workspace: true },
44
- properties: {
45
- title: { title: [{ text: { content: DEFAULT_PAGE_TITLE } }] },
46
- },
47
- });
48
- if (!page.id) {
49
- throw new Error("Page creation returned no id.");
50
- }
51
- const id = formatUuid(page.id);
52
- console.log(` Created page ${id}`);
53
- return id;
54
- }
55
- /** Append an empty custom block under `pageId` and return its dashed UUID. */
56
- function createCustomBlock(pageId) {
57
- console.log(` Creating custom block under ${pageId}…`);
58
- const result = ntnApiPatch(`/v1/blocks/${pageId}/children`, {
59
- children: [{ type: "custom_block", custom_block: {} }],
60
- });
61
- const created = result.results?.[0];
62
- if (!created?.id) {
63
- throw new Error("Custom block creation returned no block id.");
64
- }
65
- return formatUuid(created.id);
66
- }
67
- /**
68
- * Create a new custom block and attach it to the worker's definition.
69
- *
70
- * - No `--parent` → create a private page, then a block under it.
71
- * - Page `--parent` → create a block under that page.
72
- * - Database/data source `--parent` → a custom block as a collection view. The
73
- * public API has no `custom` view type and the parent-nesting the backend
74
- * uses isn't derivable from this repo, so we error rather than guess.
75
- */
76
- export function create(options) {
77
- let pageId;
78
- if (!options.parent) {
79
- pageId = createPrivatePage();
80
- }
81
- else {
82
- const parentUuid = formatUuid(extractId(options.parent));
83
- const kind = classifyParent(parentUuid);
84
- if (kind === "database" || kind === "data_source") {
85
- console.error(`Error: --parent ${parentUuid} is a ${kind.replace("_", " ")}. Creating a custom block as a collection view isn't supported yet.\n`);
86
- console.error(" The public API has no `custom` view type; the backend does the parent nesting. Create the custom block in Notion, then run `ncblock attach <block-id-or-url>`.");
87
- process.exit(1);
88
- }
89
- pageId = parentUuid;
90
- }
91
- const blockId = createCustomBlock(pageId);
92
- // Hand off to attach for definition resolution + the PATCH. It re-fetches
93
- // the block (verifying type) and prints its own success line.
94
- const attached = attach({
95
- idOrUrl: blockId,
96
- capability: options.capability,
97
- jsonOutput: options.jsonOutput,
98
- emitResult: false,
99
- });
100
- if (options.jsonOutput) {
101
- console.log(JSON.stringify({ action: "create", page_id: pageId, block_id: attached }, null, "\t"));
102
- }
103
- else {
104
- console.log(`✓ Created custom block ${attached} on page ${pageId}\n`);
105
- }
106
- }
@@ -1,255 +0,0 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { resolve as resolvePath } from "node:path";
3
- import { extractId, formatUuid } from "./ids.js";
4
- import { ntnApi, ntnApiPatch } from "./ntn.js";
5
- import { pullManifest } from "./pullManifest.js";
6
- import { mergeTarget, readTarget, writeTarget } from "./target.js";
7
- // ── get ─────────────────────────────────────────────────────────
8
- export function getDataSources(idOrUrl) {
9
- const uuid = formatUuid(extractId(idOrUrl));
10
- console.log(`\nFetching block ${uuid}…\n`);
11
- const block = ntnApi(`/v1/blocks/${uuid}`);
12
- if (block.type !== "custom_block") {
13
- console.error(`Error: block ${uuid} is type "${block.type}", not "custom_block".`);
14
- process.exit(1);
15
- }
16
- const dataSources = block.custom_block?.data_sources ?? [];
17
- if (dataSources.length === 0) {
18
- console.log("No data sources bound to this custom block.\n");
19
- return;
20
- }
21
- console.log(JSON.stringify(dataSources, null, "\t"));
22
- console.log("");
23
- }
24
- function patchBlock(idOrUrl, dataSources, jsonOutput) {
25
- const uuid = formatUuid(extractId(idOrUrl));
26
- if (!jsonOutput) {
27
- console.log(`\nUpdating block ${uuid}…\n`);
28
- }
29
- const result = ntnApiPatch(`/v1/blocks/${uuid}`, {
30
- custom_block: { data_sources: dataSources },
31
- });
32
- const updated = result.custom_block?.data_sources ?? [];
33
- if (jsonOutput) {
34
- console.log(JSON.stringify({ block_id: uuid, data_sources: updated }, null, "\t"));
35
- }
36
- else if (updated.length === 0) {
37
- console.log("✓ Cleared all data source bindings.\n");
38
- }
39
- else {
40
- console.log(`✓ Set ${updated.length} data source binding(s):\n`);
41
- console.log(JSON.stringify(updated, null, "\t"));
42
- console.log("");
43
- }
44
- return result;
45
- }
46
- function targetToMappings(dataSources) {
47
- return Object.entries(dataSources).map(([key, ds]) => {
48
- const mapping = {
49
- key,
50
- data_source_id: ds.data_source_id,
51
- };
52
- if (Object.keys(ds.property_ids_by_key).length > 0) {
53
- mapping.property_ids_by_key = ds.property_ids_by_key;
54
- }
55
- return mapping;
56
- });
57
- }
58
- function emitDryRun(jsonOutput, payload) {
59
- if (jsonOutput) {
60
- console.log(JSON.stringify(payload, null, "\t"));
61
- }
62
- else {
63
- console.log("\nDry run — would PATCH:\n");
64
- console.log(JSON.stringify(payload, null, "\t"));
65
- console.log("");
66
- }
67
- }
68
- export function loadManifest(manifestPath) {
69
- const path = resolvePath(process.cwd(), manifestPath ?? "custom_blocks.json");
70
- if (!existsSync(path)) {
71
- console.error(`Error: manifest not found at ${path}\n`);
72
- console.error("Run 'ncblock connect <database-url-or-id>' to generate it, or pass --manifest <path>.");
73
- process.exit(1);
74
- }
75
- let manifest;
76
- try {
77
- manifest = JSON.parse(readFileSync(path, "utf-8"));
78
- }
79
- catch {
80
- console.error(`Error: failed to parse ${path}\n`);
81
- console.error("custom_blocks.json must be valid JSON.");
82
- process.exit(1);
83
- }
84
- return { path, manifest };
85
- }
86
- /**
87
- * Resolve a manifest data source entry's property IDs by querying the live
88
- * data source schema and matching declared `name` fields.
89
- *
90
- * Notion column names are case-sensitive. Property IDs are URL-encoded in
91
- * API responses (e.g. `%3EPdk` → `>Pdk`); we decode before storing them.
92
- */
93
- export function resolvePropertyIdsByKey(args) {
94
- const { dataSourceId, manifestKey, manifestProps } = args;
95
- const uuid = formatUuid(extractId(dataSourceId));
96
- const schema = ntnApi(`/v1/data_sources/${uuid}`);
97
- const schemaProps = schema.properties ?? {};
98
- // Build a name-keyed index. The API map is keyed by name already, but be
99
- // defensive and use the `name` field on each entry so we don't depend on
100
- // the map's key shape.
101
- const byName = new Map();
102
- for (const prop of Object.values(schemaProps)) {
103
- byName.set(prop.name, prop);
104
- }
105
- const out = {};
106
- for (const [propKey, manifestProp] of Object.entries(manifestProps)) {
107
- const match = byName.get(manifestProp.name);
108
- if (!match) {
109
- console.error(`Error: column '${manifestProp.name}' (${manifestProp.type}) not found in data source ${uuid} (manifest key '${manifestKey}').\n` +
110
- ` Either rename a column in Notion to '${manifestProp.name}', or edit .notion/target.json's data_sources.${manifestKey}.property_ids_by_key manually.`);
111
- process.exit(1);
112
- }
113
- if (match.type !== manifestProp.type) {
114
- console.error(`Error: column '${manifestProp.name}' is type '${match.type}' in the data source, but the manifest declares '${manifestProp.type}'.\n` +
115
- ` Change the Notion column type, or update custom_blocks.json so the types line up.`);
116
- process.exit(1);
117
- }
118
- out[propKey] = decodeURIComponent(match.id);
119
- }
120
- return out;
121
- }
122
- function buildFromManifest(manifestPath, bindings) {
123
- const { manifest } = loadManifest(manifestPath);
124
- const keys = Object.keys(manifest.dataSources ?? {});
125
- if (keys.length === 0) {
126
- console.error("Error: manifest has no data source keys.\n");
127
- console.error("Run 'ncblock connect <database-url-or-id>' to populate custom_blocks.json.");
128
- process.exit(1);
129
- }
130
- const missing = keys.filter(k => !bindings[k]);
131
- if (missing.length > 0) {
132
- console.error(`Error: missing --bind for manifest key(s): ${missing.join(", ")}\n`);
133
- console.error("Use --bind <key>=<data-source-id> for each data source in the manifest.");
134
- process.exit(1);
135
- }
136
- return keys.map(key => {
137
- const entry = {
138
- key,
139
- data_source_id: bindings[key],
140
- };
141
- const props = manifest.dataSources[key]?.properties;
142
- if (props && Object.keys(props).length > 0) {
143
- entry.property_ids_by_key = resolvePropertyIdsByKey({
144
- dataSourceId: bindings[key],
145
- manifestKey: key,
146
- manifestProps: props,
147
- });
148
- }
149
- return entry;
150
- });
151
- }
152
- export function connect(options) {
153
- // When a positional database URL/ID is supplied, pull the schema first.
154
- // `pullManifest` writes both custom_blocks.json and `.notion/target.json`,
155
- // so the connect step below just reads from target.json as usual.
156
- if (options.idOrUrl) {
157
- pullManifest({
158
- idOrUrl: options.idOrUrl,
159
- key: options.key,
160
- out: options.out,
161
- dryRun: options.dryRun,
162
- quiet: options.quiet,
163
- });
164
- if (options.dryRun) {
165
- return;
166
- }
167
- }
168
- const existingTarget = readTarget();
169
- // Bindings come from --bind flags OR from target.json's data_sources
170
- // (where `manifest pull` stashes the resolved data_source_id). Flags win.
171
- const bindings = { ...options.bindings };
172
- if (Object.keys(bindings).length === 0 && existingTarget) {
173
- for (const [key, ds] of Object.entries(existingTarget.data_sources)) {
174
- bindings[key] = ds.data_source_id;
175
- }
176
- }
177
- if (Object.keys(bindings).length === 0) {
178
- console.error("Error: no data sources to connect — pass a database URL/ID as a positional argument, --bind <key>=<data-source-id>, or wire one up with 'ncblock connect <database-url-or-id>'.\n");
179
- process.exit(1);
180
- }
181
- // Blocks: --block flags + whatever's already in target.json. Flags don't
182
- // "win" here, they accumulate — same as how deploy treats blocks.
183
- const blockIds = Array.from(new Set([
184
- ...(existingTarget?.block_id ?? []),
185
- ...options.blockIds.map(id => formatUuid(extractId(id))),
186
- ]));
187
- if (blockIds.length === 0) {
188
- // When a positional was given, pull already ran and recorded the data
189
- // source — that's useful on its own (e.g. for a view ID where no
190
- // custom_block is associated yet). Exit cleanly so init doesn't bail
191
- // the scaffold; the user wires a block later with --block.
192
- if (options.idOrUrl) {
193
- if (!options.jsonOutput) {
194
- console.log("\nData source recorded. Wire a custom block later with `ncblock connect --block <block-id>`.\n");
195
- }
196
- return;
197
- }
198
- console.error("Error: no blocks to connect — pass --block <id>, or run 'ncblock connect --block <id>' to add one to target.json.\n");
199
- process.exit(1);
200
- }
201
- const { manifest } = loadManifest(options.manifest);
202
- const manifestKeys = Object.keys(manifest.dataSources ?? {});
203
- const unknown = Object.keys(bindings).filter(k => !manifestKeys.includes(k));
204
- if (unknown.length > 0) {
205
- console.error(`Error: binding key(s) not declared in the manifest: ${unknown.join(", ")}\n`);
206
- console.error(`Manifest keys: ${manifestKeys.join(", ") || "(none)"}. Add the key to custom_blocks.json or fix the --bind key.`);
207
- process.exit(1);
208
- }
209
- const resolvedBindings = {};
210
- for (const [key, dataSourceId] of Object.entries(bindings)) {
211
- const props = manifest.dataSources[key]?.properties ?? {};
212
- const property_ids_by_key = Object.keys(props).length > 0
213
- ? resolvePropertyIdsByKey({
214
- dataSourceId,
215
- manifestKey: key,
216
- manifestProps: props,
217
- })
218
- : {};
219
- resolvedBindings[key] = {
220
- data_source_id: formatUuid(extractId(dataSourceId)),
221
- property_ids_by_key,
222
- };
223
- }
224
- const merged = mergeTarget(existingTarget, {
225
- env: options.env,
226
- block_id: blockIds,
227
- data_sources: resolvedBindings,
228
- });
229
- const mappings = targetToMappings(merged.data_sources);
230
- if (options.dryRun) {
231
- emitDryRun(options.jsonOutput, {
232
- action: "connect",
233
- target: merged,
234
- block_patches: blockIds.map(id => ({
235
- block_id: id,
236
- data_sources: mappings,
237
- })),
238
- });
239
- return;
240
- }
241
- writeTarget(merged);
242
- if (!options.jsonOutput) {
243
- console.log(`\n✓ Wrote .notion/target.json`);
244
- }
245
- for (const id of blockIds) {
246
- patchBlock(id, mappings, options.jsonOutput);
247
- }
248
- if (options.jsonOutput) {
249
- console.log(JSON.stringify({
250
- action: "connect",
251
- target: merged,
252
- blocks_updated: blockIds,
253
- }, null, "\t"));
254
- }
255
- }