@alessandroraffa/tangyr 0.12.0 → 0.13.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.
Files changed (3) hide show
  1. package/README.md +258 -1
  2. package/dist/index.js +2137 -924
  3. package/package.json +4 -3
package/README.md CHANGED
@@ -61,7 +61,264 @@ tangyr <command>
61
61
  | `tangyr auth status` | Show current credential validation state |
62
62
  | `tangyr auth logout` | Remove the stored credential |
63
63
 
64
- Resolved environment-first (`TANGYR_ACCESS_CLIENT_ID`/`TANGYR_ACCESS_CLIENT_SECRET`), then from the stored file, then interactively. A full quickstart (configuring a remote kit origin and installing against it) lands once remote kit resolution ships.
64
+ Resolved environment-first (`TANGYR_ACCESS_CLIENT_ID`/`TANGYR_ACCESS_CLIENT_SECRET`), then from the stored file, then interactively.
65
+
66
+ **Downgrading.** The credential file (`~/.tangyr/credentials.json`) uses the client ID/secret pair format. To revert to a CLI predating this format, delete that file and run `tangyr auth login` again on the older version — the older CLI's single-key reader rejects, rather than misreads, the newer pair-format file, so no silent corruption occurs in either direction.
67
+
68
+ ### Remote kit source
69
+
70
+ A kit can be installed from a signed remote origin instead of a local
71
+ directory. The subsections below cover configuration, the cache layout,
72
+ offline/refresh/freshness behavior, and the maintenance commands; see
73
+ [Exit codes](#exit-codes) below for the codes a remote
74
+ `install`/`sync`/`kit update` can reach.
75
+
76
+ **In brief.** Once installed, `install`/`sync` resolve a warm cache entry
77
+ with no network call for 24 hours after the last successful
78
+ authorization check (`--refresh` forces a re-fetch and re-verify;
79
+ `--offline` forces cache-only resolution and fails if nothing is
80
+ cached). `tangyr kit update` refreshes the cache without running the
81
+ install/sync pipeline; `tangyr cache clear` removes cached kits. See
82
+ [Offline, refresh, and authorization freshness](#offline-refresh-and-authorization-freshness)
83
+ and [Maintenance commands](#maintenance-commands) below for the full
84
+ behavior, and [Authentication](#authentication) above for the
85
+ credential-store downgrade path.
86
+
87
+ #### Quickstart
88
+
89
+ ```bash
90
+ # 1. Authenticate — stores a validated credential pair.
91
+ export TANGYR_ACCESS_CLIENT_ID=... TANGYR_ACCESS_CLIENT_SECRET=...
92
+ tangyr auth login
93
+
94
+ # 2. Point the config at the remote origin (tangyr.config.yaml).
95
+ # source:
96
+ # type: remote
97
+ # url: https://kits.example.com
98
+
99
+ # 3. Install by name and explicit version — no "latest".
100
+ tangyr install --kit acme-kit@1.4.0
101
+ ```
102
+
103
+ `sync --kit acme-kit@1.4.0` follows the same form. Both commands fetch,
104
+ verify the publisher signature and per-file content hashes, materialize
105
+ into the local cache, and hand the result to the unchanged local install
106
+ pipeline.
107
+
108
+ #### Remote source configuration
109
+
110
+ Set the source scheme in `tangyr.config.yaml`. Both keys are optional and additive — a config that omits them behaves exactly as before.
111
+
112
+ ```yaml
113
+ source:
114
+ type: remote # "local" (default) or "remote"
115
+ url: https://kits.example.com # required when type is remote
116
+ ```
117
+
118
+ `type` defaults to `local` when absent. When it is `remote`, the local `source.*` path keys are ignored at resolution; they are retained rather than stripped, so switching back to `local` needs no restoration. A `type` that is neither `local` nor `remote` is rejected at load rather than silently treated as local, and `type: remote` with no `url` is rejected as well.
119
+
120
+ A remote kit is referenced by name and explicit version on both commands:
121
+
122
+ ```bash
123
+ tangyr install --kit acme-kit@1.4.0
124
+ tangyr sync --kit acme-kit@1.4.0
125
+ ```
126
+
127
+ The version is required and immutable — there is no `latest`. Passing a bare URL as `--kit` is rejected with a message pointing at `source.url`.
128
+
129
+ | Variable | Default | Purpose |
130
+ | ------------------ | ------------------ | --------------------------------------------------------------------- |
131
+ | `TANGYR_CACHE_DIR` | `~/.tangyr/cache/` | Root of the local materialized-kit cache. A leading `~/` is expanded. |
132
+
133
+ #### Cache layout
134
+
135
+ A materialized kit is published under the cache root at
136
+ `<cacheRoot>/<name>/<version>/`. The presence of a `.tangyr-complete` marker
137
+ file inside that directory is what makes the entry complete; an entry
138
+ without one is a partial write from an interrupted materialization and is
139
+ never read. A `.staging/` sibling directory under the cache root holds
140
+ in-progress materializations before they are published — it is not itself
141
+ a cache entry, and is safe to remove between runs. A cache entry also
142
+ carries `.tangyr-bundle.json`, the signed bundle (manifest, files,
143
+ signature) that produced it, written immediately before the completion
144
+ marker and read back on a warm-cache use to re-verify the publisher
145
+ signature without a network fetch; a bundle that declares a file at either
146
+ reserved filename is rejected before any write.
147
+
148
+ Publishing is atomic: a materialization is verified and written into a
149
+ unique temporary directory under `.staging/`, then made visible with a
150
+ single directory rename into `<name>/<version>/`, with the completion
151
+ marker written last. Two concurrent materializations of the same
152
+ `<name>@<version>` stage independently and cannot corrupt each other or
153
+ expose a half-written kit.
154
+
155
+ A warm, currently-valid entry that is also freshness-fresh (within the
156
+ 24-hour authorization-freshness window from the last successful check for
157
+ that origin) resolves fully offline, with no network call at all. Beyond
158
+ the window, the next `install`/`sync` performs a lightweight authorization
159
+ re-check against the origin, which also consults the revocation deny-list.
160
+ A definitive denial fails closed with no cache fallback; a genuine network
161
+ failure during that re-check falls back to the warm cache with a warning
162
+ and, under `--json`, a `{"degraded":true}` signal. See
163
+ [Offline, refresh, and authorization freshness](#offline-refresh-and-authorization-freshness)
164
+ below.
165
+
166
+ The cache root also carries a `.tangyr-freshness.json` file, recording, per
167
+ configured origin, the timestamp of the last successful authorization
168
+ check — used by the cache-first freshness gate a later activity in this
169
+ StepLedger adds. This file is not itself a cache entry: it is excluded from
170
+ `tangyr cache clear`.
171
+
172
+ #### Limits and file modes
173
+
174
+ A materialized bundle is bounded on two axes: the total decompressed payload
175
+ may not exceed **50 MiB**, and no single file inside it may exceed **5 MiB**.
176
+ The total ceiling is enforced while the response is streamed — the request
177
+ is aborted the moment the running total would cross it, before the payload
178
+ is buffered — so an oversized or maliciously compressed response is cut off
179
+ mid-transfer rather than fully downloaded first. The per-file ceiling is
180
+ checked after each file is decoded and hashed.
181
+
182
+ Materialized files are written at `0600` inside directories created at
183
+ `0700`. No symbolic or hard links are ever created by the materialization
184
+ pipeline, and a pre-existing symbolic link at a write target is refused
185
+ rather than followed.
186
+
187
+ **Credential-precedence trap**: setting exactly one of
188
+ `TANGYR_ACCESS_CLIENT_ID` / `TANGYR_ACCESS_CLIENT_SECRET` fails every remote
189
+ `install` and `sync` with exit `5` — even when a valid stored credential
190
+ from `tangyr auth login` exists. A leftover shell export, a half-applied
191
+ `.env`, or an inherited CI variable is enough to trigger it, because the
192
+ partial environment is treated as a misconfiguration and never falls
193
+ through to the stored credential. The fix: unset the stray variable, or set
194
+ both.
195
+
196
+ #### CI and non-interactive operation
197
+
198
+ `install`/`sync` on a remote source resolve the credential from
199
+ `TANGYR_ACCESS_CLIENT_ID`/`TANGYR_ACCESS_CLIENT_SECRET` or the stored
200
+ credential file only — this path never prompts, with or without the
201
+ global `--non-interactive` flag, so a CI pipeline supplying both
202
+ environment variables never hits a prompt on the credential-resolution
203
+ step. `auth login` additionally supports `--non-interactive` explicitly:
204
+ under it, a credential that cannot be resolved fails closed naming both
205
+ environment variables rather than prompting. The cache root is stable
206
+ and documented (`~/.tangyr/cache/` by default,
207
+ `TANGYR_CACHE_DIR`-overridable — see
208
+ [Remote source configuration](#remote-source-configuration)
209
+ above), so a CI pipeline can restore it between runs to keep a warm
210
+ cache. A fully non-interactive run with environment credentials against
211
+ the documented cache path is exercised end to end by
212
+ `tests/integration/remote-first-run.test.ts`; a real spawned CLI process
213
+ reaching a real (fixture) origin non-interactively, with zero prompts,
214
+ is additionally exercised by the exit-`5` and exit-`7` cases in
215
+ `tests/e2e/cli.test.ts`. A full non-interactive _success_ through a real
216
+ spawned process against a real signed production origin is not yet
217
+ provable — it requires a real, live, signed origin, which does not
218
+ exist yet (plan Decision 9's origin-provisioning track).
219
+
220
+ #### Offline, refresh, and authorization freshness
221
+
222
+ The cache root's `.tangyr-freshness.json` file (Activity 1) records, per
223
+ configured origin, the timestamp of the last successful authorization
224
+ check. A warm, currently-valid entry resolves fully offline — no network
225
+ call at all — for **24 hours** from that timestamp. Beyond the window, the
226
+ next `install`/`sync` performs a lightweight re-check against the origin
227
+ (and consults the revocation deny-list) before resolving.
228
+
229
+ `--offline` forces cache-only resolution: it never fetches and never
230
+ mutates. A complete, valid entry resolves immediately; a missing or
231
+ unusable entry exits `8` (Offline cache miss) naming the `tangyr kit
232
+ update` remediation. `--refresh` forces a re-fetch, re-verify, and replace,
233
+ bypassing the freshness gate entirely — it always reaches the origin.
234
+ `--offline` and `--refresh` are mutually exclusive; passing both exits `1`.
235
+
236
+ On a network failure during the lightweight re-check (not a definitive
237
+ denial), resolution falls back to the warm cache with a warning printed to
238
+ stderr, since the check could not confirm freshness or revocation status.
239
+ Under `--json`, this same condition also emits a `{"degraded":true,"kit":
240
+ "<name>@<version>"}` line to stderr. Neither `install` nor `sync` has a
241
+ full JSON output mode yet: `--json` is scoped to exactly this one signal
242
+ and is a no-op when resolution was not degraded — the rest of the
243
+ command's human-readable output is unchanged either way.
244
+
245
+ **Migrating a pre-existing cache entry.** A cache entry materialized before
246
+ this StepLedger (no `.tangyr-bundle.json`) is not corrupt — it is a
247
+ legitimate entry in an earlier format. It is treated as an offline cache
248
+ miss (exit `8`) specifically under `--offline`, because there is nothing to
249
+ re-verify signature freshness against without a network attempt. Running
250
+ `tangyr kit update --kit <name>@<version>`, or any `install`/`sync` without
251
+ `--offline`, while online, upgrades it to the current format.
252
+
253
+ #### Maintenance commands
254
+
255
+ `tangyr kit update --kit <name>@<version>` is install-free: it re-fetches
256
+ and re-verifies the named kit into the cache — replacing whatever is
257
+ already there, regardless of the current cache state (missing, stale,
258
+ warm-and-fresh, or a pre-existing legacy-format entry) — without running
259
+ the install or sync pipeline. It does not affect the authorization-freshness
260
+ window: it makes no call to the lightweight authorization probe and neither
261
+ reads nor writes `.tangyr-freshness.json`, since a full re-fetch and
262
+ re-verify is a strictly stronger refresh than a freshness check. A later
263
+ `install`/`sync` still evaluates its own freshness window independently of
264
+ any `tangyr kit update` that ran before it.
265
+
266
+ `tangyr cache clear [target]` removes cached kits from the local cache,
267
+ in one of three forms, each a distinct blast radius:
268
+
269
+ ```bash
270
+ tangyr cache clear # every cached kit, every version
271
+ tangyr cache clear acme-kit # every version of acme-kit only
272
+ tangyr cache clear acme-kit@1.4.0 # exactly that one cache entry
273
+ ```
274
+
275
+ No `target` is the widest and only destructive form: it removes every
276
+ `<cacheRoot>/<name>` directory. A bare `<name>` removes every version of
277
+ that one kit as a single directory. A `<name>@<version>` removes exactly
278
+ one cache entry, the narrowest of the three. None of the three forms ever
279
+ removes the cache root's `.staging/` directory or its
280
+ `.tangyr-freshness.json` file — both are reserved cache-root filenames,
281
+ excluded by name from every form, not only from the no-argument
282
+ enumeration.
283
+
284
+ The command is idempotent: running it again with the same argument, once
285
+ its targets are already gone, reports "nothing to remove" and exits `0`
286
+ rather than throwing or silently reporting a false positive. Without
287
+ `--yes`, it prompts for confirmation, naming the targets it is about to
288
+ remove rather than a bare count; `--dry-run` previews the targets without
289
+ removing anything; `--yes` skips the prompt.
290
+
291
+ `cache clear`'s `[target]` is this codebase's first use of Commander's
292
+ `.argument()` for a command's own positional value — distinct from every
293
+ `.option()` flag on this or any other command. It is the established
294
+ pattern for a future command that needs exactly one optional positional
295
+ value: `.argument("[name]", "<description>")`, with named flags still
296
+ declared via `.option()`. Commander delivers the positional argument's
297
+ value as the `.action()` callback's first parameter, ahead of any options
298
+ object — the reverse of every option-only command's callback signature.
299
+
300
+ ## Exit codes
301
+
302
+ The full exit-code taxonomy, in numeric order. Every code below `5`
303
+ predates the remote-source work and is shared by every command; codes
304
+ `5`-`9` are specific to a remote `install`/`sync`/`kit update` (see
305
+ [Remote kit source](#remote-kit-source) above for the pipeline that
306
+ reaches them).
307
+
308
+ | Exit code | Meaning | Reached when |
309
+ | --------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
310
+ | `0` | Success | The command completed with no fatal error. A `sync`/`install` whose loss report contains only `info`/`warning`-severity entries also exits `0`. |
311
+ | `1` | General failure | The generic, catch-all failure code for any error not mapped to a more specific code below — including a CLI usage error such as an unrecognized command, a malformed `--kit <name>@<version>` reference, or `--offline` and `--refresh` passed together; a corrupt or legacy-format installation manifest (`ManifestCorruptError`, `ManifestLegacyError`, `src/core/manifest.ts`) also falls through to this code, since `handleFatalError` carries no dedicated branch for either. |
312
+ | `2` | Conflict | Reserved for a conflict-resolution failure (`ConflictError`, `src/core/exit.ts`); declared and routed by `handleFatalError`, but not thrown by any current production code path. |
313
+ | `3` | Completed with error-severity loss | An `install`/`sync` run completes but its loss report contains at least one `error`-severity entry, distinct from the generic failure code `1`. An opt-out flag forces a zero exit for authors who accept the loss. |
314
+ | `4` | Target not detected | Reserved for a configured target tool whose runtime cannot be detected (`TargetNotDetectedError`, `src/core/exit.ts`); declared and routed the same way as exit `2`, and likewise not thrown by any current production code path. |
315
+ | `5` | Authentication required | The resolved credential is missing, partially configured (see the credential-precedence trap above), or rejected by the origin (`401`/`403`) — at login, at a cold fetch, or at the post-freshness-window re-check. |
316
+ | `6` | Remote unavailable | A transient fetch failure exhausts the bounded retry (3 attempts, 250 ms then 1000 ms backoff) with no warm cache to fall back to, or a warm-cache freshness re-check fails the same way with no usable fallback path. |
317
+ | `7` | Integrity failure | The signed bundle fails signature verification (including an unknown or out-of-window signing-key identifier), identity binding, a per-file content-hash check, a size ceiling, or path sanitization, or the requested `<name>@<version>` is present on the signed revocation deny-list. |
318
+ | `8` | Offline cache miss | `--offline` with no complete, valid cache entry for the requested reference. |
319
+ | `9` | Incompatible version | The running CLI is older than the bundle's declared minimum-compatible version, or the origin rejects this CLI's contract version. |
320
+ | `10` | Configuration missing | No `tangyr.config.yaml` is found (`ConfigNotFoundError`, `src/core/config.ts`) for a command that requires one. |
321
+ | `130` | User interrupt | The process received `SIGINT` (Ctrl-C) during a command. |
65
322
 
66
323
  ## Global flags
67
324