@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.
- package/README.md +258 -1
- package/dist/index.js +2137 -924
- 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.
|
|
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
|
|