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 +7 -0
- package/SKILL.md +3 -2
- package/bin/cli.js +1 -1
- package/datasources/airtable.md +56 -0
- package/datasources/fields.md +7 -3
- package/datasources/softr-database.md +53 -2
- package/package.json +2 -1
- package/tools/get-airtable-base +303 -0
- package/tools/get-softr-database.py +145 -0
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,
|
|
90
|
-
- For **Airtable** and
|
|
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
|
|
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,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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
28
|
-
3. **
|
|
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.
|
|
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()
|