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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '092917d22f25afea72145540f4dc8ce32e5a4f6c65889564f54e258e52eab7df'
4
- data.tar.gz: 163d891ea50b7c651740f499ffe7ba5191aba0c2c327afbf058a1bf1fc019942
3
+ metadata.gz: c72c516279aeb0df734da1a72dd89fd033ebbed2e891444b71015e04fab73e75
4
+ data.tar.gz: a1075b8990c6c1749eb03f5042c1e5bd590456307cf9b3e1a9de9c33dcc0a862
5
5
  SHA512:
6
- metadata.gz: 1fa336c3387f503c7ac846fea1f7d1de92a9633ea0104d4c0882d48951bf6abbbb1366e0a9028abac1a138f048fe2b90c018228c7b9a19725eb251c835f43b6c
7
- data.tar.gz: 2f99647d031c7f0004d4b0264c51a616f8da03bcb72a68f4e12c0e483b5865ee3d826786f7d519bb4d7aac2a8ec9d4f9a1092220ce376a226e1436e9c1e5affc
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
- Reference Ruby implementation of the **textus/1** protocol — a storage convention and JSON wire protocol for agent-readable project memory: addressable dotted keys, schema-validated entries (markdown, JSON, YAML, or text per entry), role-gated writes, declarative compute, and copy-based publish targets.
3
+ [![CI](https://github.com/patrick204nqh/textus/actions/workflows/ci.yml/badge.svg)](https://github.com/patrick204nqh/textus/actions/workflows/ci.yml)
4
+ [![Gem Version](https://img.shields.io/gem/v/textus.svg)](https://rubygems.org/gems/textus)
5
+ [![Ruby](https://img.shields.io/badge/ruby-%E2%89%A53.3-CC342D.svg)](https://www.ruby-lang.org/)
6
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
4
7
 
5
- See [`SPEC.md`](SPEC.md) for the protocol. Implementation notes live in [`docs/`](docs/).
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; the gem version is irrelevant to the wire format.
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 # when published
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
- bundle exec exe/textus init
37
+ textus init
35
38
  ```
36
39
 
37
- This scaffolds `.textus/` with a starter manifest, the five zone directories, baseline schemas, and an empty audit log. The resulting layout:
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
- role
44
- schemas/
45
- templates/
46
- extensions/
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/ # human-only
49
- working/ # human, ai, script
50
- intake/ # script (declared external inputs)
51
- pending/ # ai (proposals awaiting accept)
52
- derived/ # build only (computed outputs)
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
- A minimal `manifest.yaml`:
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","relationship":"peer","org":"acme"},"body":"hi\n"}' \
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 emit the envelope defined in SPEC §8. Write verbs require `--as=<role>` (subject to role-resolution order, §5.1).
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 verbs (no role required):**
88
+ **Read:**
98
89
 
99
90
  | Verb | Purpose |
100
91
  |---|---|
101
- | `list [--prefix=K] [--zone=Z] [--stale]` | Enumerate keys, optionally filtered |
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` | Return the full envelope |
104
- | `schema K` | Return the schema bound to an entry |
105
- | `stale [--prefix=K] [--zone=Z] [--strict]` | List stale derived/intake entries |
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 (incl. `maintained_by`) |
109
- | `extensions list [--kind=K]` | Enumerate registered fetchers, reducers, and declared hooks |
100
+ | `validate-all` | Validate every entry against its schema |
101
+ | `extensions list [--kind=K]` | Registered fetchers, reducers, declared hooks |
110
102
 
111
- **Write verbs (role-gated per zone):**
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/` tree |
126
- | `schema-init NAME` | Write a stub schema |
127
- | `schema-diff NAME` | Compare on-disk schema against entries claiming it |
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, immutable principles |
135
- | `working` | `[human, ai, script]` | Active project state — notes, decisions, network |
136
- | `intake` | `[script]` | Declared external inputs (calendar, feeds, scraped pages) |
137
- | `pending` | `[ai]` | AI proposals awaiting `textus accept` |
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
- The effective role for any CLI call is resolved in order: `--as` flag, then `TEXTUS_ROLE` env, then `.textus/role`, then default `human`. Mismatches return `write_forbidden`. Every write records the resolved role in `.textus/audit.log`.
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
- Derived entries are not authored by hand. Each declares a `projection:` block (select prefixes, pluck fields, optional sort/limit/transform) and optionally a Mustache template under `.textus/templates/`. textus implements a deliberately restricted Mustache subset (variables, sections, inverted sections, comments — no partials, no lambdas, no HTML escaping). Results are bounded at 1000 rows; template recursion at depth 8.
142
+ ## Compute and publish
145
143
 
146
- Derived entries may declare `format:` to be `markdown` (default), `json`, `yaml`, or `text`. The in-store file is the consumer-shaped artifact `cat .textus/zones/derived/marketplace.json` returns valid JSON without going through textus. `publish_to:` then performs a byte-for-byte file copy of that artifact to each destination, alongside a `.textus-managed.json` sentinel. See SPEC §5.2, §5.3, and §5.12.
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
- ## Extension points
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
- Three DSL verbs:
148
+ ## Extensions
151
149
 
152
- - **`Textus.fetcher(:name) do |config:, store:|`** pulls data into an intake entry. Returns one of `{ frontmatter:, body: }`, `{ content: }` (for `format: json|yaml` entries), or `{ body: }` (raw bytes); the store normalizes all three. Configured via `source.fetcher` in the manifest. Five built-ins ship out of the box: `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`.
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
- Extension files live in `.textus/extensions/*.rb` (one per registration, by convention). Each Store instance gets its own registry; no global state.
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
- See SPEC.md §5.11 for the full contract.
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
- - [`examples/claude-plugin/`](examples/claude-plugin/) — full tour: fetcher, reducer, lifecycle events, schema ownership, and a `derived.claude.root` entry published to `CLAUDE.md`.
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
- Runs the full suite, including conformance fixtures A–I from SPEC §12.
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
- Git hooks via [Lefthook](https://github.com/evilmartians/lefthook):
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)
@@ -1,4 +1,4 @@
1
1
  module Textus
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  PROTOCOL = "textus/1"
4
4
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: textus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrick