@alessandroraffa/tangyr 6.0.1 → 7.0.1

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 +29 -14
  2. package/dist/index.js +83 -30
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -60,10 +60,12 @@ Options declared on individual commands in addition to the [global flags](#globa
60
60
  | Command | Option | Description |
61
61
  | -------------------------------------------------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
62
62
  | `install`, `sync` | `--loss-report <path>` | Write an **extra copy** of this run's semantic loss report to `<path>`. The report itself is scope state and is rewritten at `<scope>/loss.json` by every `install` and `sync` regardless, whether or not the run exits `3`. |
63
+ | `install` | `--replace` | Install this kit over a different one already in this scope: the installed kit is uninstalled first, which restores what it displaced. Without it, a change of kit is refused rather than performed as an update |
63
64
  | `install`, `sync` | `--ignore-errors` | Suppress exit code `3` when the loss report contains `error`-severity entries |
64
65
  | `install`, `sync` | `--kit <name>` | Kit to install/synchronize: `<name>` for a local source, `<name>@<version>` for a remote one |
65
66
  | `install`, `uninstall`, `assess` | `--tools <list>` | Comma-separated list of target tools |
66
67
  | `sync`, `status`, `verify`, `doctor`, `cleanup`, `probe` | `--target <tool>` | Limit the command to a single target |
68
+ | `probe` | `--scope <scope>` | Which scope's slots to probe. Declared on every scope-aware command; `probe` read one without offering the flag until 7.0.0 |
67
69
  | `sync` | `--component <name>` | Limit sync to one component type: `instructions`, `agents`, `skills`, `commands`, `rules`, `hooks`, `mcp`, `settings`, `output-styles`, `lsp` |
68
70
  | `sync` | `--force` | Rewrite managed artifacts even when provenance matches |
69
71
  | `doctor` | `--fix` | Apply safe automated fixes |
@@ -401,20 +403,20 @@ predates the remote-source work and is shared by every command; codes
401
403
  [Remote kit source](#remote-kit-source) above for the pipeline that
402
404
  reaches them).
403
405
 
404
- | Exit code | Meaning | Reached when |
405
- | --------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
406
- | `0` | Success | The command completed with no fatal error. A `sync`/`install` whose loss report contains only `info`/`warning`-severity entries also exits `0`. |
407
- | `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. |
408
- | `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. |
409
- | `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`. `--ignore-errors` on `install`/`sync` forces a zero exit for authors who accept the loss. |
410
- | `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. |
411
- | `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. |
412
- | `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. |
413
- | `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 channel pointer for a versionless `--kit <name>` cannot be resolved, is not signed by a pinned key, or names a version older than the one already seen. |
414
- | `8` | Offline cache miss | `--offline` with no complete, valid cache entry for the requested reference. |
415
- | `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. |
416
- | `10` | Configuration missing | No `tangyr.config.yaml` is found (`ConfigNotFoundError`, `src/core/config.ts`) for a command that requires one. |
417
- | `130` | User interrupt | The process received `SIGINT` (Ctrl-C) during a command. |
406
+ | Exit code | Meaning | Reached when |
407
+ | --------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
408
+ | `0` | Success | The command completed with no fatal error. A `sync`/`install` whose loss report contains only `info`/`warning`-severity entries also exits `0`. |
409
+ | `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; an `install` or `sync` that would put a different kit over the one this scope holds also exits here rather than installing both (see `--replace`); 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. |
410
+ | `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. |
411
+ | `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`. `--ignore-errors` on `install`/`sync` forces a zero exit for authors who accept the loss. |
412
+ | `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. |
413
+ | `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. |
414
+ | `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. |
415
+ | `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 channel pointer for a versionless `--kit <name>` cannot be resolved, is not signed by a pinned key, or names a version older than the one already seen. |
416
+ | `8` | Offline cache miss | `--offline` with no complete, valid cache entry for the requested reference. |
417
+ | `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. |
418
+ | `10` | Configuration missing | No `tangyr.config.yaml` is found (`ConfigNotFoundError`, `src/core/config.ts`) for a command that requires one. |
419
+ | `130` | User interrupt | The process received `SIGINT` (Ctrl-C) during a command. |
418
420
 
419
421
  ## Global flags
420
422
 
@@ -509,3 +511,16 @@ Precedence: `--claude-config-dir` flag > `TANGYR_CLAUDE_CONFIG_DIRS` env > `clau
509
511
  Per-root artifacts (instructions, agents, skills, commands, rules, output-styles, hooks settings) are written under each root. Shared artifacts (`~/.claude.json` for MCP, `~/.agents/hooks` for hooks) are resolved from the platform default and written once regardless of how many roots are configured.
510
512
 
511
513
  The manifest records the full set of roots written so that `verify` and `uninstall` operate on all of them.
514
+
515
+ ## Documentation
516
+
517
+ | Document | For |
518
+ | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
519
+ | [Operating Tangyr in a project](docs/guides/operating-tangyr-in-a-project.md) | Running the CLI in a repository you did not set up: what it copies, rewrites and deletes, how to read a run, and how to tell a defect from a target that changed |
520
+ | [Authoring a project-local kit](docs/guides/authoring-a-project-local-kit.md) | Writing a kit and installing it into a repository |
521
+ | [Installing a private kit](docs/guides/kit-consumer-onboarding.md) | Consuming a kit from a signed remote origin |
522
+ | [Functional specification](docs/specifications/tangyr-cli-functional-spec-v1.0.md) | What the tool must do: the canonical taxonomy, the per-target contracts, the loss and conflict models |
523
+ | [Maintaining this CLI](docs/maintaining-the-cli.md) | Working on the tool itself: the decisions the code cannot explain, and the traps that have already cost something |
524
+ | [Mapping declarations](mappings/README.md) | The declarative translation inputs under `mappings/` |
525
+
526
+ The dated records under `docs/stepledgers/`, `docs/plans/`, `docs/reports/`, `docs/initiatives/` and `docs/archive/` describe work as it was done and are not maintained against the current behaviour.
package/dist/index.js CHANGED
@@ -15930,7 +15930,8 @@ function readManifest(scopePath) {
15930
15930
  // and `uninstall` is a different process from `install`. The shared-surface
15931
15931
  // records went out to disk correctly and arrived as `undefined`, which put
15932
15932
  // removal back on the marker-based guess this exists to replace.
15933
- sharedSurfaces: Array.isArray(parsed.sharedSurfaces) ? parsed.sharedSurfaces : void 0
15933
+ sharedSurfaces: Array.isArray(parsed.sharedSurfaces) ? parsed.sharedSurfaces : void 0,
15934
+ kit: typeof parsed.kit === "object" && parsed.kit !== null && typeof parsed.kit.name === "string" ? parsed.kit : void 0
15934
15935
  };
15935
15936
  }
15936
15937
  function readStrictManifest(scopePath) {
@@ -16710,6 +16711,21 @@ function makeResolver(scope, projectRoot, tool) {
16710
16711
  return resolved;
16711
16712
  };
16712
16713
  }
16714
+ function slotsForScope(scopeMap, scope, projectRoot) {
16715
+ if (scope !== "project") {
16716
+ return scopeMap?.global;
16717
+ }
16718
+ const projectSlots = scopeMap?.project;
16719
+ if (!projectSlots) {
16720
+ return void 0;
16721
+ }
16722
+ const root = projectRoot ?? process.cwd();
16723
+ const resolved = {};
16724
+ for (const [slot, value] of Object.entries(projectSlots)) {
16725
+ resolved[slot] = typeof value === "string" ? path9.resolve(root, value) : value;
16726
+ }
16727
+ return resolved;
16728
+ }
16713
16729
 
16714
16730
  // src/mappings/loader.ts
16715
16731
  var import_yaml4 = __toESM(require_dist(), 1);
@@ -25383,7 +25399,7 @@ function checkClaudeCode(options, findings, config, sourceRoot, configDir, logge
25383
25399
  checkParentWritablePath(artifact.path, artifact.component, findings);
25384
25400
  }
25385
25401
  }
25386
- function slotsForScope(config, sourceRoot, configDir, target, options) {
25402
+ function slotsForScope2(config, sourceRoot, configDir, target, options) {
25387
25403
  const scopeMap = loadMappings(sourceRoot, config, configDir).platformPaths.targets[target];
25388
25404
  const scope = options.scope ?? config.scope ?? "global";
25389
25405
  if (scope !== "project") {
@@ -25405,7 +25421,7 @@ function slotsForScope(config, sourceRoot, configDir, target, options) {
25405
25421
  return resolved;
25406
25422
  }
25407
25423
  function checkCopilot(config, sourceRoot, configDir, findings, options) {
25408
- const globalPaths = slotsForScope(
25424
+ const globalPaths = slotsForScope2(
25409
25425
  config,
25410
25426
  sourceRoot,
25411
25427
  configDir,
@@ -25428,7 +25444,7 @@ function checkCopilot(config, sourceRoot, configDir, findings, options) {
25428
25444
  }
25429
25445
  }
25430
25446
  function checkCodex(config, sourceRoot, configDir, findings, logger, options) {
25431
- const globalPaths = slotsForScope(
25447
+ const globalPaths = slotsForScope2(
25432
25448
  config,
25433
25449
  sourceRoot,
25434
25450
  configDir,
@@ -32032,9 +32048,35 @@ async function runInstallCommand(options, logger) {
32032
32048
  logger.warn(
32033
32049
  `Existing Tangyr installation found in ${scopePath} (archetype: ${existingManifest.archetype}, version: ${existingManifest.version})`
32034
32050
  );
32051
+ const installedKit = existingManifest.kit;
32052
+ const switchingKit = installedKit !== void 0 && installedKit.name !== kitInfo.name;
32035
32053
  let action;
32036
- if (options.yes) {
32054
+ if (switchingKit && options.replace) {
32055
+ logger.warn(
32056
+ `Replacing '${installedKit.name}' (${installedKit.version}) with '${kitInfo.name}' (${kitInfo.version}): the installed kit is uninstalled first, which restores what it displaced.`
32057
+ );
32058
+ action = "clean-install";
32059
+ } else if (switchingKit && options.yes) {
32060
+ logger.error(
32061
+ `This scope holds the kit '${installedKit.name}' (${installedKit.version}), not '${kitInfo.name}'.`
32062
+ );
32063
+ logger.error(
32064
+ `Installing over it would leave both kits' artifacts in place, and every target would read both. Pass --replace to uninstall '${installedKit.name}' first and install '${kitInfo.name}' in its place, or run \`tangyr uninstall\` yourself. If you meant to update '${installedKit.name}', check kitPath in your configuration.`
32065
+ );
32066
+ process.exit(1);
32067
+ } else if (options.yes) {
32037
32068
  action = "update";
32069
+ } else if (switchingKit) {
32070
+ action = await dist_default8({
32071
+ message: `This scope holds '${installedKit.name}'. Installing '${kitInfo.name}' over it would leave both. Choose an action:`,
32072
+ choices: [
32073
+ {
32074
+ name: `Replace (uninstall '${installedKit.name}', then install '${kitInfo.name}')`,
32075
+ value: "clean-install"
32076
+ },
32077
+ { name: "Abort", value: "abort" }
32078
+ ]
32079
+ });
32038
32080
  } else if (!process.stdin.isTTY) {
32039
32081
  logger.error(
32040
32082
  "Existing installation requires an explicit action. Pass --yes to update, or run interactively."
@@ -32293,6 +32335,7 @@ SKIPPED CONFLICTS (${skippedTargetPaths.size} target(s) left unchanged):`
32293
32335
  ensureScope(scopePath);
32294
32336
  const manifest = existingManifest ?? createEmptyManifest(kitInfo.archetype, kitInfo.kitFormat, scope);
32295
32337
  manifest.tools = [.../* @__PURE__ */ new Set([...manifest.tools, ...tools])];
32338
+ manifest.kit = { name: kitInfo.name, version: kitInfo.version };
32296
32339
  manifest.files = [];
32297
32340
  for (const backup of backupsToAdd) {
32298
32341
  manifest.backups.push(backup);
@@ -34221,25 +34264,10 @@ function classifyDirectoryPresence(component, dirPath) {
34221
34264
  conformant: present
34222
34265
  };
34223
34266
  }
34224
- function slotsForScope2(scopeMap, scope, projectRoot) {
34225
- if (scope !== "project") {
34226
- return scopeMap?.global;
34227
- }
34228
- const projectSlots = scopeMap?.project;
34229
- if (!projectSlots) {
34230
- return void 0;
34231
- }
34232
- const root = projectRoot ?? process.cwd();
34233
- const resolved = {};
34234
- for (const [slot, value] of Object.entries(projectSlots)) {
34235
- resolved[slot] = typeof value === "string" ? path51.resolve(root, value) : value;
34236
- }
34237
- return resolved;
34238
- }
34239
34267
  function classifyCodexFiles(config, sourceRoot, configDir, scope, projectRoot) {
34240
34268
  const mappings = loadMappings(sourceRoot, config, configDir);
34241
34269
  const scopeMap = mappings.platformPaths.targets.codex;
34242
- const paths = slotsForScope2(scopeMap, scope, projectRoot);
34270
+ const paths = slotsForScope(scopeMap, scope, projectRoot);
34243
34271
  if (!paths?.instructions || !paths.agents || !paths.skills || !paths.hooks_shared_file || !paths.mcp_shared_file || !paths.rules) {
34244
34272
  throw new Error("platform-paths.yaml: incomplete codex path mapping");
34245
34273
  }
@@ -34387,7 +34415,7 @@ function classifyCodexHooksFile(filePath) {
34387
34415
  function classifyCopilotFiles(config, sourceRoot, configDir, scope, projectRoot) {
34388
34416
  const mappings = loadMappings(sourceRoot, config, configDir);
34389
34417
  const scopeMap = mappings.platformPaths.targets.copilot;
34390
- const paths = slotsForScope2(scopeMap, scope, projectRoot);
34418
+ const paths = slotsForScope(scopeMap, scope, projectRoot);
34391
34419
  if (!paths?.agents || !paths.skills || !paths.prompts || !paths.hooks || !paths.mcp_shared_file) {
34392
34420
  throw new Error("platform-paths.yaml: incomplete copilot path mapping");
34393
34421
  }
@@ -35327,6 +35355,16 @@ async function runSyncCommand(options, logger) {
35327
35355
  const lossReport = createLossReport();
35328
35356
  const mappings = targets.includes("copilot") || targets.includes("codex") || targets.includes("opencode") || targets.includes("cursor") || targets.includes("cline") || targets.some((target) => isDeferredTarget(target)) ? loadMappings(sourceRoot, config, configDir) : void 0;
35329
35357
  const kitSync = kitInfo ? prepareKitManifest(syncOptions.scope, syncOptions.projectRoot, kitInfo) : void 0;
35358
+ const installedKit = kitSync?.manifest.kit;
35359
+ if (kitInfo && installedKit && installedKit.name !== kitInfo.name) {
35360
+ logger.error(
35361
+ `This scope holds the kit '${installedKit.name}' (${installedKit.version}), and the configuration names '${kitInfo.name}'.`
35362
+ );
35363
+ logger.error(
35364
+ `sync updates the installed kit; it does not replace it. Run \`tangyr install --replace\` to put '${kitInfo.name}' in its place, or check kitPath if you meant to update '${installedKit.name}'.`
35365
+ );
35366
+ process.exit(1);
35367
+ }
35330
35368
  if (kitInfo && kitSync && options.dryRun) {
35331
35369
  printManifestDiff(logger, kitInfo.name, kitSync.diff);
35332
35370
  }
@@ -35609,6 +35647,7 @@ function updateManifest(kitInfo, kitSync) {
35609
35647
  });
35610
35648
  }
35611
35649
  }
35650
+ kitSync.manifest.kit = { name: kitInfo.name, version: kitInfo.version };
35612
35651
  refreshArtifactStates(kitSync.manifest.artifacts);
35613
35652
  writeManifest(kitSync.scopePath, kitSync.manifest);
35614
35653
  }
@@ -35691,11 +35730,16 @@ function cleanupRemovedManagedArtifacts(config, mappings, targets, options, remo
35691
35730
  return;
35692
35731
  }
35693
35732
  const currentComponent = options.component;
35694
- const copilotPaths = mappings.platformPaths.targets.copilot.global;
35695
- const codexPaths = mappings.platformPaths.targets.codex.global;
35696
- const opencodePaths = mappings.platformPaths.targets.opencode.global;
35697
- const cursorPaths = mappings.platformPaths.targets.cursor.global;
35698
- const clinePaths = mappings.platformPaths.targets.cline.global;
35733
+ const scopeOf = (target) => slotsForScope(
35734
+ mappings.platformPaths.targets[target],
35735
+ options.scope,
35736
+ options.projectRoot
35737
+ );
35738
+ const copilotPaths = scopeOf("copilot");
35739
+ const codexPaths = scopeOf("codex");
35740
+ const opencodePaths = scopeOf("opencode");
35741
+ const cursorPaths = scopeOf("cursor");
35742
+ const clinePaths = scopeOf("cline");
35699
35743
  for (const relativePath of removedSourcePaths) {
35700
35744
  const component = componentForRemovedSource(relativePath, config);
35701
35745
  if (currentComponent && component !== currentComponent) {
@@ -36829,7 +36873,13 @@ function buildProgram() {
36829
36873
  ).option(
36830
36874
  "--json",
36831
36875
  `Emit {"degraded": true} on stderr only when this resolution fell back to a warm cache after a network failure; otherwise this flag has no effect and output is unchanged \u2014 unlike --json on other commands, which always replaces the command's entire output.`
36832
- ).option("--tools <list>", "Comma-separated list of target tools").option("--loss-report <path>", "Write semantic loss report JSON to a file").option("--scope <scope>", "Installation scope: global or project").option(
36876
+ ).option("--tools <list>", "Comma-separated list of target tools").option(
36877
+ "--loss-report <path>",
36878
+ "Write an extra copy of this run's semantic loss report to <path>. The report itself is written to <scope>/loss.json by every install and sync regardless"
36879
+ ).option(
36880
+ "--replace",
36881
+ "Install this kit over a different one already in this scope, uninstalling that one first"
36882
+ ).option("--scope <scope>", "Installation scope: global or project").option(
36833
36883
  "--ignore-errors",
36834
36884
  "Suppress exit code 3 when error-severity loss entries are present"
36835
36885
  ).action(
@@ -36885,7 +36935,10 @@ function buildProgram() {
36885
36935
  ).option(
36886
36936
  "--json",
36887
36937
  `Emit {"degraded": true} on stderr only when this resolution fell back to a warm cache after a network failure; otherwise this flag has no effect and output is unchanged \u2014 unlike --json on other commands, which always replaces the command's entire output.`
36888
- ).option("--target <tool>", "Limit sync to a single target").option("--component <name>", "Limit sync to a single component type").option("--force", "Rewrite managed artifacts even when provenance matches").option("--loss-report <path>", "Write semantic loss report JSON to a file").option("--scope <scope>", "Installation scope: global or project").option(
36938
+ ).option("--target <tool>", "Limit sync to a single target").option("--component <name>", "Limit sync to a single component type").option("--force", "Rewrite managed artifacts even when provenance matches").option(
36939
+ "--loss-report <path>",
36940
+ "Write an extra copy of this run's semantic loss report to <path>. The report itself is written to <scope>/loss.json by every install and sync regardless"
36941
+ ).option("--scope <scope>", "Installation scope: global or project").option(
36889
36942
  "--ignore-errors",
36890
36943
  "Suppress exit code 3 when error-severity loss entries are present"
36891
36944
  ).action(
@@ -36974,7 +37027,7 @@ function buildProgram() {
36974
37027
  context.logger
36975
37028
  );
36976
37029
  });
36977
- program2.command("probe").description("Probe configured platform paths").option("--target <tool>", "Limit probing to a single target").option("--json", "Emit JSON output").action((options) => {
37030
+ program2.command("probe").description("Probe configured platform paths").option("--target <tool>", "Limit probing to a single target").option("--scope <scope>", "Installation scope: global or project").option("--json", "Emit JSON output").action((options) => {
36978
37031
  const context = commandContext(program2);
36979
37032
  runProbeCommand({ ...context.options, ...options }, context.logger);
36980
37033
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alessandroraffa/tangyr",
3
- "version": "6.0.1",
3
+ "version": "7.0.1",
4
4
  "description": "CLI for the Tangyr discipline — install and manage operating kits for AI coding tools",
5
5
  "license": "MIT",
6
6
  "engines": {