morphdb 0.1.2__tar.gz → 0.1.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. {morphdb-0.1.2 → morphdb-0.1.3}/PKG-INFO +123 -69
  2. {morphdb-0.1.2 → morphdb-0.1.3}/README.md +122 -68
  3. {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/__init__.py +1 -1
  4. {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/cli/__init__.py +8 -6
  5. {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/cli/main.py +55 -15
  6. {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/cli/skill.py +10 -8
  7. {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/skill/SKILL.md +3 -2
  8. {morphdb-0.1.2 → morphdb-0.1.3}/morphdb.egg-info/PKG-INFO +123 -69
  9. {morphdb-0.1.2 → morphdb-0.1.3}/pyproject.toml +1 -1
  10. {morphdb-0.1.2 → morphdb-0.1.3}/tests/test_cli.py +40 -8
  11. {morphdb-0.1.2 → morphdb-0.1.3}/LICENSE +0 -0
  12. {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/__main__.py +0 -0
  13. {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/apps.py +0 -0
  14. {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/associations.py +0 -0
  15. {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/cli/dashboard.py +0 -0
  16. {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/cli/service.py +0 -0
  17. {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/db.py +0 -0
  18. {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/errors.py +0 -0
  19. {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/fieldtypes.py +0 -0
  20. {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/objects.py +0 -0
  21. {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/router.py +0 -0
  22. {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/routes.py +0 -0
  23. {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/schema.py +0 -0
  24. {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/server.py +0 -0
  25. {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/skill/scripts/morphdb_schema.py +0 -0
  26. {morphdb-0.1.2 → morphdb-0.1.3}/morphdb/util.py +0 -0
  27. {morphdb-0.1.2 → morphdb-0.1.3}/morphdb.egg-info/SOURCES.txt +0 -0
  28. {morphdb-0.1.2 → morphdb-0.1.3}/morphdb.egg-info/dependency_links.txt +0 -0
  29. {morphdb-0.1.2 → morphdb-0.1.3}/morphdb.egg-info/entry_points.txt +0 -0
  30. {morphdb-0.1.2 → morphdb-0.1.3}/morphdb.egg-info/requires.txt +0 -0
  31. {morphdb-0.1.2 → morphdb-0.1.3}/morphdb.egg-info/top_level.txt +0 -0
  32. {morphdb-0.1.2 → morphdb-0.1.3}/setup.cfg +0 -0
  33. {morphdb-0.1.2 → morphdb-0.1.3}/tests/test_apps.py +0 -0
  34. {morphdb-0.1.2 → morphdb-0.1.3}/tests/test_core.py +0 -0
  35. {morphdb-0.1.2 → morphdb-0.1.3}/tests/test_hardening.py +0 -0
  36. {morphdb-0.1.2 → morphdb-0.1.3}/tests/test_relations.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: morphdb
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: A coding-agent-friendly, multi-tenant backend for vibe-coded websites. One process hosts many isolated apps; reshape each app's schema as fast as your agent iterates while the frontend keeps calling the same generic endpoints.
5
5
  Author: morphdb contributors
6
6
  License: MIT
@@ -30,21 +30,119 @@ Dynamic: license-file
30
30
  **A coding-agent-friendly, multi-tenant backend for vibe-coded websites.**
31
31
 
32
32
  Reshape the data model as fast as your coding agent iterates — the frontend
33
- keeps calling the same small set of generic, deterministic endpoints.
33
+ keeps calling the same small set of generic, deterministic endpoints. One
34
+ process hosts many isolated apps (one per site), zero dependencies, backed by
35
+ SQLite.
34
36
 
37
+ ## Install
38
+
39
+ ```bash
40
+ pip install morphdb
35
41
  ```
36
- you (the coding agent) the frontend you build
37
- ────────────────────── ──────────────────────
38
- reshape the schema freely │ calls fixed generic endpoints
39
- PUT /schema/{type} │ POST /objects/{type}
40
- GET /schema │ GET /objects/{type}?field=…
41
- DELETE /schema/{type} │ PATCH /objects/{type}/{guid}
42
- │ │
43
- └────────────── MorphDB ───────────┘
44
- (one process · many apps · SQLite)
45
- every call: X-App-Key: <app>
42
+
43
+ Manage the local server with the `morphdb` CLI:
44
+
45
+ ```bash
46
+ morphdb start # run in the background (default 127.0.0.1:8787)
47
+ morphdb status # running? where? how many apps?
48
+ morphdb stop # stop it
49
+ morphdb run # run in the foreground instead (blocking)
50
+ morphdb dashboard # read-only web view of every app + its tables
51
+ morphdb install-skill # install the MorphDB Claude Code skill (into ~/.claude)
52
+ ```
53
+
54
+ Data lives in `~/.morphdb/data.sqlite3` (change it with `--db PATH` or
55
+ `--db :memory:`; move the state dir with `$MORPHDB_HOME`). Server flags:
56
+ `--host`, `--port`, `--db`. From a source checkout with no install, the
57
+ foreground server is `python3 -m morphdb --port 8787 --db ./app.sqlite3`.
58
+
59
+ To upgrade later: `pip install -U morphdb`, then `morphdb stop && morphdb start`
60
+ to reload the new code (data in `~/.morphdb` is preserved across `0.1.x`).
61
+
62
+ **Pointing clients at a hosted MorphDB.** Set `MORPHDB_HOST` to a full URL (e.g.
63
+ `https://db.example.com`) and the schema CLI — plus any frontend that reads
64
+ `window.MORPHDB_HOST` — calls that hosted server (running this same code) instead
65
+ of localhost. It's a client-side setting that names a *backend*, not a database
66
+ connection string.
67
+
68
+ ## Use it
69
+
70
+ With the server running (`morphdb start`):
71
+
72
+ ```bash
73
+ BASE=http://127.0.0.1:8787
74
+
75
+ # 0. register an app; send its key as X-App-Key on every schema/object call
76
+ curl -X POST $BASE/app -d '{"key":"my-site"}'
77
+ H="X-App-Key: my-site"
78
+
79
+ # 1. define types + a relation
80
+ curl -X PUT $BASE/schema/user -H "$H" -d '{"fields":{"name":"string"}}'
81
+ curl -X PUT $BASE/schema/task -H "$H" -d '{
82
+ "fields": {"title":"string","done":"boolean","priority":"number"},
83
+ "relations": {"assignee":{"to":"user","cardinality":"many_to_one","inverse":"tasks"}}}'
84
+
85
+ # 2. create + read + query
86
+ U=$(curl -s -X POST $BASE/objects/user -H "$H" -d '{"name":"Ann"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["_guid"])')
87
+ curl -X POST $BASE/objects/task -H "$H" -d "{\"title\":\"buy milk\",\"priority\":2,\"assignee\":\"$U\"}"
88
+ curl -H "$H" "$BASE/objects/task?done=false&sort=priority&order=desc"
89
+ curl -H "$H" "$BASE/objects/user/$U" # → includes "tasks":[…]
90
+
91
+ # 3. morph the schema later — existing rows just gain the new field as null
92
+ curl -X PUT $BASE/schema/task -H "$H" -d '{"merge":true,"fields":{"due":"datetime"}}'
93
+ ```
94
+
95
+ See `examples/todo/index.html` for a complete single-file frontend backed by MorphDB.
96
+
97
+ ## Command-line interface
98
+
99
+ `morphdb` runs the server as a **background service** — `start` launches it
100
+ detached and hands your terminal straight back; `status` / `stop` find it again
101
+ via a pid file under the state dir.
102
+
103
+ | Command | What it does |
104
+ | --- | --- |
105
+ | `morphdb` or `morphdb start` | Start the server in the background (returns immediately). |
106
+ | `morphdb status` | Is it running? URL, pid, health, and app count. |
107
+ | `morphdb stop` | Stop the background server. |
108
+ | `morphdb logs` | Show the background server's log (`-n N` lines, `-f` to follow). |
109
+ | `morphdb run` | Run in the **foreground** (blocking) instead. |
110
+ | `morphdb dashboard` | Open a read-only web view of every app and its tables. |
111
+ | `morphdb install-skill` | Install the bundled Claude Code skill (below). |
112
+ | `morphdb --version` | Print the version. |
113
+
114
+ `start` / `run` accept `--host` (default `127.0.0.1`), `--port` (default `8787`),
115
+ and `--db` (a SQLite path or `:memory:`; default `~/.morphdb/data.sqlite3`).
116
+ `dashboard` accepts `--port` (default `8788`), `--db`, and `--no-open`. Service
117
+ state (pid, log, the default db) lives under `~/.morphdb` — relocate it with
118
+ `$MORPHDB_HOME`.
119
+
120
+ ```bash
121
+ morphdb start # background, default 127.0.0.1:8787
122
+ morphdb start --port 9000 --db ./my.sqlite3
123
+ morphdb status # -> running (pid …) at http://… [healthy]
124
+ morphdb dashboard # opens http://127.0.0.1:8788
125
+ morphdb stop
126
+ morphdb run # foreground instead (Ctrl-C to quit)
127
+ ```
128
+
129
+ ### Install the Claude Code skill
130
+
131
+ `install-skill` writes the bundled MorphDB skill into a Claude skills directory,
132
+ so a coding agent automatically reaches for MorphDB when building a data-backed
133
+ site:
134
+
135
+ ```bash
136
+ morphdb install-skill # -> ~/.claude/skills/morphdb (all projects)
137
+ morphdb install-skill --project # -> ./.claude/skills/morphdb (current project)
138
+ morphdb install-skill --project DIR # -> DIR/.claude/skills/morphdb
46
139
  ```
47
140
 
141
+ It installs the skill **bundled in the installed package** (not live from
142
+ GitHub) and is **idempotent** — re-running overwrites with the current version.
143
+ To get the newest skill, `pip install -U morphdb` first, then re-run. Restart
144
+ Claude Code afterward to pick it up.
145
+
48
146
  ## Why
49
147
 
50
148
  AI coding agents are great at building HTML/CSS/JS frontends but thrash hard on
@@ -58,6 +156,19 @@ invalidation). Adding, removing, or retyping a field is an O(1) metadata edit
58
156
  **no migration, no row rewrite, no downtime** — regardless of how much data
59
157
  exists. Meanwhile the frontend talks to generic endpoints that never change.
60
158
 
159
+ ```
160
+ you (the coding agent) the frontend you build
161
+ ────────────────────── ──────────────────────
162
+ reshape the schema freely │ calls fixed generic endpoints
163
+ PUT /schema/{type} │ POST /objects/{type}
164
+ GET /schema │ GET /objects/{type}?field=…
165
+ DELETE /schema/{type} │ PATCH /objects/{type}/{guid}
166
+ │ │
167
+ └────────────── MorphDB ───────────┘
168
+ (one process · many apps · SQLite)
169
+ every call: X-App-Key: <app>
170
+ ```
171
+
61
172
  ## The shape of it
62
173
 
63
174
  One MorphDB process hosts **many apps** (one per website), fully isolated from
@@ -126,63 +237,6 @@ curl -X PATCH $BASE/objects/user/<u> -d '{"tasks":["<t1>","<t2>"]}'
126
237
  > Scope: a localhost-scale developer tool. Not built for multi-tenant auth,
127
238
  > horizontal scale, or production durability guarantees.
128
239
 
129
- ## Install / run
130
-
131
- ```bash
132
- pip install morphdb
133
- ```
134
-
135
- Manage the local server with the `morphdb` CLI:
136
-
137
- ```bash
138
- morphdb start # run in the background (default 127.0.0.1:8787)
139
- morphdb status # running? where? how many apps?
140
- morphdb stop # stop it
141
- morphdb run # run in the foreground instead (blocking)
142
- morphdb dashboard # read-only web view of every app + its tables
143
- morphdb install-skill # install the MorphDB Claude Code skill (into ~/.claude)
144
- ```
145
-
146
- Data lives in `~/.morphdb/data.sqlite3` (change it with `--db PATH` or
147
- `--db :memory:`; move the state dir with `$MORPHDB_HOME`). Server flags:
148
- `--host`, `--port`, `--db`. From a source checkout with no install, the
149
- foreground server is `python3 -m morphdb --port 8787 --db ./app.sqlite3`.
150
-
151
- Then: `curl http://127.0.0.1:8787/help` for a live reference.
152
-
153
- **Pointing clients at a hosted MorphDB.** Set `MORPHDB_HOST` to a full URL (e.g.
154
- `https://db.example.com`) and the schema CLI — plus any frontend that reads
155
- `window.MORPHDB_HOST` — calls that hosted server (running this same code) instead
156
- of localhost. It's a client-side setting that names a *backend*, not a database
157
- connection string.
158
-
159
- ## Quickstart
160
-
161
- ```bash
162
- BASE=http://127.0.0.1:8787
163
-
164
- # 0. register an app; send its key as X-App-Key on every schema/object call
165
- curl -X POST $BASE/app -d '{"key":"my-site"}'
166
- H="X-App-Key: my-site"
167
-
168
- # 1. define types + a relation
169
- curl -X PUT $BASE/schema/user -H "$H" -d '{"fields":{"name":"string"}}'
170
- curl -X PUT $BASE/schema/task -H "$H" -d '{
171
- "fields": {"title":"string","done":"boolean","priority":"number"},
172
- "relations": {"assignee":{"to":"user","cardinality":"many_to_one","inverse":"tasks"}}}'
173
-
174
- # 2. create + read + query
175
- U=$(curl -s -X POST $BASE/objects/user -H "$H" -d '{"name":"Ann"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["_guid"])')
176
- curl -X POST $BASE/objects/task -H "$H" -d "{\"title\":\"buy milk\",\"priority\":2,\"assignee\":\"$U\"}"
177
- curl -H "$H" "$BASE/objects/task?done=false&sort=priority&order=desc"
178
- curl -H "$H" "$BASE/objects/user/$U" # → includes "tasks":[…]
179
-
180
- # 3. morph the schema later — existing rows just gain the new field as null
181
- curl -X PUT $BASE/schema/task -H "$H" -d '{"merge":true,"fields":{"due":"datetime"}}'
182
- ```
183
-
184
- See `examples/todo/index.html` for a complete single-file frontend backed by MorphDB.
185
-
186
240
  ## Data model
187
241
 
188
242
  | Concept | What it is |
@@ -3,21 +3,119 @@
3
3
  **A coding-agent-friendly, multi-tenant backend for vibe-coded websites.**
4
4
 
5
5
  Reshape the data model as fast as your coding agent iterates — the frontend
6
- keeps calling the same small set of generic, deterministic endpoints.
6
+ keeps calling the same small set of generic, deterministic endpoints. One
7
+ process hosts many isolated apps (one per site), zero dependencies, backed by
8
+ SQLite.
7
9
 
10
+ ## Install
11
+
12
+ ```bash
13
+ pip install morphdb
8
14
  ```
9
- you (the coding agent) the frontend you build
10
- ────────────────────── ──────────────────────
11
- reshape the schema freely │ calls fixed generic endpoints
12
- PUT /schema/{type} │ POST /objects/{type}
13
- GET /schema │ GET /objects/{type}?field=…
14
- DELETE /schema/{type} │ PATCH /objects/{type}/{guid}
15
- │ │
16
- └────────────── MorphDB ───────────┘
17
- (one process · many apps · SQLite)
18
- every call: X-App-Key: <app>
15
+
16
+ Manage the local server with the `morphdb` CLI:
17
+
18
+ ```bash
19
+ morphdb start # run in the background (default 127.0.0.1:8787)
20
+ morphdb status # running? where? how many apps?
21
+ morphdb stop # stop it
22
+ morphdb run # run in the foreground instead (blocking)
23
+ morphdb dashboard # read-only web view of every app + its tables
24
+ morphdb install-skill # install the MorphDB Claude Code skill (into ~/.claude)
25
+ ```
26
+
27
+ Data lives in `~/.morphdb/data.sqlite3` (change it with `--db PATH` or
28
+ `--db :memory:`; move the state dir with `$MORPHDB_HOME`). Server flags:
29
+ `--host`, `--port`, `--db`. From a source checkout with no install, the
30
+ foreground server is `python3 -m morphdb --port 8787 --db ./app.sqlite3`.
31
+
32
+ To upgrade later: `pip install -U morphdb`, then `morphdb stop && morphdb start`
33
+ to reload the new code (data in `~/.morphdb` is preserved across `0.1.x`).
34
+
35
+ **Pointing clients at a hosted MorphDB.** Set `MORPHDB_HOST` to a full URL (e.g.
36
+ `https://db.example.com`) and the schema CLI — plus any frontend that reads
37
+ `window.MORPHDB_HOST` — calls that hosted server (running this same code) instead
38
+ of localhost. It's a client-side setting that names a *backend*, not a database
39
+ connection string.
40
+
41
+ ## Use it
42
+
43
+ With the server running (`morphdb start`):
44
+
45
+ ```bash
46
+ BASE=http://127.0.0.1:8787
47
+
48
+ # 0. register an app; send its key as X-App-Key on every schema/object call
49
+ curl -X POST $BASE/app -d '{"key":"my-site"}'
50
+ H="X-App-Key: my-site"
51
+
52
+ # 1. define types + a relation
53
+ curl -X PUT $BASE/schema/user -H "$H" -d '{"fields":{"name":"string"}}'
54
+ curl -X PUT $BASE/schema/task -H "$H" -d '{
55
+ "fields": {"title":"string","done":"boolean","priority":"number"},
56
+ "relations": {"assignee":{"to":"user","cardinality":"many_to_one","inverse":"tasks"}}}'
57
+
58
+ # 2. create + read + query
59
+ U=$(curl -s -X POST $BASE/objects/user -H "$H" -d '{"name":"Ann"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["_guid"])')
60
+ curl -X POST $BASE/objects/task -H "$H" -d "{\"title\":\"buy milk\",\"priority\":2,\"assignee\":\"$U\"}"
61
+ curl -H "$H" "$BASE/objects/task?done=false&sort=priority&order=desc"
62
+ curl -H "$H" "$BASE/objects/user/$U" # → includes "tasks":[…]
63
+
64
+ # 3. morph the schema later — existing rows just gain the new field as null
65
+ curl -X PUT $BASE/schema/task -H "$H" -d '{"merge":true,"fields":{"due":"datetime"}}'
66
+ ```
67
+
68
+ See `examples/todo/index.html` for a complete single-file frontend backed by MorphDB.
69
+
70
+ ## Command-line interface
71
+
72
+ `morphdb` runs the server as a **background service** — `start` launches it
73
+ detached and hands your terminal straight back; `status` / `stop` find it again
74
+ via a pid file under the state dir.
75
+
76
+ | Command | What it does |
77
+ | --- | --- |
78
+ | `morphdb` or `morphdb start` | Start the server in the background (returns immediately). |
79
+ | `morphdb status` | Is it running? URL, pid, health, and app count. |
80
+ | `morphdb stop` | Stop the background server. |
81
+ | `morphdb logs` | Show the background server's log (`-n N` lines, `-f` to follow). |
82
+ | `morphdb run` | Run in the **foreground** (blocking) instead. |
83
+ | `morphdb dashboard` | Open a read-only web view of every app and its tables. |
84
+ | `morphdb install-skill` | Install the bundled Claude Code skill (below). |
85
+ | `morphdb --version` | Print the version. |
86
+
87
+ `start` / `run` accept `--host` (default `127.0.0.1`), `--port` (default `8787`),
88
+ and `--db` (a SQLite path or `:memory:`; default `~/.morphdb/data.sqlite3`).
89
+ `dashboard` accepts `--port` (default `8788`), `--db`, and `--no-open`. Service
90
+ state (pid, log, the default db) lives under `~/.morphdb` — relocate it with
91
+ `$MORPHDB_HOME`.
92
+
93
+ ```bash
94
+ morphdb start # background, default 127.0.0.1:8787
95
+ morphdb start --port 9000 --db ./my.sqlite3
96
+ morphdb status # -> running (pid …) at http://… [healthy]
97
+ morphdb dashboard # opens http://127.0.0.1:8788
98
+ morphdb stop
99
+ morphdb run # foreground instead (Ctrl-C to quit)
100
+ ```
101
+
102
+ ### Install the Claude Code skill
103
+
104
+ `install-skill` writes the bundled MorphDB skill into a Claude skills directory,
105
+ so a coding agent automatically reaches for MorphDB when building a data-backed
106
+ site:
107
+
108
+ ```bash
109
+ morphdb install-skill # -> ~/.claude/skills/morphdb (all projects)
110
+ morphdb install-skill --project # -> ./.claude/skills/morphdb (current project)
111
+ morphdb install-skill --project DIR # -> DIR/.claude/skills/morphdb
19
112
  ```
20
113
 
114
+ It installs the skill **bundled in the installed package** (not live from
115
+ GitHub) and is **idempotent** — re-running overwrites with the current version.
116
+ To get the newest skill, `pip install -U morphdb` first, then re-run. Restart
117
+ Claude Code afterward to pick it up.
118
+
21
119
  ## Why
22
120
 
23
121
  AI coding agents are great at building HTML/CSS/JS frontends but thrash hard on
@@ -31,6 +129,19 @@ invalidation). Adding, removing, or retyping a field is an O(1) metadata edit
31
129
  **no migration, no row rewrite, no downtime** — regardless of how much data
32
130
  exists. Meanwhile the frontend talks to generic endpoints that never change.
33
131
 
132
+ ```
133
+ you (the coding agent) the frontend you build
134
+ ────────────────────── ──────────────────────
135
+ reshape the schema freely │ calls fixed generic endpoints
136
+ PUT /schema/{type} │ POST /objects/{type}
137
+ GET /schema │ GET /objects/{type}?field=…
138
+ DELETE /schema/{type} │ PATCH /objects/{type}/{guid}
139
+ │ │
140
+ └────────────── MorphDB ───────────┘
141
+ (one process · many apps · SQLite)
142
+ every call: X-App-Key: <app>
143
+ ```
144
+
34
145
  ## The shape of it
35
146
 
36
147
  One MorphDB process hosts **many apps** (one per website), fully isolated from
@@ -99,63 +210,6 @@ curl -X PATCH $BASE/objects/user/<u> -d '{"tasks":["<t1>","<t2>"]}'
99
210
  > Scope: a localhost-scale developer tool. Not built for multi-tenant auth,
100
211
  > horizontal scale, or production durability guarantees.
101
212
 
102
- ## Install / run
103
-
104
- ```bash
105
- pip install morphdb
106
- ```
107
-
108
- Manage the local server with the `morphdb` CLI:
109
-
110
- ```bash
111
- morphdb start # run in the background (default 127.0.0.1:8787)
112
- morphdb status # running? where? how many apps?
113
- morphdb stop # stop it
114
- morphdb run # run in the foreground instead (blocking)
115
- morphdb dashboard # read-only web view of every app + its tables
116
- morphdb install-skill # install the MorphDB Claude Code skill (into ~/.claude)
117
- ```
118
-
119
- Data lives in `~/.morphdb/data.sqlite3` (change it with `--db PATH` or
120
- `--db :memory:`; move the state dir with `$MORPHDB_HOME`). Server flags:
121
- `--host`, `--port`, `--db`. From a source checkout with no install, the
122
- foreground server is `python3 -m morphdb --port 8787 --db ./app.sqlite3`.
123
-
124
- Then: `curl http://127.0.0.1:8787/help` for a live reference.
125
-
126
- **Pointing clients at a hosted MorphDB.** Set `MORPHDB_HOST` to a full URL (e.g.
127
- `https://db.example.com`) and the schema CLI — plus any frontend that reads
128
- `window.MORPHDB_HOST` — calls that hosted server (running this same code) instead
129
- of localhost. It's a client-side setting that names a *backend*, not a database
130
- connection string.
131
-
132
- ## Quickstart
133
-
134
- ```bash
135
- BASE=http://127.0.0.1:8787
136
-
137
- # 0. register an app; send its key as X-App-Key on every schema/object call
138
- curl -X POST $BASE/app -d '{"key":"my-site"}'
139
- H="X-App-Key: my-site"
140
-
141
- # 1. define types + a relation
142
- curl -X PUT $BASE/schema/user -H "$H" -d '{"fields":{"name":"string"}}'
143
- curl -X PUT $BASE/schema/task -H "$H" -d '{
144
- "fields": {"title":"string","done":"boolean","priority":"number"},
145
- "relations": {"assignee":{"to":"user","cardinality":"many_to_one","inverse":"tasks"}}}'
146
-
147
- # 2. create + read + query
148
- U=$(curl -s -X POST $BASE/objects/user -H "$H" -d '{"name":"Ann"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["_guid"])')
149
- curl -X POST $BASE/objects/task -H "$H" -d "{\"title\":\"buy milk\",\"priority\":2,\"assignee\":\"$U\"}"
150
- curl -H "$H" "$BASE/objects/task?done=false&sort=priority&order=desc"
151
- curl -H "$H" "$BASE/objects/user/$U" # → includes "tasks":[…]
152
-
153
- # 3. morph the schema later — existing rows just gain the new field as null
154
- curl -X PUT $BASE/schema/task -H "$H" -d '{"merge":true,"fields":{"due":"datetime"}}'
155
- ```
156
-
157
- See `examples/todo/index.html` for a complete single-file frontend backed by MorphDB.
158
-
159
213
  ## Data model
160
214
 
161
215
  | Concept | What it is |
@@ -5,4 +5,4 @@ coding agent iterates, while the frontend keeps calling the same small set of
5
5
  generic, deterministic endpoints.
6
6
  """
7
7
 
8
- __version__ = "0.1.2"
8
+ __version__ = "0.1.3"
@@ -6,12 +6,14 @@ view. It never changes how the core stores or serves data.
6
6
 
7
7
  Commands (see :mod:`morphdb.cli.main`):
8
8
 
9
- morphdb start the server in the background (alias of `start`)
10
- morphdb start same, explicit
11
- morphdb status is it running? where? how many apps?
12
- morphdb stop stop the background server
13
- morphdb run run in the foreground (blocking; for dev)
14
- morphdb dashboard open a read-only web view of every app + its tables
9
+ morphdb start the server in the background (alias of `start`)
10
+ morphdb start same, explicit
11
+ morphdb status is it running? where? how many apps?
12
+ morphdb stop stop the background server
13
+ morphdb logs show the server log (-f to follow)
14
+ morphdb run run in the foreground (blocking; for dev)
15
+ morphdb dashboard open a read-only web view of every app + its tables
16
+ morphdb install-skill install/update the bundled Claude Code skill
15
17
 
16
18
  Storage: the local server keeps data in a per-user SQLite file at
17
19
  ``~/.morphdb/data.sqlite3`` (override the file with ``--db``, or move the state
@@ -1,17 +1,20 @@
1
1
  """``morphdb`` console-script entry point: a small process/admin CLI.
2
2
 
3
- morphdb start the server in the background (alias of `start`)
4
- morphdb start start the server in the background
5
- morphdb status show whether it is running, where, and how many apps
6
- morphdb stop stop the background server
7
- morphdb run run the server in the foreground (blocking)
8
- morphdb dashboard open the read-only admin dashboard
3
+ morphdb start the server in the background (alias of `start`)
4
+ morphdb start start the server in the background
5
+ morphdb status show whether it is running, where, and how many apps
6
+ morphdb stop stop the background server
7
+ morphdb logs show the background server's log (-f to follow)
8
+ morphdb run run the server in the foreground (blocking)
9
+ morphdb dashboard open the read-only admin dashboard
10
+ morphdb install-skill install the bundled Claude Code skill
9
11
 
10
12
  ``python -m morphdb`` remains the plain foreground server (what `start` and the
11
13
  skill spawn under the hood); this CLI only wraps it.
12
14
  """
13
15
 
14
16
  import argparse
17
+ import os
15
18
  import sys
16
19
 
17
20
  from . import dashboard, service
@@ -60,6 +63,39 @@ def cmd_stop(args):
60
63
  return 0
61
64
 
62
65
 
66
+ def cmd_logs(args):
67
+ path = service.log_file()
68
+ if not os.path.exists(path):
69
+ print(f"No log yet at {path}. Has the server run? Try `morphdb start`.")
70
+ return 1
71
+ with open(path, "r", errors="replace") as f:
72
+ lines = f.readlines()
73
+ tail = lines[-args.lines:] if args.lines and args.lines > 0 else lines
74
+ sys.stdout.write("".join(tail))
75
+ if tail and not tail[-1].endswith("\n"):
76
+ sys.stdout.write("\n")
77
+ if args.follow:
78
+ _follow(path)
79
+ return 0
80
+
81
+
82
+ def _follow(path):
83
+ """Stream new lines appended to the log, like `tail -f`, until Ctrl-C."""
84
+ import time
85
+ with open(path, "r", errors="replace") as f:
86
+ f.seek(0, os.SEEK_END)
87
+ try:
88
+ while True:
89
+ line = f.readline()
90
+ if line:
91
+ sys.stdout.write(line)
92
+ sys.stdout.flush()
93
+ else:
94
+ time.sleep(0.3)
95
+ except KeyboardInterrupt:
96
+ pass
97
+
98
+
63
99
  def cmd_dashboard(args):
64
100
  dashboard.serve(args.db or service.default_db(), port=args.port,
65
101
  open_browser=not args.no_open)
@@ -68,16 +104,14 @@ def cmd_dashboard(args):
68
104
 
69
105
  def cmd_install_skill(args):
70
106
  try:
71
- dest = skill_mod.install_skill(project=args.project, force=args.force)
72
- except FileExistsError as e:
73
- print(f"Skill already installed at {e}. Re-run with --force to overwrite.")
74
- return 1
107
+ dest, existed = skill_mod.install_skill(project=args.project)
75
108
  except (FileNotFoundError, OSError) as e:
76
109
  print(f"Could not install skill: {e}")
77
110
  return 1
78
- where = "this project" if args.project else "your home (~/.claude)"
79
- print(f"Installed the 'morphdb' Claude skill to {dest}\n"
80
- f" ({where}). Restart Claude Code (or reload skills) to pick it up.")
111
+ verb = "Updated" if existed else "Installed"
112
+ where = "this project" if args.project else "~/.claude"
113
+ print(f"{verb} the 'morphdb' Claude skill at {dest} ({where}).\n"
114
+ f" Restart Claude Code (or reload skills) to pick it up.")
81
115
  return 0
82
116
 
83
117
 
@@ -112,6 +146,13 @@ def build_parser():
112
146
  sub.add_parser("stop", help="stop the background server"
113
147
  ).set_defaults(func=cmd_stop)
114
148
 
149
+ sp = sub.add_parser("logs", help="show the background server's log")
150
+ sp.add_argument("-n", "--lines", type=int, default=200,
151
+ help="number of trailing lines to show (default 200)")
152
+ sp.add_argument("-f", "--follow", action="store_true",
153
+ help="stream new log lines until Ctrl-C")
154
+ sp.set_defaults(func=cmd_logs)
155
+
115
156
  sp = sub.add_parser("dashboard", help="open the read-only admin dashboard")
116
157
  sp.add_argument("--port", type=int, default=8788, help="dashboard port (default 8788)")
117
158
  sp.add_argument("--db", default=None, help="database to inspect (default the server's)")
@@ -119,12 +160,11 @@ def build_parser():
119
160
  sp.set_defaults(func=cmd_dashboard)
120
161
 
121
162
  sp = sub.add_parser("install-skill",
122
- help="install the MorphDB skill into Claude Code")
163
+ help="install/update the bundled Claude Code skill")
123
164
  sp.add_argument("--project", nargs="?", const=".", default=None,
124
165
  metavar="DIR",
125
166
  help="install into a project's .claude (DIR, default cwd) "
126
167
  "instead of ~/.claude")
127
- sp.add_argument("--force", action="store_true", help="overwrite if it exists")
128
168
  sp.set_defaults(func=cmd_install_skill)
129
169
 
130
170
  return p
@@ -25,30 +25,32 @@ def _copy_tree(src, dst):
25
25
  f.write(src.read_bytes())
26
26
 
27
27
 
28
- def install_skill(claude_dir=None, project=None, force=False):
28
+ def install_skill(claude_dir=None, project=None):
29
29
  """Copy the packaged skill into a `.claude/skills/morphdb` directory.
30
30
 
31
+ Idempotent: re-running overwrites the destination with the skill bundled in
32
+ the *currently installed* ``morphdb`` package, so it always lands the version
33
+ you have (run ``pip install -U morphdb`` first to refresh to the latest). It
34
+ reads from package data, not from GitHub.
35
+
31
36
  ``project`` (a path, or "." for cwd) installs into that project's
32
37
  ``.claude``; otherwise it installs into ``~/.claude`` (all projects).
33
38
  ``claude_dir`` overrides the `.claude` location outright (used by tests).
34
- Returns the destination path. Raises FileExistsError if it already exists
35
- and ``force`` is false.
39
+ Returns ``(dest_path, existed_before)``.
36
40
  """
37
41
  if claude_dir is None:
38
42
  base = os.path.abspath(project) if project else os.path.expanduser("~")
39
43
  claude_dir = os.path.join(base, ".claude")
40
44
  dest = os.path.join(claude_dir, "skills", SKILL_NAME)
41
45
 
42
- if os.path.exists(dest) and not force:
43
- raise FileExistsError(dest)
44
-
45
46
  src = resources.files("morphdb") / "skill"
46
47
  if not src.is_dir():
47
48
  raise FileNotFoundError(
48
49
  "packaged skill not found (morphdb/skill missing from the install).")
49
50
 
50
- if os.path.exists(dest) and force:
51
+ existed = os.path.exists(dest)
52
+ if existed:
51
53
  import shutil
52
54
  shutil.rmtree(dest)
53
55
  _copy_tree(src, dest)
54
- return dest
56
+ return dest, existed
@@ -61,8 +61,9 @@ API).
61
61
 
62
62
  **Debug tip:** if the frontend can't reach the backend (connection refused, a
63
63
  `fetch` throws) and you're running locally, the server is probably down — run
64
- `morphdb status`, then `morphdb start` if stopped. That's the first thing to
65
- check when a working app suddenly stops loading or saving data.
64
+ `morphdb status`, then `morphdb start` if stopped, and `morphdb logs` (add `-f`
65
+ to follow) to see errors. That's the first thing to check when a working app
66
+ suddenly stops loading or saving data.
66
67
 
67
68
  ## Mental model
68
69
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: morphdb
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: A coding-agent-friendly, multi-tenant backend for vibe-coded websites. One process hosts many isolated apps; reshape each app's schema as fast as your agent iterates while the frontend keeps calling the same generic endpoints.
5
5
  Author: morphdb contributors
6
6
  License: MIT
@@ -30,21 +30,119 @@ Dynamic: license-file
30
30
  **A coding-agent-friendly, multi-tenant backend for vibe-coded websites.**
31
31
 
32
32
  Reshape the data model as fast as your coding agent iterates — the frontend
33
- keeps calling the same small set of generic, deterministic endpoints.
33
+ keeps calling the same small set of generic, deterministic endpoints. One
34
+ process hosts many isolated apps (one per site), zero dependencies, backed by
35
+ SQLite.
34
36
 
37
+ ## Install
38
+
39
+ ```bash
40
+ pip install morphdb
35
41
  ```
36
- you (the coding agent) the frontend you build
37
- ────────────────────── ──────────────────────
38
- reshape the schema freely │ calls fixed generic endpoints
39
- PUT /schema/{type} │ POST /objects/{type}
40
- GET /schema │ GET /objects/{type}?field=…
41
- DELETE /schema/{type} │ PATCH /objects/{type}/{guid}
42
- │ │
43
- └────────────── MorphDB ───────────┘
44
- (one process · many apps · SQLite)
45
- every call: X-App-Key: <app>
42
+
43
+ Manage the local server with the `morphdb` CLI:
44
+
45
+ ```bash
46
+ morphdb start # run in the background (default 127.0.0.1:8787)
47
+ morphdb status # running? where? how many apps?
48
+ morphdb stop # stop it
49
+ morphdb run # run in the foreground instead (blocking)
50
+ morphdb dashboard # read-only web view of every app + its tables
51
+ morphdb install-skill # install the MorphDB Claude Code skill (into ~/.claude)
52
+ ```
53
+
54
+ Data lives in `~/.morphdb/data.sqlite3` (change it with `--db PATH` or
55
+ `--db :memory:`; move the state dir with `$MORPHDB_HOME`). Server flags:
56
+ `--host`, `--port`, `--db`. From a source checkout with no install, the
57
+ foreground server is `python3 -m morphdb --port 8787 --db ./app.sqlite3`.
58
+
59
+ To upgrade later: `pip install -U morphdb`, then `morphdb stop && morphdb start`
60
+ to reload the new code (data in `~/.morphdb` is preserved across `0.1.x`).
61
+
62
+ **Pointing clients at a hosted MorphDB.** Set `MORPHDB_HOST` to a full URL (e.g.
63
+ `https://db.example.com`) and the schema CLI — plus any frontend that reads
64
+ `window.MORPHDB_HOST` — calls that hosted server (running this same code) instead
65
+ of localhost. It's a client-side setting that names a *backend*, not a database
66
+ connection string.
67
+
68
+ ## Use it
69
+
70
+ With the server running (`morphdb start`):
71
+
72
+ ```bash
73
+ BASE=http://127.0.0.1:8787
74
+
75
+ # 0. register an app; send its key as X-App-Key on every schema/object call
76
+ curl -X POST $BASE/app -d '{"key":"my-site"}'
77
+ H="X-App-Key: my-site"
78
+
79
+ # 1. define types + a relation
80
+ curl -X PUT $BASE/schema/user -H "$H" -d '{"fields":{"name":"string"}}'
81
+ curl -X PUT $BASE/schema/task -H "$H" -d '{
82
+ "fields": {"title":"string","done":"boolean","priority":"number"},
83
+ "relations": {"assignee":{"to":"user","cardinality":"many_to_one","inverse":"tasks"}}}'
84
+
85
+ # 2. create + read + query
86
+ U=$(curl -s -X POST $BASE/objects/user -H "$H" -d '{"name":"Ann"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["_guid"])')
87
+ curl -X POST $BASE/objects/task -H "$H" -d "{\"title\":\"buy milk\",\"priority\":2,\"assignee\":\"$U\"}"
88
+ curl -H "$H" "$BASE/objects/task?done=false&sort=priority&order=desc"
89
+ curl -H "$H" "$BASE/objects/user/$U" # → includes "tasks":[…]
90
+
91
+ # 3. morph the schema later — existing rows just gain the new field as null
92
+ curl -X PUT $BASE/schema/task -H "$H" -d '{"merge":true,"fields":{"due":"datetime"}}'
93
+ ```
94
+
95
+ See `examples/todo/index.html` for a complete single-file frontend backed by MorphDB.
96
+
97
+ ## Command-line interface
98
+
99
+ `morphdb` runs the server as a **background service** — `start` launches it
100
+ detached and hands your terminal straight back; `status` / `stop` find it again
101
+ via a pid file under the state dir.
102
+
103
+ | Command | What it does |
104
+ | --- | --- |
105
+ | `morphdb` or `morphdb start` | Start the server in the background (returns immediately). |
106
+ | `morphdb status` | Is it running? URL, pid, health, and app count. |
107
+ | `morphdb stop` | Stop the background server. |
108
+ | `morphdb logs` | Show the background server's log (`-n N` lines, `-f` to follow). |
109
+ | `morphdb run` | Run in the **foreground** (blocking) instead. |
110
+ | `morphdb dashboard` | Open a read-only web view of every app and its tables. |
111
+ | `morphdb install-skill` | Install the bundled Claude Code skill (below). |
112
+ | `morphdb --version` | Print the version. |
113
+
114
+ `start` / `run` accept `--host` (default `127.0.0.1`), `--port` (default `8787`),
115
+ and `--db` (a SQLite path or `:memory:`; default `~/.morphdb/data.sqlite3`).
116
+ `dashboard` accepts `--port` (default `8788`), `--db`, and `--no-open`. Service
117
+ state (pid, log, the default db) lives under `~/.morphdb` — relocate it with
118
+ `$MORPHDB_HOME`.
119
+
120
+ ```bash
121
+ morphdb start # background, default 127.0.0.1:8787
122
+ morphdb start --port 9000 --db ./my.sqlite3
123
+ morphdb status # -> running (pid …) at http://… [healthy]
124
+ morphdb dashboard # opens http://127.0.0.1:8788
125
+ morphdb stop
126
+ morphdb run # foreground instead (Ctrl-C to quit)
127
+ ```
128
+
129
+ ### Install the Claude Code skill
130
+
131
+ `install-skill` writes the bundled MorphDB skill into a Claude skills directory,
132
+ so a coding agent automatically reaches for MorphDB when building a data-backed
133
+ site:
134
+
135
+ ```bash
136
+ morphdb install-skill # -> ~/.claude/skills/morphdb (all projects)
137
+ morphdb install-skill --project # -> ./.claude/skills/morphdb (current project)
138
+ morphdb install-skill --project DIR # -> DIR/.claude/skills/morphdb
46
139
  ```
47
140
 
141
+ It installs the skill **bundled in the installed package** (not live from
142
+ GitHub) and is **idempotent** — re-running overwrites with the current version.
143
+ To get the newest skill, `pip install -U morphdb` first, then re-run. Restart
144
+ Claude Code afterward to pick it up.
145
+
48
146
  ## Why
49
147
 
50
148
  AI coding agents are great at building HTML/CSS/JS frontends but thrash hard on
@@ -58,6 +156,19 @@ invalidation). Adding, removing, or retyping a field is an O(1) metadata edit
58
156
  **no migration, no row rewrite, no downtime** — regardless of how much data
59
157
  exists. Meanwhile the frontend talks to generic endpoints that never change.
60
158
 
159
+ ```
160
+ you (the coding agent) the frontend you build
161
+ ────────────────────── ──────────────────────
162
+ reshape the schema freely │ calls fixed generic endpoints
163
+ PUT /schema/{type} │ POST /objects/{type}
164
+ GET /schema │ GET /objects/{type}?field=…
165
+ DELETE /schema/{type} │ PATCH /objects/{type}/{guid}
166
+ │ │
167
+ └────────────── MorphDB ───────────┘
168
+ (one process · many apps · SQLite)
169
+ every call: X-App-Key: <app>
170
+ ```
171
+
61
172
  ## The shape of it
62
173
 
63
174
  One MorphDB process hosts **many apps** (one per website), fully isolated from
@@ -126,63 +237,6 @@ curl -X PATCH $BASE/objects/user/<u> -d '{"tasks":["<t1>","<t2>"]}'
126
237
  > Scope: a localhost-scale developer tool. Not built for multi-tenant auth,
127
238
  > horizontal scale, or production durability guarantees.
128
239
 
129
- ## Install / run
130
-
131
- ```bash
132
- pip install morphdb
133
- ```
134
-
135
- Manage the local server with the `morphdb` CLI:
136
-
137
- ```bash
138
- morphdb start # run in the background (default 127.0.0.1:8787)
139
- morphdb status # running? where? how many apps?
140
- morphdb stop # stop it
141
- morphdb run # run in the foreground instead (blocking)
142
- morphdb dashboard # read-only web view of every app + its tables
143
- morphdb install-skill # install the MorphDB Claude Code skill (into ~/.claude)
144
- ```
145
-
146
- Data lives in `~/.morphdb/data.sqlite3` (change it with `--db PATH` or
147
- `--db :memory:`; move the state dir with `$MORPHDB_HOME`). Server flags:
148
- `--host`, `--port`, `--db`. From a source checkout with no install, the
149
- foreground server is `python3 -m morphdb --port 8787 --db ./app.sqlite3`.
150
-
151
- Then: `curl http://127.0.0.1:8787/help` for a live reference.
152
-
153
- **Pointing clients at a hosted MorphDB.** Set `MORPHDB_HOST` to a full URL (e.g.
154
- `https://db.example.com`) and the schema CLI — plus any frontend that reads
155
- `window.MORPHDB_HOST` — calls that hosted server (running this same code) instead
156
- of localhost. It's a client-side setting that names a *backend*, not a database
157
- connection string.
158
-
159
- ## Quickstart
160
-
161
- ```bash
162
- BASE=http://127.0.0.1:8787
163
-
164
- # 0. register an app; send its key as X-App-Key on every schema/object call
165
- curl -X POST $BASE/app -d '{"key":"my-site"}'
166
- H="X-App-Key: my-site"
167
-
168
- # 1. define types + a relation
169
- curl -X PUT $BASE/schema/user -H "$H" -d '{"fields":{"name":"string"}}'
170
- curl -X PUT $BASE/schema/task -H "$H" -d '{
171
- "fields": {"title":"string","done":"boolean","priority":"number"},
172
- "relations": {"assignee":{"to":"user","cardinality":"many_to_one","inverse":"tasks"}}}'
173
-
174
- # 2. create + read + query
175
- U=$(curl -s -X POST $BASE/objects/user -H "$H" -d '{"name":"Ann"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["_guid"])')
176
- curl -X POST $BASE/objects/task -H "$H" -d "{\"title\":\"buy milk\",\"priority\":2,\"assignee\":\"$U\"}"
177
- curl -H "$H" "$BASE/objects/task?done=false&sort=priority&order=desc"
178
- curl -H "$H" "$BASE/objects/user/$U" # → includes "tasks":[…]
179
-
180
- # 3. morph the schema later — existing rows just gain the new field as null
181
- curl -X PUT $BASE/schema/task -H "$H" -d '{"merge":true,"fields":{"due":"datetime"}}'
182
- ```
183
-
184
- See `examples/todo/index.html` for a complete single-file frontend backed by MorphDB.
185
-
186
240
  ## Data model
187
241
 
188
242
  | Concept | What it is |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "morphdb"
7
- version = "0.1.2"
7
+ version = "0.1.3"
8
8
  description = "A coding-agent-friendly, multi-tenant backend for vibe-coded websites. One process hosts many isolated apps; reshape each app's schema as fast as your agent iterates while the frontend keeps calling the same generic endpoints."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -13,6 +13,7 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
13
13
 
14
14
  from morphdb import apps, db, objects, schema # noqa: E402
15
15
  from morphdb.cli import dashboard, service # noqa: E402
16
+ from morphdb.cli import main as cli_main # noqa: E402
16
17
  from morphdb.cli import skill as skill_mod # noqa: E402
17
18
 
18
19
 
@@ -98,23 +99,54 @@ class TestDashboardGather(unittest.TestCase):
98
99
  class TestInstallSkill(unittest.TestCase):
99
100
  def test_install_copies_skill_files(self):
100
101
  d = tempfile.mkdtemp()
101
- dest = skill_mod.install_skill(claude_dir=d)
102
+ dest, existed = skill_mod.install_skill(claude_dir=d)
103
+ self.assertFalse(existed)
102
104
  self.assertTrue(os.path.isfile(os.path.join(dest, "SKILL.md")))
103
105
  self.assertTrue(os.path.isfile(
104
106
  os.path.join(dest, "scripts", "morphdb_schema.py")))
105
- # name + location
106
- self.assertEqual(os.path.basename(dest), "morphdb")
107
107
  self.assertEqual(dest, os.path.join(d, "skills", "morphdb"))
108
108
 
109
- def test_refuses_without_force_then_overwrites(self):
109
+ def test_reinstall_is_idempotent(self):
110
110
  d = tempfile.mkdtemp()
111
111
  skill_mod.install_skill(claude_dir=d)
112
- with self.assertRaises(FileExistsError):
113
- skill_mod.install_skill(claude_dir=d)
114
- # force overwrites cleanly
115
- dest = skill_mod.install_skill(claude_dir=d, force=True)
112
+ dest, existed = skill_mod.install_skill(claude_dir=d) # re-run overwrites
113
+ self.assertTrue(existed)
116
114
  self.assertTrue(os.path.isfile(os.path.join(dest, "SKILL.md")))
117
115
 
118
116
 
117
+ class TestLogs(unittest.TestCase):
118
+ def setUp(self):
119
+ self._old = os.environ.get("MORPHDB_HOME")
120
+ self.tmp = tempfile.mkdtemp()
121
+ os.environ["MORPHDB_HOME"] = self.tmp
122
+
123
+ def tearDown(self):
124
+ if self._old is None:
125
+ os.environ.pop("MORPHDB_HOME", None)
126
+ else:
127
+ os.environ["MORPHDB_HOME"] = self._old
128
+
129
+ def _run(self, argv):
130
+ import contextlib
131
+ import io
132
+ buf = io.StringIO()
133
+ with contextlib.redirect_stdout(buf):
134
+ rc = cli_main.main(argv)
135
+ return rc, buf.getvalue()
136
+
137
+ def test_missing_log(self):
138
+ rc, out = self._run(["logs"])
139
+ self.assertEqual(rc, 1)
140
+ self.assertIn("No log yet", out)
141
+
142
+ def test_shows_tail(self):
143
+ with open(service.log_file(), "w") as f:
144
+ f.write("line1\nline2\nline3\n")
145
+ rc, out = self._run(["logs", "-n", "2"])
146
+ self.assertEqual(rc, 0)
147
+ self.assertIn("line3", out)
148
+ self.assertNotIn("line1", out)
149
+
150
+
119
151
  if __name__ == "__main__":
120
152
  unittest.main(verbosity=2)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes