@alessandroraffa/tangyr 1.0.4 → 3.0.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 CHANGED
@@ -342,9 +342,9 @@ In `tangyr-kit.yaml`, `discipline` is optional: declare it to opt into the corpu
342
342
 
343
343
  ## Conflict policy
344
344
 
345
- Before writing, each destination is classified and resolved according to `onConflict` in `tangyr.config.yaml`: `ask` (default; prompts, and resolves to backup-and-replace in a non-interactive run), `skip`, `backup`, or `overwrite`.
345
+ Before writing, each destination is classified and resolved according to `onConflict` in `tangyr.config.yaml`: `ask` (default; prompts, and resolves to **skip** when there is no one to ask), `skip`, `backup`, or `overwrite`.
346
346
 
347
- `--yes` supplies the confirmation a non-interactive run needs; it does **not** override the posture. Under `--yes`, `skip` leaves conflicting destinations unchanged and says which, `overwrite` replaces without a backup, and `backup` copies the existing file — or the existing directory, recursively — into `.tangyr/backups/<timestamp>/` and records it in the manifest so `uninstall` can put it back. A symlink is replaced without a backup: what it points at is untouched.
347
+ `--yes` supplies the confirmation a non-interactive run needs; it does **not** override the posture. Under `--yes`, `skip` leaves conflicting destinations unchanged and says which, `overwrite` replaces without a backup, `ask` leaves the destination alone and names the postures that replace, and `backup` copies the existing file — or the existing directory, recursively — into `.tangyr/backups/<timestamp>/` and records it in the manifest so `uninstall` can put it back. A symlink is backed up as a symlink and restored as one: its target is untouched either way, but the link itself is the operator's artifact — often the one committed with the project — so replacing it without a record left nothing to put back.
348
348
 
349
349
  One decision is taken per destination, not per target, so a file several targets write — `AGENTS.md` is claimed by four — is backed up once.
350
350
 
package/dist/index.js CHANGED
@@ -21980,6 +21980,25 @@ function realPathOrInput(filePath) {
21980
21980
  return filePath;
21981
21981
  }
21982
21982
  }
21983
+ function clearSymlinkAtDestination(destination) {
21984
+ let stat;
21985
+ try {
21986
+ stat = fs23.lstatSync(destination);
21987
+ } catch {
21988
+ return null;
21989
+ }
21990
+ if (!stat.isSymbolicLink()) {
21991
+ return null;
21992
+ }
21993
+ let target = null;
21994
+ try {
21995
+ target = fs23.readlinkSync(destination);
21996
+ } catch {
21997
+ target = null;
21998
+ }
21999
+ fs23.unlinkSync(destination);
22000
+ return target ?? "(unreadable link target)";
22001
+ }
21983
22002
 
21984
22003
  // src/core/artifact-state.ts
21985
22004
  import fs24 from "fs";
@@ -22304,13 +22323,11 @@ async function runUninstallCommand(options, logger) {
22304
22323
  }
22305
22324
  }
22306
22325
  if (fullUninstall) {
22307
- const newestFirst = [...manifest.backups].reverse();
22308
22326
  const restored = /* @__PURE__ */ new Set();
22309
- for (const backup of newestFirst) {
22327
+ const kept = [];
22328
+ for (const backup of manifest.backups) {
22310
22329
  if (restored.has(backup.originalPath)) {
22311
- logger.verbose(
22312
- ` superseded by a newer backup, not restored: ${backup.backupLocation}`
22313
- );
22330
+ kept.push(backup);
22314
22331
  continue;
22315
22332
  }
22316
22333
  restored.add(backup.originalPath);
@@ -22322,6 +22339,19 @@ async function runUninstallCommand(options, logger) {
22322
22339
  );
22323
22340
  }
22324
22341
  }
22342
+ if (kept.length > 0) {
22343
+ logger.warn(
22344
+ `
22345
+ ${String(kept.length)} later backup(s) were NOT restored: the earliest copy of each location is the one that comes back, because it is the content from before the first install.`
22346
+ );
22347
+ for (const backup of kept) {
22348
+ logger.warn(` ${backup.originalPath}`);
22349
+ logger.warn(` later copy left at: ${backup.backupLocation}`);
22350
+ }
22351
+ logger.warn(
22352
+ " The manifest is removed below, so nothing will reference these copies afterwards. Move what you need out of the backups directory now."
22353
+ );
22354
+ }
22325
22355
  const manifestPath = path24.join(scopePath, "manifest.json");
22326
22356
  if (fs27.existsSync(manifestPath)) {
22327
22357
  fs27.unlinkSync(manifestPath);
@@ -26598,6 +26628,11 @@ function syncDirectComponent(config, sourceRoot, options, logger, directComponen
26598
26628
  };
26599
26629
  } else if (artifactClass === "managed-symlink") {
26600
26630
  fs36.unlinkSync(destination);
26631
+ } else if (artifactClass === "managed-compiled") {
26632
+ logger.warn(
26633
+ `${destination}: replacing another target's compiled instructions file with a link to the kit. A target that appends its rules here (opencode has no rules slot of its own) does not get them. Give it its own instructions path, or disable it.`
26634
+ );
26635
+ fs36.unlinkSync(destination);
26601
26636
  }
26602
26637
  ensureDir(path34.dirname(destination));
26603
26638
  createRelativeSymlink(source, destination, directComponent.type, {
@@ -28591,6 +28626,12 @@ function writeCompiledArtifact(component, destination, content, expectedHash, co
28591
28626
  return { component, status: "conflict", path: destination };
28592
28627
  }
28593
28628
  ensureDir(path37.dirname(destination));
28629
+ const clearedLink = clearSymlinkAtDestination(destination);
28630
+ if (clearedLink !== null) {
28631
+ logger.verbose(
28632
+ ` ${destination}: replaced a symlink to ${clearedLink} with generated content`
28633
+ );
28634
+ }
28594
28635
  fs38.writeFileSync(destination, content);
28595
28636
  return { component, status: "success", path: destination };
28596
28637
  }
@@ -29000,6 +29041,12 @@ function writeInstructionsFile(component, destination, content, contentHash, con
29000
29041
  return { component, status: "conflict", path: destination };
29001
29042
  }
29002
29043
  ensureDir(path39.dirname(destination));
29044
+ const clearedLink = clearSymlinkAtDestination(destination);
29045
+ if (clearedLink !== null) {
29046
+ logger.verbose(
29047
+ ` ${destination}: replaced a symlink to ${clearedLink} with generated content`
29048
+ );
29049
+ }
29003
29050
  fs39.writeFileSync(destination, content);
29004
29051
  return { component, status: "success", path: destination };
29005
29052
  }
@@ -29192,6 +29239,12 @@ function writeCompiledArtifact2(component, destination, content, expectedHash, c
29192
29239
  return { component, status: "conflict", path: destination };
29193
29240
  }
29194
29241
  ensureDir(path39.dirname(destination));
29242
+ const clearedLink = clearSymlinkAtDestination(destination);
29243
+ if (clearedLink !== null) {
29244
+ logger.verbose(
29245
+ ` ${destination}: replaced a symlink to ${clearedLink} with generated content`
29246
+ );
29247
+ }
29195
29248
  fs39.writeFileSync(destination, content);
29196
29249
  return { component, status: "success", path: destination };
29197
29250
  }
@@ -29827,6 +29880,12 @@ function writeCompiledArtifact3(component, destination, content, expectedHash, c
29827
29880
  return { component, status: "conflict", path: destination };
29828
29881
  }
29829
29882
  ensureDir(path40.dirname(destination));
29883
+ const clearedLink = clearSymlinkAtDestination(destination);
29884
+ if (clearedLink !== null) {
29885
+ logger.verbose(
29886
+ ` ${destination}: replaced a symlink to ${clearedLink} with generated content`
29887
+ );
29888
+ }
29830
29889
  fs40.writeFileSync(destination, content);
29831
29890
  return { component, status: "success", path: destination };
29832
29891
  }
@@ -30302,6 +30361,12 @@ function writeCompiledArtifact4(component, destination, content, expectedHash, c
30302
30361
  return { component, status: "conflict", path: destination };
30303
30362
  }
30304
30363
  ensureDir(path41.dirname(destination));
30364
+ const clearedLink = clearSymlinkAtDestination(destination);
30365
+ if (clearedLink !== null) {
30366
+ logger.verbose(
30367
+ ` ${destination}: replaced a symlink to ${clearedLink} with generated content`
30368
+ );
30369
+ }
30305
30370
  fs41.writeFileSync(destination, content);
30306
30371
  return { component, status: "success", path: destination };
30307
30372
  }
@@ -30795,6 +30860,12 @@ function writeCompiledArtifact5(component, destination, content, expectedHash, c
30795
30860
  return { component, status: "conflict", path: destination };
30796
30861
  }
30797
30862
  ensureDir(path42.dirname(destination));
30863
+ const clearedLink = clearSymlinkAtDestination(destination);
30864
+ if (clearedLink !== null) {
30865
+ logger.verbose(
30866
+ ` ${destination}: replaced a symlink to ${clearedLink} with generated content`
30867
+ );
30868
+ }
30798
30869
  fs42.writeFileSync(destination, content);
30799
30870
  return { component, status: "success", path: destination };
30800
30871
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alessandroraffa/tangyr",
3
- "version": "1.0.4",
3
+ "version": "3.0.0",
4
4
  "description": "CLI for the Tangyr discipline — install and manage operating kits for AI coding tools",
5
5
  "license": "MIT",
6
6
  "engines": {