create-ncblock 0.0.16 → 0.0.18
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/package.json +1 -1
- package/scripts/init.ts +66 -45
- package/scripts/scaffold-assets/AGENTS.md +10 -18
- package/scripts/utils/classifyId.ts +227 -0
- package/scripts/utils/resolveInitArgs.ts +50 -0
- package/sdk-version.json +1 -1
package/package.json
CHANGED
package/scripts/init.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execSync, spawnSync } from "child_process"
|
|
2
2
|
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "fs"
|
|
3
|
-
import {
|
|
3
|
+
import { dirname, resolve } from "path"
|
|
4
4
|
import {
|
|
5
5
|
clearScreenDown,
|
|
6
6
|
createInterface,
|
|
@@ -9,6 +9,12 @@ import {
|
|
|
9
9
|
moveCursor,
|
|
10
10
|
} from "readline"
|
|
11
11
|
import { fileURLToPath } from "url"
|
|
12
|
+
import {
|
|
13
|
+
type Classified,
|
|
14
|
+
classifyPositionalId,
|
|
15
|
+
EXIT_USER_ID_MISTAKE,
|
|
16
|
+
} from "./utils/classifyId"
|
|
17
|
+
import { resolveInitArgs } from "./utils/resolveInitArgs"
|
|
12
18
|
import {
|
|
13
19
|
getTemplateByName,
|
|
14
20
|
getTemplates,
|
|
@@ -18,8 +24,8 @@ import {
|
|
|
18
24
|
|
|
19
25
|
/**
|
|
20
26
|
* Seed `.notion/target.json` with whatever's known at scaffold time. Empty
|
|
21
|
-
* defaults are intentional — subsequent
|
|
22
|
-
*
|
|
27
|
+
* defaults are intentional — subsequent `ncblock connect` / `deploy` calls
|
|
28
|
+
* merge into this file.
|
|
23
29
|
*/
|
|
24
30
|
type InitialTarget = {
|
|
25
31
|
env: string
|
|
@@ -57,6 +63,10 @@ function writeInitialTarget(
|
|
|
57
63
|
*
|
|
58
64
|
* Env is sourced from target.json (written by writeInitialTarget before
|
|
59
65
|
* any ncblock call) — no need to thread ENV through the spawn.
|
|
66
|
+
*
|
|
67
|
+
* Exits init immediately on `EXIT_USER_ID_MISTAKE` so we don't fall through
|
|
68
|
+
* to "Ready! Next steps:" with a broken target.json. Any other non-zero exit
|
|
69
|
+
* is thrown so the caller's try/catch can suppress without forwarding stderr.
|
|
60
70
|
*/
|
|
61
71
|
function runNcblock(dest: string, args: string[]): void {
|
|
62
72
|
const result = spawnSync(
|
|
@@ -64,9 +74,10 @@ function runNcblock(dest: string, args: string[]): void {
|
|
|
64
74
|
[resolve(dest, "node_modules/ncblock/bin/cli/cli.js"), ...args],
|
|
65
75
|
{ cwd: dest, stdio: "inherit" },
|
|
66
76
|
)
|
|
77
|
+
if (result.status === EXIT_USER_ID_MISTAKE) {
|
|
78
|
+
process.exit(EXIT_USER_ID_MISTAKE)
|
|
79
|
+
}
|
|
67
80
|
if (result.status !== 0) {
|
|
68
|
-
// Child already printed a user-friendly message; throw so the caller's
|
|
69
|
-
// try/catch can suppress without us having to forward stderr.
|
|
70
81
|
throw new Error(
|
|
71
82
|
`ncblock ${args.join(" ")} exited with code ${result.status}`,
|
|
72
83
|
)
|
|
@@ -563,18 +574,32 @@ async function main() {
|
|
|
563
574
|
args.install as boolean | undefined,
|
|
564
575
|
)
|
|
565
576
|
|
|
566
|
-
//
|
|
567
|
-
//
|
|
568
|
-
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
(installDeps
|
|
577
|
+
// Precedence: --collection > --block/positional > prompt. Classification
|
|
578
|
+
// downstream walks a positional block/view ID up to its parent database.
|
|
579
|
+
const blockIdArg = args.block as string | undefined
|
|
580
|
+
const promptedCollection =
|
|
581
|
+
args.collection === undefined && blockIdArg === undefined && installDeps
|
|
572
582
|
? (await ask("Database URL/ID (enter to skip):", "skip")).trim()
|
|
573
|
-
: "skip")
|
|
574
|
-
const collectionId =
|
|
575
|
-
collectionAnswer && collectionAnswer !== "skip"
|
|
576
|
-
? collectionAnswer
|
|
577
583
|
: undefined
|
|
584
|
+
const collectionOverride =
|
|
585
|
+
(args.collection as string | undefined) ??
|
|
586
|
+
(promptedCollection && promptedCollection !== "skip"
|
|
587
|
+
? promptedCollection
|
|
588
|
+
: undefined)
|
|
589
|
+
|
|
590
|
+
const resolved = resolveInitArgs({
|
|
591
|
+
collection: collectionOverride,
|
|
592
|
+
block: blockIdArg,
|
|
593
|
+
envBlockId: process.env.BLOCK_ID,
|
|
594
|
+
envName: process.env.ENV,
|
|
595
|
+
})
|
|
596
|
+
const classified: Classified | null = resolved.idToClassify
|
|
597
|
+
? classifyPositionalId(resolved.idToClassify, resolved.env)
|
|
598
|
+
: null
|
|
599
|
+
// Explicit --collection wins over classification; classify normalizes
|
|
600
|
+
// the positional input to a dashed UUID and falls back to the raw value.
|
|
601
|
+
const blockId = classified?.blockId ?? resolved.blockId
|
|
602
|
+
const databaseId = resolved.databaseId ?? classified?.databaseId
|
|
578
603
|
|
|
579
604
|
printSummary({
|
|
580
605
|
dest,
|
|
@@ -615,20 +640,19 @@ async function main() {
|
|
|
615
640
|
step("Initialized git repo")
|
|
616
641
|
}
|
|
617
642
|
|
|
618
|
-
|
|
619
|
-
(args.block as string | undefined) || process.env.BLOCK_ID || undefined
|
|
620
|
-
const envName = process.env.ENV
|
|
621
|
-
|
|
622
|
-
// Always seed .notion/target.json so every project starts in a known
|
|
623
|
-
// state. env / block_id are populated from what we have; data_sources
|
|
624
|
-
// gets filled in by `manifest pull` if it runs.
|
|
625
|
-
writeInitialTarget(dest, { env: envName, blockId })
|
|
643
|
+
writeInitialTarget(dest, { env: resolved.env, blockId })
|
|
626
644
|
step(`Seeded .notion/target.json`)
|
|
645
|
+
if (classified?.kind === "view") {
|
|
646
|
+
console.log(
|
|
647
|
+
` ${c.dim}(view ID resolved — using as block id, database ${databaseId ?? "(none)"})${c.reset}`,
|
|
648
|
+
)
|
|
649
|
+
}
|
|
627
650
|
|
|
651
|
+
let connected = false
|
|
628
652
|
if (installDeps) {
|
|
629
653
|
// Close the readline interface before shelling out — otherwise it
|
|
630
654
|
// holds onto stdin/stdout and the inherited stdio in child processes
|
|
631
|
-
// gets buffered, which makes
|
|
655
|
+
// gets buffered, which makes ncblock output land out of order.
|
|
632
656
|
closePromptInterface()
|
|
633
657
|
|
|
634
658
|
console.log("")
|
|
@@ -636,27 +660,16 @@ async function main() {
|
|
|
636
660
|
execSync(`${pm} ${installArgs}`, { cwd: dest, stdio: "inherit" })
|
|
637
661
|
step("Installed dependencies")
|
|
638
662
|
|
|
639
|
-
|
|
640
|
-
// block URL to a manifest entry AND merges the resolved data_source_id
|
|
641
|
-
// into target.json's data_sources. Then `connect` (no flags needed —
|
|
642
|
-
// reads everything from target.json) PATCHes the block if one's set.
|
|
643
|
-
let pulled = false
|
|
644
|
-
if (collectionId) {
|
|
663
|
+
if (databaseId) {
|
|
645
664
|
console.log("")
|
|
646
665
|
try {
|
|
647
|
-
runNcblock(dest, ["
|
|
648
|
-
step(
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
if (pulled && blockId) {
|
|
656
|
-
console.log("")
|
|
657
|
-
try {
|
|
658
|
-
runNcblock(dest, ["connect"])
|
|
659
|
-
step("Wired block bindings (.notion/target.json + PATCH)")
|
|
666
|
+
runNcblock(dest, ["connect", databaseId, "--quiet"])
|
|
667
|
+
step(
|
|
668
|
+
blockId
|
|
669
|
+
? "Wired block bindings (custom_blocks.json + .notion/target.json + PATCH)"
|
|
670
|
+
: "Recorded data source in custom_blocks.json + .notion/target.json",
|
|
671
|
+
)
|
|
672
|
+
connected = true
|
|
660
673
|
} catch {
|
|
661
674
|
// connect already printed a user-friendly message
|
|
662
675
|
}
|
|
@@ -674,13 +687,21 @@ async function main() {
|
|
|
674
687
|
}
|
|
675
688
|
console.log(` ${pm} run build`)
|
|
676
689
|
if (deployCommand) {
|
|
677
|
-
console.log(` ${deployCommand}
|
|
690
|
+
console.log(` ${deployCommand}`)
|
|
678
691
|
} else {
|
|
679
692
|
console.log(
|
|
680
|
-
`\n Then, go back to the instructions in Notion and paste the ${c.bold}ntn deploy${c.reset} command
|
|
693
|
+
`\n Then, go back to the instructions in Notion and paste the ${c.bold}ntn deploy${c.reset} command.`,
|
|
681
694
|
)
|
|
682
695
|
}
|
|
683
696
|
|
|
697
|
+
if (installDeps && !connected) {
|
|
698
|
+
console.log(
|
|
699
|
+
`\n ${c.yellow}No data source connected yet.${c.reset} To wire one up:`,
|
|
700
|
+
)
|
|
701
|
+
console.log(` npx ncblock connect <database-url-or-id>`)
|
|
702
|
+
}
|
|
703
|
+
console.log("")
|
|
704
|
+
|
|
684
705
|
closePromptInterface()
|
|
685
706
|
}
|
|
686
707
|
|
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
A **Notion custom view** — a sandboxed `<iframe>` rendered inside a Notion block. The iframe is the entire viewport; the only channel to the host is a `postMessage` bridge wrapped by `ncblock`.
|
|
4
4
|
|
|
5
|
+
## Connecting to a Notion database
|
|
6
|
+
|
|
7
|
+
If `custom_blocks.json` or `.notion/target.json` doesn't yet reference a datasource, ask the user if they want to connect a database by url or id, then run:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx ncblock connect <database-url-or-id>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
That writes `custom_blocks.json`, `.notion/target.json`, and PATCHes any blocks already listed there. Discover other commands with `npx ncblock --help`.
|
|
14
|
+
|
|
5
15
|
## Talking to the host
|
|
6
16
|
|
|
7
17
|
- Always use the React hooks from `ncblock`. Never call `window.parent.postMessage` directly — the SDK owns the protocol.
|
|
@@ -32,7 +42,6 @@ Hooks at a glance:
|
|
|
32
42
|
|
|
33
43
|
- Mount into `<div id="root">` — `useCustomBlockAutoResize` looks for that exact id.
|
|
34
44
|
- `custom_blocks.json` at the project root defines the data contract. The SDK's Vite plugin serves it in dev; `ntn` bundles it into the deploy.
|
|
35
|
-
- `custom_blocks.json` is the **portable contract** — no IDs. `.notion/target.json` holds the per-deploy IDs (block IDs, data-source IDs, resolved property IDs) and is committed alongside it.
|
|
36
45
|
|
|
37
46
|
## Previewing during development
|
|
38
47
|
|
|
@@ -45,23 +54,6 @@ Two options:
|
|
|
45
54
|
cd custom && pnpm install && pnpm run dev:shell # http://localhost:9875
|
|
46
55
|
```
|
|
47
56
|
|
|
48
|
-
## Connecting to Notion
|
|
49
|
-
|
|
50
|
-
`ncblock` cli provides helpers to wire a scaffolded project to a Notion block and data source. Wirings are saved to `.notion/target.json`. Check the output of the cli for latest syntax instead of copying these commands verbatim.
|
|
51
|
-
|
|
52
|
-
```bash
|
|
53
|
-
# Pull a schema manifest from a Notion database into custom_blocks.json
|
|
54
|
-
npx ncblock manifest pull <db-id-or-url>
|
|
55
|
-
|
|
56
|
-
# Connect a local project to a Notion block and/or data source.
|
|
57
|
-
npx ncblock connect
|
|
58
|
-
|
|
59
|
-
# Deploy a dist/ directory to a block.
|
|
60
|
-
npx ncblock deploy
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
Templates will often already have some of these set in `.notion/target.json`.
|
|
64
|
-
|
|
65
57
|
## Where to look next
|
|
66
58
|
|
|
67
59
|
- `node_modules/ncblock/README.md` — landing page with a TOC into the per-category docs below.
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { execSync } from "node:child_process"
|
|
2
|
+
|
|
3
|
+
/** Sentinel exit code for "user pasted the wrong kind of ID" errors. */
|
|
4
|
+
export const EXIT_USER_ID_MISTAKE = 2
|
|
5
|
+
|
|
6
|
+
const MAX_PARENT_HOPS = 5
|
|
7
|
+
|
|
8
|
+
const YELLOW = "\x1b[33m"
|
|
9
|
+
const RESET = "\x1b[0m"
|
|
10
|
+
|
|
11
|
+
export type Classified = {
|
|
12
|
+
kind: "block" | "view"
|
|
13
|
+
/** What we'll write to `target.json#block_id`. Equals the user's input. */
|
|
14
|
+
blockId: string
|
|
15
|
+
/** What we'll pass to `ncblock connect` (if known). */
|
|
16
|
+
databaseId?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the user's positional ID into `{ blockId, databaseId }` BEFORE
|
|
21
|
+
* scaffolding, so bad inputs don't leave a half-written project on disk and
|
|
22
|
+
* good inputs already know which DB to pull from.
|
|
23
|
+
*
|
|
24
|
+
* - `/v1/databases/<id>` succeeds → user pasted a database ID into the
|
|
25
|
+
* block-ID slot. Fail-fast.
|
|
26
|
+
* - `/v1/blocks/<id>` succeeds → it's a block. `blockId` is the input;
|
|
27
|
+
* `databaseId` is walked from `block.parent` (database_id parent, or
|
|
28
|
+
* data_source parent, or recurse through block/page parents).
|
|
29
|
+
* - `/v1/views/<id>` succeeds → it's a view (Notion URL `?v=<id>` form).
|
|
30
|
+
* `blockId` = input (the view ID is the custom_block ID for the deploy
|
|
31
|
+
* target); `databaseId` = `view.parent.database_id`.
|
|
32
|
+
* - All three 404 → wrong workspace. Fail-fast.
|
|
33
|
+
* - Any non-404 failure → surface ntn's stderr and exit with its status,
|
|
34
|
+
* instead of misclassifying as "not in your workspace."
|
|
35
|
+
*
|
|
36
|
+
* Best-effort: if `ntn` isn't on PATH, returns `null` (with a warning logged
|
|
37
|
+
* by `isNtnAvailable`) — `ncblock connect`'s downstream errors will surface
|
|
38
|
+
* real issues.
|
|
39
|
+
*/
|
|
40
|
+
export function classifyPositionalId(
|
|
41
|
+
rawId: string,
|
|
42
|
+
env: string,
|
|
43
|
+
): Classified | null {
|
|
44
|
+
const id = formatDashedUuid(rawId)
|
|
45
|
+
|
|
46
|
+
if (!isNtnAvailable()) {
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const dbResult = tryNtnApi(env, `/v1/databases/${id}`)
|
|
51
|
+
if (dbResult.ok && isDatabaseShape(dbResult.body)) {
|
|
52
|
+
bailUserInput(
|
|
53
|
+
`${id} is a database id, please paste the block id or view id`,
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const blockResult = tryNtnApi(env, `/v1/blocks/${id}`)
|
|
58
|
+
if (blockResult.ok) {
|
|
59
|
+
return {
|
|
60
|
+
kind: "block",
|
|
61
|
+
blockId: id,
|
|
62
|
+
databaseId: walkBlockToDatabase(blockResult.body, env, 0),
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const viewResult = tryNtnApi(env, `/v1/views/${id}`)
|
|
67
|
+
if (viewResult.ok) {
|
|
68
|
+
const v = viewResult.body as
|
|
69
|
+
| { parent?: { database_id?: string } }
|
|
70
|
+
| undefined
|
|
71
|
+
return { kind: "view", blockId: id, databaseId: v?.parent?.database_id }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Every probe failed. If any returned a non-404, show ntn's actual stderr
|
|
75
|
+
// — saying "not in your workspace" would mislead the user when the real
|
|
76
|
+
// issue is e.g. a 400 validation error. Only the all-404 case gets the
|
|
77
|
+
// wrong-workspace nudge.
|
|
78
|
+
const firstError = [dbResult, blockResult, viewResult].find(r => r.error)
|
|
79
|
+
if (firstError) {
|
|
80
|
+
process.stderr.write(firstError.error ?? "")
|
|
81
|
+
process.exit(firstError.status ?? 1)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
bailUserInput(
|
|
85
|
+
`${id} doesn't look like a valid id for your workspace. Ensure you're logged in to the right workspace with \`${loginCommandHint(env)}\`.`,
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Walk up a block's parent chain looking for an enclosing database. The
|
|
91
|
+
* common case is `parent.type === "database_id"` (one hop), but blocks can
|
|
92
|
+
* also be children of pages or other blocks, so we recurse up to
|
|
93
|
+
* MAX_PARENT_HOPS.
|
|
94
|
+
*/
|
|
95
|
+
function walkBlockToDatabase(
|
|
96
|
+
block: unknown,
|
|
97
|
+
env: string,
|
|
98
|
+
depth: number,
|
|
99
|
+
): string | undefined {
|
|
100
|
+
if (depth >= MAX_PARENT_HOPS) {
|
|
101
|
+
return undefined
|
|
102
|
+
}
|
|
103
|
+
const parent = (block as { parent?: Record<string, unknown> } | undefined)
|
|
104
|
+
?.parent
|
|
105
|
+
const parentType = typeof parent?.type === "string" ? parent.type : undefined
|
|
106
|
+
if (!parent || !parentType) {
|
|
107
|
+
return undefined
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const parentId = parent[parentType]
|
|
111
|
+
if (typeof parentId !== "string") {
|
|
112
|
+
return undefined
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
switch (parentType) {
|
|
116
|
+
case "database_id":
|
|
117
|
+
return parentId
|
|
118
|
+
case "data_source_id": {
|
|
119
|
+
const ds = tryNtnApi(env, `/v1/data_sources/${parentId}`)
|
|
120
|
+
if (!ds.ok) {
|
|
121
|
+
return undefined
|
|
122
|
+
}
|
|
123
|
+
// TODO(custom-blocks): validate the data source response shape with
|
|
124
|
+
// a proper type guard instead of casting.
|
|
125
|
+
const dbId = (
|
|
126
|
+
ds.body as { parent?: { database_id?: string } } | undefined
|
|
127
|
+
)?.parent?.database_id
|
|
128
|
+
return dbId
|
|
129
|
+
}
|
|
130
|
+
case "block_id":
|
|
131
|
+
case "page_id": {
|
|
132
|
+
const parentBlock = tryNtnApi(env, `/v1/blocks/${parentId}`)
|
|
133
|
+
return parentBlock.ok
|
|
134
|
+
? walkBlockToDatabase(parentBlock.body, env, depth + 1)
|
|
135
|
+
: undefined
|
|
136
|
+
}
|
|
137
|
+
default:
|
|
138
|
+
return undefined
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function isNtnAvailable(): boolean {
|
|
143
|
+
try {
|
|
144
|
+
execSync("ntn --version", { stdio: "ignore" })
|
|
145
|
+
return true
|
|
146
|
+
} catch {
|
|
147
|
+
console.warn(
|
|
148
|
+
` ${YELLOW}Warning:${RESET} \`ntn\` not on PATH — skipping ID classification. Install ntn to catch wrong-workspace / wrong-ID-kind mistakes upfront.`,
|
|
149
|
+
)
|
|
150
|
+
return false
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
type NtnApiResult = {
|
|
155
|
+
ok: boolean
|
|
156
|
+
body?: unknown
|
|
157
|
+
/** Captured stderr from a non-404 failure (so the caller can surface it). */
|
|
158
|
+
error?: string
|
|
159
|
+
/** ntn's exit code on failure, for propagation. */
|
|
160
|
+
status?: number
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 404s mean "not this kind of resource" — keep probing. Other failures (400
|
|
165
|
+
* validation errors, 401/403, network) get captured so the classifier can
|
|
166
|
+
* surface them only if every probe fails (i.e. we'd otherwise mislead the user
|
|
167
|
+
* with "not in your workspace").
|
|
168
|
+
*/
|
|
169
|
+
function tryNtnApi(env: string, apiPath: string): NtnApiResult {
|
|
170
|
+
const prefix =
|
|
171
|
+
env && env !== "production" ? `NOTION_KEYRING=0 ntn --env ${env}` : "ntn"
|
|
172
|
+
try {
|
|
173
|
+
const out = execSync(`${prefix} api ${apiPath}`, {
|
|
174
|
+
encoding: "utf-8",
|
|
175
|
+
stdio: "pipe",
|
|
176
|
+
})
|
|
177
|
+
try {
|
|
178
|
+
return { ok: true, body: JSON.parse(out) }
|
|
179
|
+
} catch {
|
|
180
|
+
return {
|
|
181
|
+
ok: false,
|
|
182
|
+
error: `Failed to parse successful response from ntn api ${apiPath}`,
|
|
183
|
+
status: 1,
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
} catch (err) {
|
|
187
|
+
const stderr =
|
|
188
|
+
(err as { stderr?: Buffer | string })?.stderr?.toString() ?? ""
|
|
189
|
+
if (stderr.includes("404")) {
|
|
190
|
+
return { ok: false }
|
|
191
|
+
}
|
|
192
|
+
const status = (err as { status?: number | null })?.status ?? 1
|
|
193
|
+
return { ok: false, error: stderr, status: status ?? 1 }
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function loginCommandHint(env: string): string {
|
|
198
|
+
return env && env !== "production"
|
|
199
|
+
? `NOTION_KEYRING=0 ntn --env ${env} login`
|
|
200
|
+
: "ntn login"
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function isDatabaseShape(body: unknown): boolean {
|
|
204
|
+
if (typeof body !== "object" || body === null) {
|
|
205
|
+
return false
|
|
206
|
+
}
|
|
207
|
+
if ("object" in body && (body as { object: unknown }).object === "database") {
|
|
208
|
+
return true
|
|
209
|
+
}
|
|
210
|
+
return (
|
|
211
|
+
"data_sources" in body &&
|
|
212
|
+
Array.isArray((body as { data_sources: unknown }).data_sources)
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function formatDashedUuid(raw: string): string {
|
|
217
|
+
const h = raw.replace(/-/g, "")
|
|
218
|
+
if (h.length !== 32) {
|
|
219
|
+
return raw
|
|
220
|
+
}
|
|
221
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function bailUserInput(message: string): never {
|
|
225
|
+
console.error(`\n ${YELLOW}Error:${RESET} ${message}\n`)
|
|
226
|
+
process.exit(EXIT_USER_ID_MISTAKE)
|
|
227
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decide what ID the user is actually passing to the init script — the
|
|
3
|
+
* "parse the inputs" half of init's ID handling. The other half (deciding
|
|
4
|
+
* what an ID *is* — block? view? database in the wrong slot?) lives in
|
|
5
|
+
* `classifyId.ts`.
|
|
6
|
+
*
|
|
7
|
+
* Kept out of `init.ts` so the CLI/env precedence rules are unit-testable
|
|
8
|
+
* without spawning a tsx subprocess.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type InitArgsInput = {
|
|
12
|
+
/** Value of --collection, if explicitly passed. */
|
|
13
|
+
collection?: string
|
|
14
|
+
/** Value of --block or a positional Notion ID, if passed. */
|
|
15
|
+
block?: string
|
|
16
|
+
/** `process.env.BLOCK_ID` — last-resort source for the block ID. */
|
|
17
|
+
envBlockId?: string
|
|
18
|
+
/** `process.env.ENV` — populates `.notion/target.json#env`. */
|
|
19
|
+
envName?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type InitArgsResolution = {
|
|
23
|
+
/** Block ID (from --block, positional, or BLOCK_ID env). */
|
|
24
|
+
blockId: string | undefined
|
|
25
|
+
/**
|
|
26
|
+
* Explicit --collection value. When set, classification is skipped — we
|
|
27
|
+
* already know the database. When undefined, we'll classify the
|
|
28
|
+
* block-shaped input to figure out the parent DB.
|
|
29
|
+
*/
|
|
30
|
+
databaseId: string | undefined
|
|
31
|
+
/**
|
|
32
|
+
* The single ID to feed into `classifyPositionalId`, or `undefined` to
|
|
33
|
+
* skip classification. Set when we have a block-shaped input AND no
|
|
34
|
+
* explicit `--collection` (otherwise the DB is already known).
|
|
35
|
+
*/
|
|
36
|
+
idToClassify: string | undefined
|
|
37
|
+
/** Env name for `.notion/target.json#env` and env-aware ntn calls. */
|
|
38
|
+
env: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function resolveInitArgs(input: InitArgsInput): InitArgsResolution {
|
|
42
|
+
const blockId = input.block || input.envBlockId || undefined
|
|
43
|
+
const databaseId = input.collection
|
|
44
|
+
// `--collection` is the user's assertion that they already know the
|
|
45
|
+
// database; otherwise we need to probe the block-shaped input to find
|
|
46
|
+
// the parent DB.
|
|
47
|
+
const idToClassify = databaseId === undefined ? blockId : undefined
|
|
48
|
+
const env = input.envName ?? "production"
|
|
49
|
+
return { blockId, databaseId, idToClassify, env }
|
|
50
|
+
}
|
package/sdk-version.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.0.
|
|
1
|
+
{"version":"0.0.16"}
|