textus 0.2.0 → 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/README.md +77 -96
- data/SPEC.md +10 -0
- data/lib/textus/cli.rb +5 -1
- data/lib/textus/store.rb +11 -1
- data/lib/textus/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c72c516279aeb0df734da1a72dd89fd033ebbed2e891444b71015e04fab73e75
|
|
4
|
+
data.tar.gz: a1075b8990c6c1749eb03f5042c1e5bd590456307cf9b3e1a9de9c33dcc0a862
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2218efa63e1ae6f246af3341bbb27e713a71a381a4aa84d15d46e687b8585c3bb5f992d75456c6112c960fec8a286c3cb7e6e672af04db0d2657811b45627ffb
|
|
7
|
+
data.tar.gz: 98ec77bf76fe14c850470e9dee3aebe55bba44ae6d4aa6303951eab2d697f2eed4d8d2d44278615e7e80873704d41dcd46109f244414ca2d16c7a4c4a7c12e2a
|
data/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,21 @@ is additive within a major; a new major would change the wire string.
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.3.0] — 2026-05-20 — Configurable store root
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- `--root <path>` CLI flag and `TEXTUS_ROOT` environment variable for store
|
|
18
|
+
discovery. `Textus::Store.discover` now accepts an optional `root:` kwarg.
|
|
19
|
+
Unblocks embedding a textus store at non-default paths (e.g. nested under a
|
|
20
|
+
plugin directory like `plugins/<name>/.textus/`) where walking up from cwd
|
|
21
|
+
to find `.textus/` is undesirable or ambiguous.
|
|
22
|
+
|
|
23
|
+
### Documentation
|
|
24
|
+
|
|
25
|
+
- SPEC.md §3.1 documents the new store-location precedence:
|
|
26
|
+
1. `--root` / `root:` kwarg, 2. `TEXTUS_ROOT`, 3. cwd walk.
|
|
27
|
+
|
|
13
28
|
## [0.2.0] — 2026-05-20 — Storage rewrite, agent surface, extension DSL (BREAKING)
|
|
14
29
|
|
|
15
30
|
This release reshapes textus from a markdown-only frontmatter store into a
|
data/README.md
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
# textus
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/patrick204nqh/textus/actions/workflows/ci.yml)
|
|
4
|
+
[](https://rubygems.org/gems/textus)
|
|
5
|
+
[](https://www.ruby-lang.org/)
|
|
6
|
+
[](LICENSE)
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
A context store for codebases that humans and AI agents both have to read and write. Dotted keys, schema-validated entries, role-gated writes, byte-copy publish, an audit log of every change. Built so an agent landing in your repo can run one command (`textus intro`) and know what to read, what to write, and what's off-limits.
|
|
9
|
+
|
|
10
|
+
Reference implementation in Ruby. Wire format `textus/1`. SPEC: [`SPEC.md`](SPEC.md). Implementation notes: [`docs/`](docs/).
|
|
6
11
|
|
|
7
12
|
## Versioning
|
|
8
13
|
|
|
@@ -11,12 +16,12 @@ Two versions, deliberately independent:
|
|
|
11
16
|
- **Protocol wire string:** `textus/1`. Stable; breaking changes require `textus/2`.
|
|
12
17
|
- **Gem version:** semver, currently `0.2.0`. Gem `0.x.y` and `1.x` both speak `textus/1`.
|
|
13
18
|
|
|
14
|
-
Envelope payloads carry the `protocol` field
|
|
19
|
+
Envelope payloads carry the `protocol` field. The gem version is irrelevant to the wire format.
|
|
15
20
|
|
|
16
21
|
## Install
|
|
17
22
|
|
|
18
23
|
```sh
|
|
19
|
-
gem install textus
|
|
24
|
+
gem install textus
|
|
20
25
|
```
|
|
21
26
|
|
|
22
27
|
Or from this repo:
|
|
@@ -28,141 +33,131 @@ bundle exec exe/textus --help
|
|
|
28
33
|
|
|
29
34
|
## Quick start
|
|
30
35
|
|
|
31
|
-
Bootstrap a fresh tree:
|
|
32
|
-
|
|
33
36
|
```sh
|
|
34
|
-
|
|
37
|
+
textus init
|
|
35
38
|
```
|
|
36
39
|
|
|
37
|
-
|
|
40
|
+
You get `.textus/` with all five zone directories, baseline schemas, an empty audit log, and a starter manifest:
|
|
38
41
|
|
|
39
42
|
```
|
|
40
43
|
.textus/
|
|
41
|
-
manifest.yaml
|
|
42
|
-
audit.log
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
manifest.yaml # zone declarations + key-to-path mapping
|
|
45
|
+
audit.log # append-only NDJSON, every write
|
|
46
|
+
schemas/ # YAML field shapes per entry family
|
|
47
|
+
templates/ # mustache templates for derived entries
|
|
48
|
+
extensions/ # one .rb per fetcher / reducer / hook
|
|
49
|
+
sentinels/ # publish bookkeeping
|
|
47
50
|
zones/
|
|
48
|
-
canon/
|
|
49
|
-
working/
|
|
50
|
-
intake/
|
|
51
|
-
pending/
|
|
52
|
-
derived/
|
|
51
|
+
canon/ # human-only — identity, voice, decisions
|
|
52
|
+
working/ # human / ai / script — day-to-day catalog
|
|
53
|
+
intake/ # script — declared external inputs (fetchers)
|
|
54
|
+
pending/ # ai + human — proposals awaiting accept
|
|
55
|
+
derived/ # build only — computed outputs
|
|
53
56
|
```
|
|
54
57
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
```yaml
|
|
58
|
-
version: textus/1
|
|
59
|
-
|
|
60
|
-
zones:
|
|
61
|
-
- { name: canon, writable_by: [human] }
|
|
62
|
-
- { name: working, writable_by: [human, ai, script] }
|
|
63
|
-
- { name: intake, writable_by: [script] }
|
|
64
|
-
- { name: pending, writable_by: [ai] }
|
|
65
|
-
- { name: derived, writable_by: [build] }
|
|
66
|
-
|
|
67
|
-
entries:
|
|
68
|
-
- key: canon.identity
|
|
69
|
-
path: canon/identity.md
|
|
70
|
-
zone: canon
|
|
71
|
-
schema: identity
|
|
72
|
-
|
|
73
|
-
- key: working.network.org
|
|
74
|
-
path: working/network/org
|
|
75
|
-
zone: working
|
|
76
|
-
schema: person
|
|
77
|
-
owner: textus:network
|
|
78
|
-
nested: true
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
Manifest `path:` fields are relative to `.textus/zones/` — implementations prepend `zones/` when resolving. So `working.network.org.jane` lives at `.textus/zones/working/network/org/jane.md`.
|
|
58
|
+
Manifest `path:` fields are relative to `.textus/zones/`. So `working.network.org.jane` lives at `.textus/zones/working/network/org/jane.md`.
|
|
82
59
|
|
|
83
60
|
Read and write:
|
|
84
61
|
|
|
85
62
|
```sh
|
|
86
63
|
textus get working.network.org.jane --format=json
|
|
87
64
|
textus list --zone=working --format=json
|
|
88
|
-
echo '{"frontmatter":{"name":"bob","
|
|
65
|
+
echo '{"frontmatter":{"name":"bob","org":"acme"},"body":"hi\n"}' \
|
|
89
66
|
| textus put working.network.org.bob --as=human --stdin --format=json
|
|
90
67
|
textus stale --zone=derived --format=json
|
|
91
68
|
```
|
|
92
69
|
|
|
70
|
+
For the full shape — Claude plugin with agents, skills, commands, pending walkthrough, intake fetcher — see [`examples/claude-plugin/`](examples/claude-plugin/).
|
|
71
|
+
|
|
72
|
+
## What 0.2 ships
|
|
73
|
+
|
|
74
|
+
- **Per-entry formats.** `format: markdown | json | yaml | text` on a manifest entry. `cat .textus/zones/derived/marketplace.json | jq .` works without going through textus — the in-store file *is* the consumer-shaped artifact. Structured outputs carry `_meta` at the top level (`generated_at`, `from`, `template`, `reducer`).
|
|
75
|
+
- **Per-leaf publishing.** Nested entries declare `publish_each: "skills/{basename}/SKILL.md"`. Every leaf byte-copies to its consumer location on `textus build`. No more hand-mirrored `agents/` / `skills/` / `commands/` directories.
|
|
76
|
+
- **Stable identity (`uid:`).** 16-char hex, auto-minted on first `put`, preserved across writes and moves. `textus mv old.key new.key` renames in place — uid survives, audit row records `from_key`, `to_key`, `uid`. Reorganising a tree no longer breaks references.
|
|
77
|
+
- **Strict key grammar.** `/^[a-z0-9][a-z0-9-]*$/`, max 8 segments × 64 chars. `textus migrate-keys --dry-run|--write` rewrites existing stores with illegal segments deterministically.
|
|
78
|
+
- **`textus intro`.** One-shot store orientation: zones with writers + purposes, entry families with schemas and publish targets, loaded extensions, write flows per role, the full CLI verb table. The boot signal for any agent — one tool call and it knows your store.
|
|
79
|
+
- **`textus doctor`.** Health check across 8 categories: missing schemas/templates, broken extensions, illegal nested keys, sentinel drift, audit log readability. Returns `ok: true` only when nothing is wrong; warnings and info don't flip the bit.
|
|
80
|
+
- **Actionable hints on every error.** `UnknownKey` carries ranked "did you mean" suggestions. `WriteForbidden` names the role that *would* be allowed. `BadFrontmatter` tells you exactly what to rename. Printed to stderr alongside the JSON envelope on stdout.
|
|
81
|
+
|
|
82
|
+
Symlink-mode publish was removed; publish is `FileUtils.cp` + sentinel. Sentinels for published files live under `.textus/sentinels/<target_rel>.textus-managed.json` so consumer directories stay clean. Legacy sibling sentinels auto-migrate on next publish.
|
|
83
|
+
|
|
93
84
|
## CLI verbs
|
|
94
85
|
|
|
95
|
-
All verbs accept `--format=json` and
|
|
86
|
+
All verbs accept `--format=json` and return the envelope defined in SPEC §8. Write verbs require `--as=<role>` (role resolution: `--as` → `TEXTUS_ROLE` env → `.textus/role` file → default `human`).
|
|
96
87
|
|
|
97
|
-
**Read
|
|
88
|
+
**Read:**
|
|
98
89
|
|
|
99
90
|
| Verb | Purpose |
|
|
100
91
|
|---|---|
|
|
101
|
-
| `
|
|
92
|
+
| `intro` | Store orientation: zones, entries, extensions, write flows, CLI map |
|
|
93
|
+
| `list [--prefix=K] [--zone=Z]` | Enumerate keys |
|
|
102
94
|
| `where K` | Resolve a key to its filesystem path |
|
|
103
|
-
| `get K` |
|
|
104
|
-
| `schema K` |
|
|
105
|
-
| `stale [--prefix=K] [--zone=Z]
|
|
106
|
-
| `deps K` / `rdeps K` | Forward/reverse projection dependencies |
|
|
95
|
+
| `get K` | Full envelope (frontmatter, body, uid, etag, format) |
|
|
96
|
+
| `schema K` | Schema bound to an entry |
|
|
97
|
+
| `stale [--prefix=K] [--zone=Z]` | List stale derived/intake entries |
|
|
98
|
+
| `deps K` / `rdeps K` | Forward / reverse projection dependencies |
|
|
107
99
|
| `published` | List `publish_to:` targets and their backing keys |
|
|
108
|
-
| `validate-all` | Validate every entry against its schema
|
|
109
|
-
| `extensions list [--kind=K]` |
|
|
100
|
+
| `validate-all` | Validate every entry against its schema |
|
|
101
|
+
| `extensions list [--kind=K]` | Registered fetchers, reducers, declared hooks |
|
|
110
102
|
|
|
111
|
-
**Write
|
|
103
|
+
**Write:**
|
|
112
104
|
|
|
113
105
|
| Verb | Role |
|
|
114
106
|
|---|---|
|
|
115
107
|
| `put K --stdin --as=R [--fetcher=NAME]` | per zone |
|
|
116
108
|
| `delete K --if-etag=E --as=R` | per zone |
|
|
117
109
|
| `refresh K --as=script` | per zone (typically `script`) |
|
|
110
|
+
| `mv old new --as=R [--dry-run]` | per zone (same-zone moves; uid preserved) |
|
|
118
111
|
| `build [--prefix=K] [--dry-run]` | `build` |
|
|
119
112
|
| `accept K --as=human` | `human` only |
|
|
120
113
|
|
|
114
|
+
**Health & maintenance:**
|
|
115
|
+
|
|
116
|
+
| Verb | Purpose |
|
|
117
|
+
|---|---|
|
|
118
|
+
| `doctor` | 8 health checks; `ok: true` when clean |
|
|
119
|
+
| `migrate-keys [--dry-run]` | Rename files whose basenames violate the strict key grammar |
|
|
120
|
+
|
|
121
121
|
**Scaffolding (human-only):**
|
|
122
122
|
|
|
123
123
|
| Verb | Purpose |
|
|
124
124
|
|---|---|
|
|
125
|
-
| `init` | Scaffold a fresh `.textus/`
|
|
126
|
-
| `schema-init NAME` |
|
|
127
|
-
| `schema-diff NAME` | Compare
|
|
125
|
+
| `init` | Scaffold a fresh `.textus/` |
|
|
126
|
+
| `schema-init NAME` | Stub a schema |
|
|
127
|
+
| `schema-diff NAME` | Compare a schema against entries that claim it |
|
|
128
128
|
| `schema-migrate NAME [--rename=OLD:NEW]` | Rewrite frontmatter keys across affected entries |
|
|
129
129
|
|
|
130
130
|
## Zones and roles
|
|
131
131
|
|
|
132
132
|
| Zone | `writable_by` | Purpose |
|
|
133
133
|
|---|---|---|
|
|
134
|
-
| `canon` | `[human]` | Identity, voice,
|
|
135
|
-
| `working` | `[human, ai, script]` | Active project state
|
|
136
|
-
| `intake` | `[script]` | Declared external inputs (
|
|
137
|
-
| `pending` | `[ai]` | AI proposals
|
|
134
|
+
| `canon` | `[human]` | Identity, voice, decisions — slow-changing |
|
|
135
|
+
| `working` | `[human, ai, script]` | Active project state |
|
|
136
|
+
| `intake` | `[script]` | Declared external inputs (fetchers) |
|
|
137
|
+
| `pending` | `[ai, human]` | AI proposals; humans run `textus accept` to apply |
|
|
138
138
|
| `derived` | `[build]` | Computed outputs from `textus build` |
|
|
139
139
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
## Compute layer
|
|
140
|
+
Mismatches return `write_forbidden` with a hint naming the role that *would* be allowed. Every write records the resolved role in `.textus/audit.log`.
|
|
143
141
|
|
|
144
|
-
|
|
142
|
+
## Compute and publish
|
|
145
143
|
|
|
146
|
-
Derived entries
|
|
144
|
+
Derived entries declare a `projection:` (`select`, `pluck`, `sort_by`, `limit`, optional `reducer`) and either a template under `.textus/templates/` (markdown/text) or a templateless path that lets a reducer shape the output directly (json/yaml). Projections cap at 1000 rows; the vendored Mustache subset caps at depth 8. No partials, no lambdas, no HTML escaping.
|
|
147
145
|
|
|
148
|
-
|
|
146
|
+
`publish_to: [path]` byte-copies a single derived file to one target. `publish_each: "template/{basename}.md"` on a nested entry byte-copies every leaf to its templated target — substitutes `{leaf}`, `{basename}`, `{key}`, `{ext}`. Sentinels for every published file live under `.textus/sentinels/`. See SPEC §5.2, §5.3, §5.12.
|
|
149
147
|
|
|
150
|
-
|
|
148
|
+
## Extensions
|
|
151
149
|
|
|
152
|
-
|
|
153
|
-
- **`Textus.reducer(:name) do |rows:, config:|`** — shapes rows in a derived projection. Pure function. Configured via `projection.reducer`.
|
|
154
|
-
- **`Textus.hook(:event, :name) do |kwargs|`** — reacts to a lifecycle event. Five events: `:put`, `:delete`, `:refresh`, `:build`, `:accept`.
|
|
150
|
+
Three DSL verbs, registered in `.textus/extensions/*.rb`. Each `Store` gets its own registry — no global state.
|
|
155
151
|
|
|
156
|
-
|
|
152
|
+
- **`Textus.fetcher(:name) do |config:, store:|`** — pulls data into an intake entry. Returns `{frontmatter:, body:}`, `{content:}` (for json/yaml entries), or `{body:}` (raw). The store normalizes all three shapes. Configured via `source.fetcher` in the manifest. Five built-ins ship: `json`, `csv`, `markdown-links`, `ical-events`, `rss`.
|
|
153
|
+
- **`Textus.reducer(:name) do |rows:, config:|`** — shapes rows in a derived projection. Pure function. Configured via `projection.reducer`. May return an Array (templated builds) or a Hash (templateless json/yaml).
|
|
154
|
+
- **`Textus.hook(:event, :name) do |kwargs|`** — fires on `:put`, `:delete`, `:refresh`, `:build`, or `:accept`. In-process; 2 s timeout per hook; failures land in the audit log as `event_error` rows.
|
|
157
155
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
Schema fields may also declare `maintained_by:` and a top-level `evolution:` block (`added_in`, `deprecated_at`, `migrate_from`). SPEC §5.8.
|
|
156
|
+
Schemas (`.textus/schemas/<name>.yaml`) declare field shapes, per-field `maintained_by:` ownership, and an `evolution:` block (`added_in`, `deprecated_at`, `migrate_from`). Full contract in SPEC §5.8 and §5.11.
|
|
161
157
|
|
|
162
158
|
## Examples
|
|
163
159
|
|
|
164
|
-
|
|
165
|
-
- [`examples/mcp-server/`](examples/mcp-server/) — 50-line MCP server wrapping `textus get/put` as tools.
|
|
160
|
+
[`examples/claude-plugin/`](examples/claude-plugin/) — a Claude Code plugin (`voice-tools`) whose entire content surface — agents, skills, commands, `CLAUDE.md`, `plugin.json`, `marketplace.json` — is textus-managed. Demonstrates per-entry formats, `publish_each`, intake fetchers, in-process reducers and hooks, the AI-propose / human-accept loop, and the `inject_intro:` flag that puts an orientation preamble at the top of `CLAUDE.md`.
|
|
166
161
|
|
|
167
162
|
## Tests
|
|
168
163
|
|
|
@@ -170,7 +165,7 @@ Schema fields may also declare `maintained_by:` and a top-level `evolution:` blo
|
|
|
170
165
|
bundle exec rspec
|
|
171
166
|
```
|
|
172
167
|
|
|
173
|
-
|
|
168
|
+
240 examples; includes conformance fixtures A–I from SPEC §12.
|
|
174
169
|
|
|
175
170
|
## Code quality
|
|
176
171
|
|
|
@@ -179,21 +174,7 @@ bundle exec rubocop # lint
|
|
|
179
174
|
bundle exec rubocop -A # lint + autocorrect
|
|
180
175
|
```
|
|
181
176
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
```sh
|
|
185
|
-
brew bundle install # installs lefthook (see Brewfile)
|
|
186
|
-
lefthook install # writes .git/hooks/{pre-commit,pre-push}
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
Git hooks (defined in `lefthook.yml`):
|
|
190
|
-
- `pre-commit` — runs `rubocop` on staged Ruby files.
|
|
191
|
-
- `pre-push` — runs the full `rspec` suite and `rubocop` over the tree.
|
|
192
|
-
|
|
193
|
-
Bypass with `LEFTHOOK=0 git commit ...` when needed.
|
|
194
|
-
|
|
195
|
-
CI runs `rspec` (Ruby 3.3 / 3.4) and `rubocop` via GitHub Actions
|
|
196
|
-
([`.github/workflows/ci.yml`](.github/workflows/ci.yml)).
|
|
177
|
+
Lefthook hooks (`brew bundle install` then `lefthook install`) run rubocop on `pre-commit` and `rspec + rubocop` on `pre-push`. Bypass with `LEFTHOOK=0 git commit ...` when needed. CI runs `rspec` (Ruby 3.3 / 3.4) and `rubocop` via GitHub Actions.
|
|
197
178
|
|
|
198
179
|
## License
|
|
199
180
|
|
data/SPEC.md
CHANGED
|
@@ -75,6 +75,16 @@ Zone directories under `zones/` are conventional; their write semantics are decl
|
|
|
75
75
|
|
|
76
76
|
`.textus/audit.log` is an append-only NDJSON file written under a file lock by every successful `put`, `delete`, `accept`, and `build`. `.textus/role` (one line containing a role name) is optional and participates in the role-resolution order (§5).
|
|
77
77
|
|
|
78
|
+
### 3.1 Store location precedence (v0.3)
|
|
79
|
+
|
|
80
|
+
Implementations MUST resolve the store root in this order; the first match wins:
|
|
81
|
+
|
|
82
|
+
1. `--root <path>` flag passed to the CLI (or `root:` kwarg to `Store.discover`).
|
|
83
|
+
2. `TEXTUS_ROOT` environment variable.
|
|
84
|
+
3. Walk up from cwd looking for a `.textus/` directory containing `manifest.yaml`.
|
|
85
|
+
|
|
86
|
+
When (1) or (2) names a path that has no `manifest.yaml`, the CLI exits with `io_error` and a message naming the resolved absolute path. When (3) reaches the filesystem root without finding a store, the CLI exits with `io_error` naming the search start point.
|
|
87
|
+
|
|
78
88
|
## 4. Manifest
|
|
79
89
|
|
|
80
90
|
The manifest declares: (a) which zones exist and which roles may write to each, (b) the key-to-subtree mapping, (c) the schema applied to entries in each subtree, and (d) the owner string recorded in writes.
|
data/lib/textus/cli.rb
CHANGED
|
@@ -16,9 +16,13 @@ module Textus
|
|
|
16
16
|
@stdout = stdout
|
|
17
17
|
@stderr = stderr
|
|
18
18
|
@cwd = cwd
|
|
19
|
+
@root_arg = nil
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
def run(argv) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/AbcSize
|
|
23
|
+
OptionParser.new do |o|
|
|
24
|
+
o.on("--root=PATH") { |v| @root_arg = v }
|
|
25
|
+
end.order!(argv)
|
|
22
26
|
verb = argv.shift
|
|
23
27
|
raise UsageError.new("missing verb") if verb.nil?
|
|
24
28
|
|
|
@@ -60,7 +64,7 @@ module Textus
|
|
|
60
64
|
private
|
|
61
65
|
|
|
62
66
|
def store
|
|
63
|
-
@store ||= Store.discover(@cwd)
|
|
67
|
+
@store ||= Store.discover(@cwd, root: @root_arg)
|
|
64
68
|
end
|
|
65
69
|
|
|
66
70
|
def parse_format!(argv)
|
data/lib/textus/store.rb
CHANGED
|
@@ -17,7 +17,10 @@ module Textus
|
|
|
17
17
|
SecureRandom.hex(8)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
def self.discover(start_dir = Dir.pwd)
|
|
20
|
+
def self.discover(start_dir = Dir.pwd, root: nil)
|
|
21
|
+
explicit = root || ENV.fetch("TEXTUS_ROOT", nil)
|
|
22
|
+
return discover_explicit(explicit) if explicit
|
|
23
|
+
|
|
21
24
|
dir = File.expand_path(start_dir)
|
|
22
25
|
loop do
|
|
23
26
|
candidate = File.join(dir, ".textus")
|
|
@@ -31,6 +34,13 @@ module Textus
|
|
|
31
34
|
raise IoError.new("no .textus directory found from #{start_dir}")
|
|
32
35
|
end
|
|
33
36
|
|
|
37
|
+
private_class_method def self.discover_explicit(root_arg)
|
|
38
|
+
abs = File.expand_path(root_arg)
|
|
39
|
+
raise IoError.new("no textus store at #{abs}") unless File.directory?(abs) && File.exist?(File.join(abs, "manifest.yaml"))
|
|
40
|
+
|
|
41
|
+
new(abs)
|
|
42
|
+
end
|
|
43
|
+
|
|
34
44
|
def initialize(root)
|
|
35
45
|
@root = File.expand_path(root)
|
|
36
46
|
@manifest = Manifest.load(@root)
|
data/lib/textus/version.rb
CHANGED