softr-vibe-coding 1.8.0 → 1.10.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/CHANGELOG.md CHANGED
@@ -4,6 +4,13 @@ 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.0] - 2026-06-04
8
+ - Bundle get-airtable-base CLI script for full base metadata export; document in SKILL.md, airtable.md, fields.md
9
+
10
+ ## [1.9.0] - 2026-06-04
11
+ - Bundle get-softr-database CLI script for schema export; document in SKILL.md, softr-database.md, fields.md
12
+ - 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.
13
+
7
14
  ## [1.8.0] - 2026-06-04
8
15
  - Expand references/native-chrome-styling.md to the full native shell — add Footer (semantic <footer> target + 160px/overflow-wrap contact-column email-wrap fix), floating "island" header/footer treatment, and Page background (Softr stacks the same fill on html/body/#page-content/inner-wrapper, so paint on html + clear the stack, EXCLUDING the .softr-topbar subtree so the dropdown panel survives) + a Console background-finder snippet; broaden SKILL.md Reference Guides row + README; add anti-patterns row for the page-background stacking; bump to 1.8.0
9
16
 
package/SKILL.md CHANGED
@@ -86,8 +86,9 @@ When the user describes their block, figure out which of these areas apply and a
86
86
 
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
- - 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, fall back to asking them to paste the `tablespace-with-tables` network response (DevTools -> Network -> filter that string while on Studio's Data tab) — the JSON contains every field ID, type, and dropdown option UUID. 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); fallback paste-in workflows in [datasources/fields.md](datasources/fields.md#field-inspector-block).
90
- - For **Airtable** and other sources where empty `q.select({})` works, suggest the Field Inspector block.
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**, 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/bin/cli.js CHANGED
@@ -10,7 +10,7 @@ var SETTINGS_FILE = path.join(os.homedir(), '.claude', 'settings.json');
10
10
  var PACKAGE_ROOT = path.resolve(__dirname, '..');
11
11
 
12
12
  var SKILL_FILES = ['SKILL.md', 'ui-ux-guidelines.md', 'README.md', 'LICENSE'];
13
- var SKILL_DIRS = ['references', 'datasources'];
13
+ var SKILL_DIRS = ['references', 'datasources', 'tools'];
14
14
 
15
15
  var HOOK_COMMAND = 'npx -y --prefer-online ' + SKILL_NAME + '@latest sync';
16
16
 
@@ -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 |
@@ -107,19 +107,23 @@ 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).
113
115
 
114
- 2. **Network inspector (full schema in one shot, no MCP needed)** -- in Studio's Data tab with browser DevTools open, filter Network requests by `tablespace-with-tables`. The Response JSON contains every table's complete schema, including:
116
+ 2. **`get-softr-database` CLI script (bundled, no MCP needed)** -- a Python CLI bundled with this skill at `~/.claude/skills/softr-vibe-coding/tools/get-softr-database.py`. Exports the full schema (every table, every field, all dropdown option UUIDs) to `~/Desktop/softr-database-<id>-<timestamp>.json`. Run with `python3 ~/.claude/skills/softr-vibe-coding/tools/get-softr-database.py <database_id>` (prompts for API key) or set `SOFTR_API_KEY=xxx` env var to skip the prompt. Stdlib only, no `pip install`. Best when you want a portable JSON dump for sharing in chat, archiving, or diffing across schema versions. Full usage in [softr-database.md](softr-database.md#bundled-cli-script-get-softr-database).
117
+
118
+ 3. **Network inspector (full schema in one shot, no MCP needed)** -- in Studio's Data tab with browser DevTools open, filter Network requests by `tablespace-with-tables`. The Response JSON contains every table's complete schema, including:
115
119
  - Each field's `id`, `name`, `type`, and `options`
116
120
  - For dropdown / SELECT fields: the full `choices` array with every option's `id` (UUID), `label`, and `color`
117
121
 
118
122
  Use this when scaffolding a block that needs many field IDs at once, or to look up dropdown option UUIDs needed for write payloads. **When working with an AI assistant without the MCP installed**, paste this JSON response into the chat -- second-best way to share accurate field IDs and dropdown UUIDs in one shot.
119
123
 
120
- 3. **Inline in Studio (one field at a time)** -- in the Data tab, click a field's name to open its edit drawer. The field ID appears next to the "Field name" label (e.g. `ID: 37fts`). Fastest for spot-checking a single field.
124
+ 4. **Inline in Studio (one field at a time)** -- in the Data tab, click a field's name to open its edit drawer. The field ID appears next to the "Field name" label (e.g. `ID: 37fts`). Fastest for spot-checking a single field.
121
125
 
122
- 4. **Softr Database REST API with `fieldNames=true`** -- runtime inspection from inside a Vibe Coding block (internal-portal blocks only, since this exposes a PAT in client code):
126
+ 5. **Softr Database REST API with `fieldNames=true`** -- runtime inspection from inside a Vibe Coding block (internal-portal blocks only, since this exposes a PAT in client code):
123
127
 
124
128
  ```jsx
125
129
  import { useEffect, useState } from "react";
@@ -24,11 +24,62 @@ q.select({ name: "First Name" })
24
24
  Find field IDs in this order of preference:
25
25
 
26
26
  1. **Softr Database MCP** (recommended when working with an AI assistant) — the AI calls schema/list-fields tools directly. See [../references/softr-database-mcp.md](../references/softr-database-mcp.md).
27
- 2. **Network inspector** — DevTools -> Network -> filter `tablespace-with-tables` for the full schema including dropdown option UUIDs. Paste the JSON into chat to share with an AI when the MCP isn't installed.
28
- 3. **Inline in Studio** — click a field's name in the Data tab; the ID appears in the field-edit drawer.
27
+ 2. **`get-softr-database` CLI script (bundled)** — a Python CLI bundled with this skill at `~/.claude/skills/softr-vibe-coding/tools/get-softr-database.py`. Exports the full schema (every table, field, dropdown option UUID) to `~/Desktop/softr-database-<id>-<timestamp>.json`. Stdlib only, no `pip install`. See [Bundled CLI script](#bundled-cli-script-get-softr-database) below.
28
+ 3. **Network inspector** — DevTools -> Network -> filter `tablespace-with-tables` for the full schema including dropdown option UUIDs. Paste the JSON into chat to share with an AI when the MCP isn't installed.
29
+ 4. **Inline in Studio** — click a field's name in the Data tab; the ID appears in the field-edit drawer.
29
30
 
30
31
  The generic Field Inspector pattern with empty `q.select({})` does NOT work for Softr Database — see [fields.md](fields.md#field-inspector-block).
31
32
 
33
+ ## Bundled CLI script: `get-softr-database`
34
+
35
+ A Python CLI bundled with this skill that exports a complete Softr Tables database schema (every table, every field, all dropdown option UUIDs) to a timestamped JSON file on your Desktop. Stdlib only — no `pip install` required.
36
+
37
+ **Script location after `npx softr-vibe-coding@latest init`:**
38
+
39
+ ```
40
+ ~/.claude/skills/softr-vibe-coding/tools/get-softr-database.py
41
+ ```
42
+
43
+ **Run it directly:**
44
+
45
+ ```bash
46
+ python3 ~/.claude/skills/softr-vibe-coding/tools/get-softr-database.py <database_id>
47
+ ```
48
+
49
+ It prompts for your Softr API key (input hidden via `getpass`). To skip the prompt entirely, pass via env var:
50
+
51
+ ```bash
52
+ SOFTR_API_KEY=xxx python3 ~/.claude/skills/softr-vibe-coding/tools/get-softr-database.py <database_id>
53
+ ```
54
+
55
+ Run with no args to be prompted for both the database ID and API key.
56
+
57
+ **Output:** `~/Desktop/softr-database-<databaseId>-<YYYYMMDD-HHMMSS>.json` containing:
58
+
59
+ ```json
60
+ {
61
+ "exportedAt": "...",
62
+ "source": "https://tables-api.softr.io/api/v1",
63
+ "databaseId": "...",
64
+ "database": { /* full database metadata */ },
65
+ "tableCount": N,
66
+ "fieldCount": M,
67
+ "tables": [ /* every table with its full fields[] array */ ]
68
+ }
69
+ ```
70
+
71
+ **Optional alias** for a shorter command. Add to your `~/.zshrc` or `~/.bashrc`:
72
+
73
+ ```bash
74
+ alias get-softr-database='python3 ~/.claude/skills/softr-vibe-coding/tools/get-softr-database.py'
75
+ ```
76
+
77
+ After `source ~/.zshrc`, just run `get-softr-database <database_id>` from anywhere.
78
+
79
+ **Get your Softr API key:** Softr workspace settings → API keys → create a new key with read access to the target database.
80
+
81
+ **When to use vs the MCP:** the MCP server is better for AI-assisted workflows (the assistant calls schema tools directly without any user action). This CLI script is better when you want a portable JSON file — for sharing in chat, archiving alongside your project, diffing across schema versions, or pasting a single big blob into Claude. The two approaches don't conflict; many projects use both.
82
+
32
83
  ## Supported Fields
33
84
 
34
85
  | Field Type | Writable | Notes |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "softr-vibe-coding",
3
- "version": "1.8.0",
3
+ "version": "1.10.0",
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"
@@ -11,6 +11,7 @@
11
11
  "ui-ux-guidelines.md",
12
12
  "references/",
13
13
  "datasources/",
14
+ "tools/",
14
15
  "LICENSE",
15
16
  "README.md",
16
17
  "CHANGELOG.md"
@@ -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
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ get-softr-database
4
+ ==================
5
+ Export a full Softr Tables database **schema** (every table, every field, and all
6
+ of their details) to a timestamped JSON file on your Desktop.
7
+
8
+ What it does
9
+ ------------
10
+ Given a Softr API key and a database ID, it calls two endpoints:
11
+
12
+ GET /api/v1/databases/{id} -> database metadata
13
+ GET /api/v1/databases/{id}/tables -> all tables, each with its full
14
+ `fields` array (type, options,
15
+ choices, formulas, AI settings, ...)
16
+
17
+ and writes the combined result to:
18
+
19
+ ~/Desktop/softr-database-<databaseId>-<YYYYMMDD-HHMMSS>.json
20
+
21
+ Usage
22
+ -----
23
+ get-softr-database # prompts for DB ID, then API key (hidden)
24
+ get-softr-database <database_id> # prompts only for the API key
25
+ SOFTR_API_KEY=xxx get-softr-database <database_id> # fully non-interactive
26
+
27
+ The API key may also be supplied via the SOFTR_API_KEY environment variable, so
28
+ it never has to be typed (or stored) in plain text.
29
+
30
+ Only the Python standard library is used — no `pip install` required.
31
+ """
32
+
33
+ import os
34
+ import sys
35
+ import json
36
+ import getpass
37
+ import datetime
38
+ import urllib.request
39
+ import urllib.error
40
+
41
+ API_BASE = "https://tables-api.softr.io/api/v1"
42
+ TIMEOUT_SECONDS = 60
43
+
44
+
45
+ def api_get(path, api_key):
46
+ """GET a Softr Tables API path and return the parsed JSON body."""
47
+ req = urllib.request.Request(
48
+ API_BASE + path,
49
+ headers={
50
+ "Softr-Api-Key": api_key,
51
+ "Content-Type": "application/json",
52
+ },
53
+ method="GET",
54
+ )
55
+ try:
56
+ with urllib.request.urlopen(req, timeout=TIMEOUT_SECONDS) as resp:
57
+ return json.loads(resp.read().decode("utf-8"))
58
+ except urllib.error.HTTPError as e:
59
+ body = e.read().decode("utf-8", "replace")
60
+ hint = ""
61
+ if e.code in (401, 403):
62
+ hint = "\n (Check that the API key is correct and has access to this database.)"
63
+ elif e.code == 404:
64
+ hint = "\n (Check that the database ID is correct.)"
65
+ raise SystemExit(f"\n[x] HTTP {e.code} on GET {path}\n {body}{hint}")
66
+ except urllib.error.URLError as e:
67
+ raise SystemExit(f"\n[x] Network error on GET {path}: {e.reason}")
68
+ except json.JSONDecodeError:
69
+ raise SystemExit(f"\n[x] Could not parse JSON response from GET {path}")
70
+
71
+
72
+ def prompt_database_id():
73
+ if len(sys.argv) > 1 and sys.argv[1].strip():
74
+ return sys.argv[1].strip()
75
+ try:
76
+ value = input("Softr database ID: ").strip()
77
+ except (EOFError, KeyboardInterrupt):
78
+ raise SystemExit("\n[x] Cancelled.")
79
+ if not value:
80
+ raise SystemExit("[x] No database ID provided.")
81
+ return value
82
+
83
+
84
+ def prompt_api_key():
85
+ value = os.environ.get("SOFTR_API_KEY", "").strip()
86
+ if value:
87
+ return value
88
+ try:
89
+ value = getpass.getpass("Softr API key (input hidden): ").strip()
90
+ except (EOFError, KeyboardInterrupt):
91
+ raise SystemExit("\n[x] Cancelled.")
92
+ if not value:
93
+ raise SystemExit("[x] No API key provided.")
94
+ return value
95
+
96
+
97
+ def output_dir():
98
+ desktop = os.path.join(os.path.expanduser("~"), "Desktop")
99
+ return desktop if os.path.isdir(desktop) else os.path.expanduser("~")
100
+
101
+
102
+ def main():
103
+ database_id = prompt_database_id()
104
+ api_key = prompt_api_key()
105
+
106
+ print(f"\n-> Fetching database {database_id} ...")
107
+ db = api_get(f"/databases/{database_id}", api_key).get("data", {}) or {}
108
+ print(f" Database: {db.get('name', '(unknown)')} "
109
+ f"({db.get('tablesCount', '?')} tables reported)")
110
+
111
+ print("-> Fetching tables and fields ...")
112
+ tables = api_get(f"/databases/{database_id}/tables", api_key).get("data", []) or []
113
+ total_fields = sum(len(t.get("fields", []) or []) for t in tables)
114
+ print(f" Retrieved {len(tables)} tables, {total_fields} fields total.")
115
+
116
+ now = datetime.datetime.now()
117
+ payload = {
118
+ "exportedAt": now.isoformat(timespec="seconds"),
119
+ "source": API_BASE,
120
+ "databaseId": database_id,
121
+ "database": db,
122
+ "tableCount": len(tables),
123
+ "fieldCount": total_fields,
124
+ "tables": tables,
125
+ }
126
+
127
+ stamp = now.strftime("%Y%m%d-%H%M%S")
128
+ filename = f"softr-database-{database_id}-{stamp}.json"
129
+ path = os.path.join(output_dir(), filename)
130
+
131
+ with open(path, "w", encoding="utf-8") as f:
132
+ json.dump(payload, f, indent=2, ensure_ascii=False)
133
+
134
+ print(f"\n[ok] Saved schema to:\n {path}")
135
+
136
+ # Brief per-table summary so the result is readable at a glance.
137
+ if tables:
138
+ print("\n Tables:")
139
+ for t in tables:
140
+ print(f" - {t.get('name', '(unnamed)')}: "
141
+ f"{len(t.get('fields', []) or [])} fields")
142
+
143
+
144
+ if __name__ == "__main__":
145
+ main()