softr-vibe-coding 1.9.0 → 1.10.1
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/CHANGELOG.md +6 -0
- package/SKILL.md +2 -1
- package/datasources/airtable.md +56 -0
- package/datasources/fields.md +2 -0
- package/datasources/reading.md +27 -7
- package/package.json +1 -1
- package/tools/get-airtable-base +303 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,12 @@ All notable changes to this skill are documented here. Versions follow [Semantic
|
|
|
4
4
|
|
|
5
5
|
Entries from 1.3.1 onward are generated automatically from git commit subjects between version bumps (see `.github/workflows/publish.yml`). Entries before 1.3.1 were backfilled by hand from the existing commit history.
|
|
6
6
|
|
|
7
|
+
## [1.10.1] - 2026-06-12
|
|
8
|
+
- Document useFieldOptions companion-useRecords requirement — the hook only populates once an active useRecords in the same block has loaded the table schema; without it options settles to [] with isLoading false (bites write-only/helper blocks hardest). Add the required companion query to the example, correct the return shape to { options, isLoading } with options { id, label, color }, note the cross-table helper-block pattern, and add a prefer-live-fall-back-to-hardcoded recommendation; bump to 1.10.1
|
|
9
|
+
|
|
10
|
+
## [1.10.0] - 2026-06-04
|
|
11
|
+
- Bundle get-airtable-base CLI script for full base metadata export; document in SKILL.md, airtable.md, fields.md
|
|
12
|
+
|
|
7
13
|
## [1.9.0] - 2026-06-04
|
|
8
14
|
- Bundle get-softr-database CLI script for schema export; document in SKILL.md, softr-database.md, fields.md
|
|
9
15
|
- Bump publish workflow to Node 24-based action majors — actions/checkout@v4 -> @v6, actions/setup-node@v4 -> @v6 (clears the Node 20 deprecation; GitHub forces Node 20 actions to Node 24 on 2026-06-16). No version bump, so this run skips publish.
|
package/SKILL.md
CHANGED
|
@@ -87,7 +87,8 @@ When the user describes their block, figure out which of these areas apply and a
|
|
|
87
87
|
- **Data source type**: Is it Airtable, Softr Database, REST API, or another source? This determines the data fetching approach. **Load the relevant data source guide** from the [datasources/](datasources/) directory before writing code.
|
|
88
88
|
- **Data source fields**: For Airtable/Softr Database, you need actual field IDs. For REST APIs, you access the raw API response directly. If the user doesn't know field IDs:
|
|
89
89
|
- For **Softr Database**, the cleanest path is the **Softr Database MCP server** — ask whether they have it installed (`claude mcp list` shows it as `softr` or similar). If yes, query schema directly with the MCP tools instead of asking for paste-ins. If no, the next-best option is the bundled **`get-softr-database` CLI script** — tell the user to run `python3 ~/.claude/skills/softr-vibe-coding/tools/get-softr-database.py <database_id>` (it prompts for their Softr API key and exports the full schema to `~/Desktop/softr-database-<id>-<timestamp>.json` — Python stdlib only, nothing to install) and paste the resulting JSON into chat. As a final fallback, ask them to paste the `tablespace-with-tables` network response (DevTools -> Network -> filter that string while on Studio's Data tab) — same JSON content, different acquisition path. Optionally tell them they can install the MCP once with `claude mcp add --transport http softr https://mcp.softr.io/mcp` for future sessions. Full MCP details in [references/softr-database-mcp.md](references/softr-database-mcp.md); CLI script details in [datasources/softr-database.md](datasources/softr-database.md#bundled-cli-script-get-softr-database); fallback paste-in workflows in [datasources/fields.md](datasources/fields.md#field-inspector-block).
|
|
90
|
-
- For **Airtable** and
|
|
90
|
+
- For **Airtable**, the most thorough path is the bundled **`get-airtable-base` shell script** — `bash ~/.claude/skills/softr-vibe-coding/tools/get-airtable-base` (requires `jq` — `brew install jq` on macOS). It prompts for Base ID + PAT, then exports the full schema (every table, every field with both `fld...` IDs and column names, relationships, webhooks, interfaces) to a timestamped Desktop folder. The user pastes `02-schema.json` or the combined `00-bundle.json` into chat. For lighter inspection (just a few fields, runtime-only), suggest the Field Inspector block — empty `q.select({})` works for Airtable. CLI script details in [datasources/airtable.md](datasources/airtable.md#bundled-cli-script-get-airtable-base).
|
|
91
|
+
- For other non-Softr-DB sources where empty `q.select({})` works, suggest the Field Inspector block.
|
|
91
92
|
- **Brand colors**: Already resolved in Step 1 (Detect the brand source). Don't re-ask. The brand source is one of:
|
|
92
93
|
- **Project's `./DESIGN.md`** (recommended for client work — produced by the `building-design-md` skill)
|
|
93
94
|
- **User's quick override** (paste of primary + accent + font)
|
package/datasources/airtable.md
CHANGED
|
@@ -55,6 +55,62 @@ The asymmetry matters because reads silently degrade while writes silently disab
|
|
|
55
55
|
|
|
56
56
|
3. **Avoid renaming columns mid-project** -- use Airtable's Description field for clarification instead.
|
|
57
57
|
|
|
58
|
+
## Bundled CLI script: `get-airtable-base`
|
|
59
|
+
|
|
60
|
+
A Bash CLI bundled with this skill that exports an Airtable base's full metadata to a timestamped folder on your Desktop. Pulls schema, relationships, sync detection, webhooks, interfaces, and shares — everything the Airtable Web API exposes for a single base.
|
|
61
|
+
|
|
62
|
+
**Script location after `npx softr-vibe-coding@latest init`:**
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
~/.claude/skills/softr-vibe-coding/tools/get-airtable-base
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Requirements:** `jq` (`brew install jq` on macOS), `curl` (preinstalled on macOS/Linux), Bash.
|
|
69
|
+
|
|
70
|
+
**Run it:**
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
bash ~/.claude/skills/softr-vibe-coding/tools/get-airtable-base
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The script prompts interactively for:
|
|
77
|
+
|
|
78
|
+
- **Base ID** (e.g. `appXXXXXXXXXXXXXX` — find it in the Airtable URL or the API docs page for the base).
|
|
79
|
+
- **Personal Access Token** (input hidden — needs `schema.bases:read` scope minimum, plus `webhook:manage` for webhooks, and optionally `enterpriseAccount:read` for shares).
|
|
80
|
+
|
|
81
|
+
**Output folder:** `~/Desktop/airtable-base-<BASE_ID>-<UTC-timestamp>/` containing:
|
|
82
|
+
|
|
83
|
+
| File | Contents |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `00-bundle.json` | Combined bundle of all the below for easy sharing in one paste |
|
|
86
|
+
| `01-base-info.json` | Collaborators, interfaces, invite links |
|
|
87
|
+
| `02-schema.json` | Full schema: every table, every field (with both `fld...` IDs and column names), every view, `visibleFieldIds` per view |
|
|
88
|
+
| `03-shares.json` | Enterprise-only shares — skipped on non-Enterprise plans |
|
|
89
|
+
| `04-webhooks.json` | Registered webhooks |
|
|
90
|
+
| `05-whoami.json` | Token identity and scopes — confirms which user/PAT was used and what it can access |
|
|
91
|
+
| `06-relationships.json` | Derived from `02-schema.json`: sync destinations, cross-table links, lookup/rollup/count/formula field maps |
|
|
92
|
+
|
|
93
|
+
After completion, the script opens the folder in Finder (macOS).
|
|
94
|
+
|
|
95
|
+
**Optional alias** for a shorter command. Add to your `~/.zshrc` or `~/.bashrc`:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
alias get-airtable-base='bash ~/.claude/skills/softr-vibe-coding/tools/get-airtable-base'
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
After `source ~/.zshrc`, run `get-airtable-base` from anywhere.
|
|
102
|
+
|
|
103
|
+
**Getting a PAT:** https://airtable.com/create/tokens → Create token → grant `schema.bases:read` (add `data.records:read` only if you want to also read records via the Web API outside Softr). Scope to the specific base(s) the script should access.
|
|
104
|
+
|
|
105
|
+
**When to use this:**
|
|
106
|
+
|
|
107
|
+
- **Documenting field IDs alongside column names** in `q.select()` — the mitigation in [Maintainability gotcha](#maintainability-gotcha) above. Grep the `02-schema.json` for an `fld...` ID to find every block affected by a column rename.
|
|
108
|
+
- **Bisecting a broken Action** when a column rename has silently disabled it — the freshest `02-schema.json` is the source of truth to grep against.
|
|
109
|
+
- **Auditing relationships** (lookups, rollups, formulas, cross-table links, sync sources) — `06-relationships.json` summarizes everything the schema reveals about derived/referenced fields.
|
|
110
|
+
- **Sharing schema with an AI assistant** — paste `00-bundle.json` (or just `02-schema.json` if smaller) into chat to give the assistant accurate, current schema context.
|
|
111
|
+
|
|
112
|
+
This script reads only **metadata**, never records. To inspect record contents inside a Vibe Coding block, use the Field Inspector pattern in [fields.md](fields.md#field-inspector-block).
|
|
113
|
+
|
|
58
114
|
## Supported Fields
|
|
59
115
|
|
|
60
116
|
| Field Type | Writable | Notes |
|
package/datasources/fields.md
CHANGED
|
@@ -107,6 +107,8 @@ export default function Block() {
|
|
|
107
107
|
}
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
+
**For Airtable specifically, the bundled `get-airtable-base` script is a more comprehensive alternative** — it exports the full base schema (every table, every field with `fld...` IDs and column names, relationships, webhooks, interfaces) to a Desktop folder via the Airtable Web API. Run with `bash ~/.claude/skills/softr-vibe-coding/tools/get-airtable-base` (requires `jq` — `brew install jq` on macOS). Best when you want a portable schema snapshot, are documenting field IDs alongside column names per the [airtable.md maintainability mitigation](airtable.md#maintainability-gotcha), or need to audit lookup/rollup/sync relationships. Full usage in [airtable.md](airtable.md#bundled-cli-script-get-airtable-base).
|
|
111
|
+
|
|
110
112
|
**For Softr Database, find field IDs via:**
|
|
111
113
|
|
|
112
114
|
1. **Softr Database MCP server (recommended for AI-assisted workflows)** -- if you're collaborating with an AI assistant (Claude Code, Claude Desktop, Cursor, ChatGPT, Mistral) to write Vibe Coding blocks, the official Softr MCP server is the cleanest path. The AI calls schema/list-fields tools directly against your workspace and reads back every field's `id`, `name`, `type`, and dropdown option UUIDs -- no copy-paste, no transcription errors. Full setup, scopes, and scope limitations (Softr DB only -- does NOT cover Airtable / external sources) in [../references/softr-database-mcp.md](../references/softr-database-mcp.md).
|
package/datasources/reading.md
CHANGED
|
@@ -108,23 +108,43 @@ var options = (result.data && result.data.pages) ? result.data.pages.flatMap(fun
|
|
|
108
108
|
Returns the current option list for any `singleSelect` / `multipleSelects` field — without hardcoding option IDs in your block. Useful when the schema's option list changes (renames, additions, reorders) and you don't want to redeploy the block every time.
|
|
109
109
|
|
|
110
110
|
```jsx
|
|
111
|
-
import { useFieldOptions, q } from "@/lib/datasource";
|
|
111
|
+
import { useFieldOptions, useRecords, q } from "@/lib/datasource";
|
|
112
|
+
|
|
113
|
+
var specSelect = q.select({ status: "Status" });
|
|
114
|
+
|
|
115
|
+
// REQUIRED: a companion records query in the SAME block loads the table schema that
|
|
116
|
+
// useFieldOptions reads from. Without it, useFieldOptions settles to `{ options: [] }`
|
|
117
|
+
// (isLoading false, length 0) even though the field has choices. count: 1 is enough.
|
|
118
|
+
useRecords({ select: specSelect, count: 1 });
|
|
112
119
|
|
|
113
120
|
var statusOptions = useFieldOptions({
|
|
114
|
-
select:
|
|
121
|
+
select: specSelect,
|
|
115
122
|
field: "status", // the ALIAS from q.select(), NOT the raw field ID
|
|
116
123
|
});
|
|
117
124
|
|
|
118
|
-
// statusOptions
|
|
119
|
-
//
|
|
125
|
+
// statusOptions → { options: [...], isLoading: bool }
|
|
126
|
+
// statusOptions.options → [{ id: "sel...", label: "Active", color: "greenLight1" }, ...]
|
|
127
|
+
// id — the option's UUID, used in mutate payloads
|
|
128
|
+
// label — display string
|
|
129
|
+
// color — Airtable swatch color name (optional; handy for tinting chips)
|
|
120
130
|
```
|
|
121
131
|
|
|
132
|
+
**⚠️ Gotcha — requires a companion `useRecords` (verified 2026-06-12).** `useFieldOptions`
|
|
133
|
+
only populates once an active `useRecords` in the same block has loaded that table's schema.
|
|
134
|
+
This bites hardest in **write-only / invisible helper blocks** (the natural home for an
|
|
135
|
+
option-publishing helper) because they otherwise never query records — so `options` stays
|
|
136
|
+
`[]` forever with `isLoading: false`, which looks like "the field has no choices." The fix is
|
|
137
|
+
a throwaway `useRecords({ select, count: 1 })` alongside the `useFieldOptions` call(s); the
|
|
138
|
+
same `select` object can be shared by both. Reuse one `select` for many fields and call
|
|
139
|
+
`useFieldOptions` once per field (alias). Symptom to recognise: hook returns
|
|
140
|
+
`{ options: [], isLoading: false }` while the block is correctly bound to the data source.
|
|
141
|
+
|
|
122
142
|
**When to use this vs. hardcoding:**
|
|
123
143
|
|
|
124
|
-
- **Use `useFieldOptions`** when option IDs / labels could change post-deploy — selects with rapidly-evolving lists, user-editable choices, or any case where re-pasting blocks for an option rename is annoying.
|
|
125
|
-
- **Hardcode** when the option set is stable and frequently referenced (e.g. a status enum that drives a state machine), so the IDs live in source and rename-safety is enforced by greppable constants.
|
|
144
|
+
- **Use `useFieldOptions`** when option IDs / labels could change post-deploy — selects with rapidly-evolving lists, user-editable choices, or any case where re-pasting blocks for an option rename is annoying. Cross-table case: to render a select field from table B inside a block bound to table A (e.g. an intake form bound to Jobs that needs the Wigs `Color` options), put the `useRecords` + `useFieldOptions` in a hidden helper block bound to table B and publish the options to a `window` global (see [helper-blocks.md](../references/helper-blocks.md)).
|
|
145
|
+
- **Hardcode** when the option set is stable and frequently referenced (e.g. a status enum that drives a state machine), so the IDs live in source and rename-safety is enforced by greppable constants. A robust middle ground: prefer the live options, fall back to a hardcoded list per field so the UI still renders if the helper hasn't published yet.
|
|
126
146
|
|
|
127
|
-
`useFieldOptions` is the read-side equivalent of using `useLinkedRecords` for foreign records — it abstracts away the field's option store. Items are shaped `{ id, label }` (note: `label`, not `title` like `useLinkedRecords`).
|
|
147
|
+
`useFieldOptions` is the read-side equivalent of using `useLinkedRecords` for foreign records — it abstracts away the field's option store. Items are shaped `{ id, label, color }` (note: `label`, not `title` like `useLinkedRecords`).
|
|
128
148
|
|
|
129
149
|
## Filtering
|
|
130
150
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "softr-vibe-coding",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.1",
|
|
4
4
|
"description": "Claude Code skill for generating production-ready Softr Vibe Coding blocks (JSX). Installs into ~/.claude/skills/ and auto-updates on each Claude Code session.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"softr-vibe-coding": "./bin/cli.js"
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# get-airtable-base
|
|
4
|
+
# Pulls all available metadata for a single Airtable base via the Web API
|
|
5
|
+
# and saves it to a timestamped folder on the Desktop.
|
|
6
|
+
#
|
|
7
|
+
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
# ---------- Colors ----------
|
|
11
|
+
RED='\033[0;31m'
|
|
12
|
+
GREEN='\033[0;32m'
|
|
13
|
+
YELLOW='\033[1;33m'
|
|
14
|
+
BLUE='\033[0;34m'
|
|
15
|
+
GRAY='\033[0;90m'
|
|
16
|
+
BOLD='\033[1m'
|
|
17
|
+
NC='\033[0m'
|
|
18
|
+
|
|
19
|
+
# ---------- Pre-flight ----------
|
|
20
|
+
if ! command -v jq &>/dev/null; then
|
|
21
|
+
echo -e "${RED}Error: jq is not installed.${NC}"
|
|
22
|
+
echo "Install it with: brew install jq"
|
|
23
|
+
exit 1
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
if ! command -v curl &>/dev/null; then
|
|
27
|
+
echo -e "${RED}Error: curl is not installed.${NC}"
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# ---------- Banner ----------
|
|
32
|
+
echo ""
|
|
33
|
+
echo -e "${BOLD}${BLUE}╔════════════════════════════════════════╗${NC}"
|
|
34
|
+
echo -e "${BOLD}${BLUE}║ Airtable Base Metadata Fetcher ║${NC}"
|
|
35
|
+
echo -e "${BOLD}${BLUE}╚════════════════════════════════════════╝${NC}"
|
|
36
|
+
echo ""
|
|
37
|
+
|
|
38
|
+
# ---------- Prompts ----------
|
|
39
|
+
read -r -p "$(echo -e "${BOLD}Base ID${NC} (e.g. appXXXXXXXXXXXXXX): ")" BASE_ID
|
|
40
|
+
if [[ -z "$BASE_ID" ]]; then
|
|
41
|
+
echo -e "${RED}Error: Base ID is required.${NC}"
|
|
42
|
+
exit 1
|
|
43
|
+
fi
|
|
44
|
+
if [[ ! "$BASE_ID" =~ ^app[a-zA-Z0-9]{14}$ ]]; then
|
|
45
|
+
echo -e "${YELLOW}Warning: '$BASE_ID' doesn't look like a standard Base ID (app + 14 chars). Continuing anyway...${NC}"
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
read -r -s -p "$(echo -e "${BOLD}Personal Access Token${NC} (input hidden): ")" TOKEN
|
|
49
|
+
echo ""
|
|
50
|
+
if [[ -z "$TOKEN" ]]; then
|
|
51
|
+
echo -e "${RED}Error: PAT is required.${NC}"
|
|
52
|
+
exit 1
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
# ---------- Output folder ----------
|
|
56
|
+
TIMESTAMP=$(date -u +%Y-%m-%dT%H-%M-%SZ)
|
|
57
|
+
OUTPUT_DIR="$HOME/Desktop/airtable-base-$BASE_ID-$TIMESTAMP"
|
|
58
|
+
mkdir -p "$OUTPUT_DIR"
|
|
59
|
+
|
|
60
|
+
echo ""
|
|
61
|
+
echo -e "${GRAY}Output: $OUTPUT_DIR${NC}"
|
|
62
|
+
echo ""
|
|
63
|
+
|
|
64
|
+
AUTH=(-H "Authorization: Bearer $TOKEN")
|
|
65
|
+
API="https://api.airtable.com/v0"
|
|
66
|
+
|
|
67
|
+
# ---------- Helper ----------
|
|
68
|
+
# fetch URL OUTFILE LABEL [optional]
|
|
69
|
+
# Writes response to OUTFILE (jq-pretty). Returns 0 on HTTP 2xx, 1 otherwise.
|
|
70
|
+
fetch() {
|
|
71
|
+
local url="$1"
|
|
72
|
+
local outfile="$2"
|
|
73
|
+
local label="$3"
|
|
74
|
+
local optional="${4:-false}"
|
|
75
|
+
|
|
76
|
+
local tmp http_code body
|
|
77
|
+
tmp=$(mktemp)
|
|
78
|
+
http_code=$(curl -s -o "$tmp" -w "%{http_code}" "${AUTH[@]}" "$url" || echo "000")
|
|
79
|
+
|
|
80
|
+
if [[ "$http_code" =~ ^2 ]]; then
|
|
81
|
+
if jq '.' "$tmp" >"$outfile" 2>/dev/null; then
|
|
82
|
+
local size
|
|
83
|
+
size=$(wc -c <"$outfile" | tr -d ' ')
|
|
84
|
+
echo -e " ${GREEN}✓${NC} $label ${GRAY}(${size} bytes)${NC}"
|
|
85
|
+
rm -f "$tmp"
|
|
86
|
+
return 0
|
|
87
|
+
else
|
|
88
|
+
echo -e " ${RED}✗${NC} $label ${GRAY}(invalid JSON response)${NC}"
|
|
89
|
+
mv "$tmp" "$outfile.raw"
|
|
90
|
+
return 1
|
|
91
|
+
fi
|
|
92
|
+
else
|
|
93
|
+
body=$(cat "$tmp")
|
|
94
|
+
rm -f "$tmp"
|
|
95
|
+
if [[ "$optional" == "true" ]]; then
|
|
96
|
+
echo -e " ${YELLOW}-${NC} $label ${GRAY}(HTTP $http_code — skipped)${NC}"
|
|
97
|
+
else
|
|
98
|
+
echo -e " ${RED}✗${NC} $label ${GRAY}(HTTP $http_code)${NC}"
|
|
99
|
+
echo -e "${GRAY} $body${NC}" | head -c 300
|
|
100
|
+
echo ""
|
|
101
|
+
fi
|
|
102
|
+
return 1
|
|
103
|
+
fi
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# ---------- Phase 1: Identity & access ----------
|
|
107
|
+
echo -e "${BOLD}Phase 1: Validating token...${NC}"
|
|
108
|
+
|
|
109
|
+
fetch "$API/meta/whoami" \
|
|
110
|
+
"$OUTPUT_DIR/05-whoami.json" \
|
|
111
|
+
"Token identity (whoami)" || {
|
|
112
|
+
echo -e "${RED}Cannot continue — token is invalid or revoked.${NC}"
|
|
113
|
+
exit 1
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if [[ -f "$OUTPUT_DIR/05-whoami.json" ]]; then
|
|
117
|
+
scopes=$(jq -r '.scopes // [] | join(", ")' "$OUTPUT_DIR/05-whoami.json" 2>/dev/null || echo "(unknown)")
|
|
118
|
+
user_id=$(jq -r '.id // "(unknown)"' "$OUTPUT_DIR/05-whoami.json" 2>/dev/null || echo "(unknown)")
|
|
119
|
+
echo -e "${GRAY} User: $user_id${NC}"
|
|
120
|
+
echo -e "${GRAY} Scopes: $scopes${NC}"
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
# ---------- Phase 2: Base schema + metadata ----------
|
|
124
|
+
echo ""
|
|
125
|
+
echo -e "${BOLD}Phase 2: Fetching base metadata...${NC}"
|
|
126
|
+
|
|
127
|
+
fetch "$API/meta/bases/$BASE_ID?include=collaborators&include=interfaces&include=inviteLinks" \
|
|
128
|
+
"$OUTPUT_DIR/01-base-info.json" \
|
|
129
|
+
"Base info (collaborators, interfaces, invite links)" \
|
|
130
|
+
true || true
|
|
131
|
+
|
|
132
|
+
fetch "$API/meta/bases/$BASE_ID/tables?include[]=visibleFieldIds" \
|
|
133
|
+
"$OUTPUT_DIR/02-schema.json" \
|
|
134
|
+
"Schema (tables, fields, views with visibleFieldIds)" || {
|
|
135
|
+
echo -e "${RED}Cannot continue without schema. Exiting.${NC}"
|
|
136
|
+
exit 1
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
fetch "$API/meta/bases/$BASE_ID/shares" \
|
|
140
|
+
"$OUTPUT_DIR/03-shares.json" \
|
|
141
|
+
"Shares (Enterprise-only)" \
|
|
142
|
+
true || true
|
|
143
|
+
|
|
144
|
+
fetch "$API/bases/$BASE_ID/webhooks" \
|
|
145
|
+
"$OUTPUT_DIR/04-webhooks.json" \
|
|
146
|
+
"Webhooks" \
|
|
147
|
+
true || true
|
|
148
|
+
|
|
149
|
+
# ---------- Phase 3: Schema-derived relationships & sync detection ----------
|
|
150
|
+
echo ""
|
|
151
|
+
echo -e "${BOLD}Phase 3: Analyzing schema for relationships & sync...${NC}"
|
|
152
|
+
|
|
153
|
+
if [[ -f "$OUTPUT_DIR/02-schema.json" ]]; then
|
|
154
|
+
jq '
|
|
155
|
+
{
|
|
156
|
+
sync_destinations: [
|
|
157
|
+
.tables[] | select([.fields[].type] | any(. == "externalSyncSource")) | {
|
|
158
|
+
tableId: .id,
|
|
159
|
+
tableName: .name,
|
|
160
|
+
syncedFields: [.fields[] | select(.type == "externalSyncSource") | {id, name}]
|
|
161
|
+
}
|
|
162
|
+
],
|
|
163
|
+
cross_table_links: [
|
|
164
|
+
.tables[] as $t |
|
|
165
|
+
$t.fields[] | select(.type == "multipleRecordLinks") | {
|
|
166
|
+
fromTableId: $t.id,
|
|
167
|
+
fromTableName: $t.name,
|
|
168
|
+
fromFieldId: .id,
|
|
169
|
+
fromFieldName: .name,
|
|
170
|
+
toTableId: (.options.linkedTableId // null),
|
|
171
|
+
inverseFieldId: (.options.inverseLinkFieldId // null),
|
|
172
|
+
isReversed: (.options.isReversed // null),
|
|
173
|
+
prefersSingleRecordLink: (.options.prefersSingleRecordLink // null)
|
|
174
|
+
}
|
|
175
|
+
],
|
|
176
|
+
lookup_fields: [
|
|
177
|
+
.tables[] as $t |
|
|
178
|
+
$t.fields[] | select(.type == "multipleLookupValues") | {
|
|
179
|
+
tableId: $t.id,
|
|
180
|
+
tableName: $t.name,
|
|
181
|
+
fieldId: .id,
|
|
182
|
+
fieldName: .name,
|
|
183
|
+
viaLinkFieldId: (.options.recordLinkFieldId // null),
|
|
184
|
+
targetFieldId: (.options.fieldIdInLinkedTable // null)
|
|
185
|
+
}
|
|
186
|
+
],
|
|
187
|
+
rollup_fields: [
|
|
188
|
+
.tables[] as $t |
|
|
189
|
+
$t.fields[] | select(.type == "rollup") | {
|
|
190
|
+
tableId: $t.id,
|
|
191
|
+
tableName: $t.name,
|
|
192
|
+
fieldId: .id,
|
|
193
|
+
fieldName: .name,
|
|
194
|
+
viaLinkFieldId: (.options.recordLinkFieldId // null),
|
|
195
|
+
sourceFieldId: (.options.fieldIdInLinkedTable // null),
|
|
196
|
+
formula: (.options.reductionConditional // null)
|
|
197
|
+
}
|
|
198
|
+
],
|
|
199
|
+
count_fields: [
|
|
200
|
+
.tables[] as $t |
|
|
201
|
+
$t.fields[] | select(.type == "count") | {
|
|
202
|
+
tableId: $t.id,
|
|
203
|
+
tableName: $t.name,
|
|
204
|
+
fieldId: .id,
|
|
205
|
+
fieldName: .name,
|
|
206
|
+
viaLinkFieldId: (.options.recordLinkFieldId // null)
|
|
207
|
+
}
|
|
208
|
+
],
|
|
209
|
+
formula_fields: [
|
|
210
|
+
.tables[] as $t |
|
|
211
|
+
$t.fields[] | select(.type == "formula") | {
|
|
212
|
+
tableId: $t.id,
|
|
213
|
+
tableName: $t.name,
|
|
214
|
+
fieldId: .id,
|
|
215
|
+
fieldName: .name,
|
|
216
|
+
formula: (.options.formula // null),
|
|
217
|
+
referencedFieldIds: (.options.referencedFieldIds // [])
|
|
218
|
+
}
|
|
219
|
+
]
|
|
220
|
+
}
|
|
221
|
+
' "$OUTPUT_DIR/02-schema.json" > "$OUTPUT_DIR/06-relationships.json" 2>/dev/null
|
|
222
|
+
|
|
223
|
+
if [[ -f "$OUTPUT_DIR/06-relationships.json" ]]; then
|
|
224
|
+
sync_count=$(jq '.sync_destinations | length' "$OUTPUT_DIR/06-relationships.json")
|
|
225
|
+
link_count=$(jq '.cross_table_links | length' "$OUTPUT_DIR/06-relationships.json")
|
|
226
|
+
lookup_count=$(jq '.lookup_fields | length' "$OUTPUT_DIR/06-relationships.json")
|
|
227
|
+
rollup_count=$(jq '.rollup_fields | length' "$OUTPUT_DIR/06-relationships.json")
|
|
228
|
+
count_count=$(jq '.count_fields | length' "$OUTPUT_DIR/06-relationships.json")
|
|
229
|
+
formula_count=$(jq '.formula_fields | length' "$OUTPUT_DIR/06-relationships.json")
|
|
230
|
+
echo -e " ${GREEN}✓${NC} Sync destinations: ${BOLD}$sync_count${NC}"
|
|
231
|
+
echo -e " ${GREEN}✓${NC} Cross-table links: ${BOLD}$link_count${NC}"
|
|
232
|
+
echo -e " ${GREEN}✓${NC} Lookup fields: ${BOLD}$lookup_count${NC}"
|
|
233
|
+
echo -e " ${GREEN}✓${NC} Rollup fields: ${BOLD}$rollup_count${NC}"
|
|
234
|
+
echo -e " ${GREEN}✓${NC} Count fields: ${BOLD}$count_count${NC}"
|
|
235
|
+
echo -e " ${GREEN}✓${NC} Formula fields: ${BOLD}$formula_count${NC}"
|
|
236
|
+
fi
|
|
237
|
+
fi
|
|
238
|
+
|
|
239
|
+
# ---------- Phase 4: Combined bundle ----------
|
|
240
|
+
echo ""
|
|
241
|
+
echo -e "${BOLD}Phase 4: Building combined bundle...${NC}"
|
|
242
|
+
|
|
243
|
+
# Ensure jq --slurpfile has something to read for endpoints that were skipped/failed.
|
|
244
|
+
for f in 01-base-info.json 02-schema.json 03-shares.json 04-webhooks.json 05-whoami.json 06-relationships.json; do
|
|
245
|
+
[[ -f "$OUTPUT_DIR/$f" ]] || echo 'null' > "$OUTPUT_DIR/$f"
|
|
246
|
+
done
|
|
247
|
+
|
|
248
|
+
jq -n \
|
|
249
|
+
--slurpfile whoami "$OUTPUT_DIR/05-whoami.json" \
|
|
250
|
+
--slurpfile base "$OUTPUT_DIR/01-base-info.json" \
|
|
251
|
+
--slurpfile schema "$OUTPUT_DIR/02-schema.json" \
|
|
252
|
+
--slurpfile shares "$OUTPUT_DIR/03-shares.json" \
|
|
253
|
+
--slurpfile webhooks "$OUTPUT_DIR/04-webhooks.json" \
|
|
254
|
+
--slurpfile rels "$OUTPUT_DIR/06-relationships.json" \
|
|
255
|
+
'{
|
|
256
|
+
whoami: ($whoami[0] // null),
|
|
257
|
+
base_info: ($base[0] // null),
|
|
258
|
+
schema: ($schema[0] // null),
|
|
259
|
+
shares: ($shares[0] // null),
|
|
260
|
+
webhooks: ($webhooks[0] // null),
|
|
261
|
+
relationships: ($rels[0] // null)
|
|
262
|
+
}' > "$OUTPUT_DIR/00-bundle.json" 2>/dev/null || {
|
|
263
|
+
echo -e " ${YELLOW}Could not build full bundle.${NC}"
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if [[ -f "$OUTPUT_DIR/00-bundle.json" ]]; then
|
|
267
|
+
size=$(wc -c <"$OUTPUT_DIR/00-bundle.json" | tr -d ' ')
|
|
268
|
+
echo -e " ${GREEN}✓${NC} 00-bundle.json ${GRAY}(${size} bytes)${NC}"
|
|
269
|
+
fi
|
|
270
|
+
|
|
271
|
+
# ---------- Summary ----------
|
|
272
|
+
echo ""
|
|
273
|
+
echo -e "${BOLD}${GREEN}Done.${NC}"
|
|
274
|
+
echo ""
|
|
275
|
+
echo -e "${BOLD}Summary:${NC}"
|
|
276
|
+
|
|
277
|
+
if [[ -f "$OUTPUT_DIR/02-schema.json" ]]; then
|
|
278
|
+
TABLE_COUNT=$(jq '.tables | length' "$OUTPUT_DIR/02-schema.json")
|
|
279
|
+
FIELD_COUNT=$(jq '[.tables[].fields | length] | add' "$OUTPUT_DIR/02-schema.json")
|
|
280
|
+
VIEW_COUNT=$(jq '[.tables[].views | length] | add' "$OUTPUT_DIR/02-schema.json")
|
|
281
|
+
echo -e " Tables: ${BOLD}$TABLE_COUNT${NC}"
|
|
282
|
+
echo -e " Fields: ${BOLD}$FIELD_COUNT${NC}"
|
|
283
|
+
echo -e " Views: ${BOLD}$VIEW_COUNT${NC}"
|
|
284
|
+
fi
|
|
285
|
+
|
|
286
|
+
if [[ -f "$OUTPUT_DIR/01-base-info.json" ]]; then
|
|
287
|
+
INT_COUNT=$(jq '.interfaces // {} | keys | length' "$OUTPUT_DIR/01-base-info.json" 2>/dev/null || echo 0)
|
|
288
|
+
[[ "$INT_COUNT" -gt 0 ]] && echo -e " Interfaces: ${BOLD}$INT_COUNT${NC}"
|
|
289
|
+
fi
|
|
290
|
+
|
|
291
|
+
if [[ -f "$OUTPUT_DIR/04-webhooks.json" ]]; then
|
|
292
|
+
WH_COUNT=$(jq '.webhooks // [] | length' "$OUTPUT_DIR/04-webhooks.json" 2>/dev/null || echo 0)
|
|
293
|
+
[[ "$WH_COUNT" -gt 0 ]] && echo -e " Webhooks: ${BOLD}$WH_COUNT${NC}"
|
|
294
|
+
fi
|
|
295
|
+
|
|
296
|
+
echo ""
|
|
297
|
+
echo -e "${GRAY}Folder: $OUTPUT_DIR${NC}"
|
|
298
|
+
echo ""
|
|
299
|
+
|
|
300
|
+
# Open in Finder
|
|
301
|
+
if command -v open &>/dev/null; then
|
|
302
|
+
open "$OUTPUT_DIR"
|
|
303
|
+
fi
|