@alessandroraffa/tangyr 1.0.3 → 2.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.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/dist/index.js +63 -43
  3. package/package.json +1 -1
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
@@ -16052,22 +16052,53 @@ function isArtifactManifestPath(relativePath) {
16052
16052
  const [tool] = relativePath.split("/");
16053
16053
  return artifactManifestPrefixes.has(tool);
16054
16054
  }
16055
- function backupFile(originalPath, manifest) {
16056
- const priorSuffixes = manifest.backups.filter((b) => b.originalPath === originalPath).map((b) => b.integerSuffix);
16057
- const suffix = priorSuffixes.length > 0 ? Math.max(...priorSuffixes) + 1 : 1;
16058
- const backupPath = `${originalPath}.bak.${suffix}`;
16055
+ function resolveBackupPath(originalPath, scopePath) {
16056
+ const ts2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/gu, "").replace(/\.\d+Z$/u, "Z");
16057
+ const scopeRoot = path6.dirname(scopePath);
16058
+ let relativeToRoot = path6.relative(scopeRoot, originalPath);
16059
+ if (relativeToRoot.startsWith("..") || path6.isAbsolute(relativeToRoot)) {
16060
+ const digest = crypto2.createHash("sha256").update(path6.dirname(originalPath)).digest("hex").slice(0, 12);
16061
+ relativeToRoot = path6.join(
16062
+ `_outside-${digest}`,
16063
+ path6.basename(originalPath)
16064
+ );
16065
+ }
16066
+ const base = path6.join(scopePath, "backups", ts2, relativeToRoot);
16067
+ let candidate = base;
16068
+ for (let n7 = 2; fs6.existsSync(candidate) || isPresentLink(candidate); n7++) {
16069
+ candidate = `${base}-${String(n7)}`;
16070
+ }
16071
+ return candidate;
16072
+ }
16073
+ function isPresentLink(p) {
16074
+ try {
16075
+ return fs6.lstatSync(p).isSymbolicLink();
16076
+ } catch {
16077
+ return false;
16078
+ }
16079
+ }
16080
+ function copyForBackup(originalPath, backupPath) {
16081
+ fs6.mkdirSync(path6.dirname(backupPath), { recursive: true });
16059
16082
  const stat = fs6.lstatSync(originalPath);
16060
16083
  if (stat.isSymbolicLink()) {
16061
16084
  fs6.symlinkSync(fs6.readlinkSync(originalPath), backupPath);
16062
16085
  } else if (stat.isDirectory()) {
16063
16086
  fs6.cpSync(originalPath, backupPath, {
16064
16087
  recursive: true,
16088
+ // Preserve links as links: following them would copy the kit into the
16089
+ // backup and, worse, make the backup look like the operator's content.
16065
16090
  dereference: false,
16066
16091
  verbatimSymlinks: true
16067
16092
  });
16068
16093
  } else {
16069
16094
  fs6.copyFileSync(originalPath, backupPath);
16070
16095
  }
16096
+ }
16097
+ function backupFile(originalPath, manifest, scopePath) {
16098
+ const priorSuffixes = manifest.backups.filter((b) => b.originalPath === originalPath).map((b) => b.integerSuffix);
16099
+ const suffix = priorSuffixes.length > 0 ? Math.max(...priorSuffixes) + 1 : 1;
16100
+ const backupPath = resolveBackupPath(originalPath, scopePath);
16101
+ copyForBackup(originalPath, backupPath);
16071
16102
  manifest.backups.push({
16072
16103
  originalPath,
16073
16104
  backupLocation: backupPath,
@@ -22273,13 +22304,11 @@ async function runUninstallCommand(options, logger) {
22273
22304
  }
22274
22305
  }
22275
22306
  if (fullUninstall) {
22276
- const newestFirst = [...manifest.backups].reverse();
22277
22307
  const restored = /* @__PURE__ */ new Set();
22278
- for (const backup of newestFirst) {
22308
+ const kept = [];
22309
+ for (const backup of manifest.backups) {
22279
22310
  if (restored.has(backup.originalPath)) {
22280
- logger.verbose(
22281
- ` superseded by a newer backup, not restored: ${backup.backupLocation}`
22282
- );
22311
+ kept.push(backup);
22283
22312
  continue;
22284
22313
  }
22285
22314
  restored.add(backup.originalPath);
@@ -22291,6 +22320,19 @@ async function runUninstallCommand(options, logger) {
22291
22320
  );
22292
22321
  }
22293
22322
  }
22323
+ if (kept.length > 0) {
22324
+ logger.warn(
22325
+ `
22326
+ ${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.`
22327
+ );
22328
+ for (const backup of kept) {
22329
+ logger.warn(` ${backup.originalPath}`);
22330
+ logger.warn(` later copy left at: ${backup.backupLocation}`);
22331
+ }
22332
+ logger.warn(
22333
+ " The manifest is removed below, so nothing will reference these copies afterwards. Move what you need out of the backups directory now."
22334
+ );
22335
+ }
22294
22336
  const manifestPath = path24.join(scopePath, "manifest.json");
22295
22337
  if (fs27.existsSync(manifestPath)) {
22296
22338
  fs27.unlinkSync(manifestPath);
@@ -25091,7 +25133,6 @@ function toConflictPolicy2(value) {
25091
25133
  }
25092
25134
 
25093
25135
  // src/commands/install.ts
25094
- import crypto11 from "crypto";
25095
25136
  import fs43 from "fs";
25096
25137
  import path43 from "path";
25097
25138
 
@@ -26487,7 +26528,7 @@ function applyClaudeCodeShimUpgrade(shimPath, agentsMdPath, manifest, scopePath,
26487
26528
  const shimContent = buildAgentsMdShim(shimPath, agentsMdPath, "claude-code");
26488
26529
  const shimLink = lstatOrNull(shimPath);
26489
26530
  if (shimLink?.isSymbolicLink()) {
26490
- backupFile(shimPath, manifest);
26531
+ backupFile(shimPath, manifest, scopePath);
26491
26532
  writeManifest(scopePath, manifest);
26492
26533
  fs36.unlinkSync(shimPath);
26493
26534
  logger?.verbose(
@@ -26522,7 +26563,7 @@ function applyClaudeCodeShimUpgrade(shimPath, agentsMdPath, manifest, scopePath,
26522
26563
  );
26523
26564
  return;
26524
26565
  }
26525
- backupFile(shimPath, manifest);
26566
+ backupFile(shimPath, manifest, scopePath);
26526
26567
  writeManifest(scopePath, manifest);
26527
26568
  fs36.writeFileSync(shimPath, shimContent);
26528
26569
  addArtifactToManifest(manifest, {
@@ -26710,7 +26751,7 @@ function handleConflict(destination, config, options, logger, manifest, scopePat
26710
26751
  }
26711
26752
  if (config.onConflict === "backup") {
26712
26753
  if (manifest && scopePath) {
26713
- backupFile(destination, manifest);
26754
+ backupFile(destination, manifest, scopePath);
26714
26755
  writeManifest(scopePath, manifest);
26715
26756
  fs36.rmSync(destination, { recursive: true, force: true });
26716
26757
  } else {
@@ -28606,7 +28647,7 @@ function handleConflict2(destination, config, options, logger, manifest, scopePa
28606
28647
  }
28607
28648
  if (config.onConflict === "backup") {
28608
28649
  if (manifest && scopePath) {
28609
- backupFile(destination, manifest);
28650
+ backupFile(destination, manifest, scopePath);
28610
28651
  writeManifest(scopePath, manifest);
28611
28652
  fs38.rmSync(destination, { recursive: true, force: true });
28612
28653
  } else {
@@ -29187,7 +29228,7 @@ function handleConflict3(destination, config, options, logger, manifest, scopePa
29187
29228
  }
29188
29229
  if (config.onConflict === "backup") {
29189
29230
  if (manifest && scopePath) {
29190
- backupFile(destination, manifest);
29231
+ backupFile(destination, manifest, scopePath);
29191
29232
  writeManifest(scopePath, manifest);
29192
29233
  fs39.rmSync(destination, { recursive: true, force: true });
29193
29234
  } else {
@@ -29813,7 +29854,7 @@ function handleConflict4(destination, config, options, logger, manifest, scopePa
29813
29854
  }
29814
29855
  if (config.onConflict === "backup") {
29815
29856
  if (manifest && scopePath) {
29816
- backupFile(destination, manifest);
29857
+ backupFile(destination, manifest, scopePath);
29817
29858
  writeManifest(scopePath, manifest);
29818
29859
  fs40.rmSync(destination, { recursive: true, force: true });
29819
29860
  } else {
@@ -30288,7 +30329,7 @@ function handleConflict5(destination, config, options, logger, manifest, scopePa
30288
30329
  }
30289
30330
  if (config.onConflict === "backup") {
30290
30331
  if (manifest && scopePath) {
30291
- backupFile(destination, manifest);
30332
+ backupFile(destination, manifest, scopePath);
30292
30333
  writeManifest(scopePath, manifest);
30293
30334
  fs41.rmSync(destination, { recursive: true, force: true });
30294
30335
  } else {
@@ -30781,7 +30822,7 @@ function handleConflict6(destination, config, options, logger, manifest, scopePa
30781
30822
  }
30782
30823
  if (config.onConflict === "backup") {
30783
30824
  if (manifest && scopePath) {
30784
- backupFile(destination, manifest);
30825
+ backupFile(destination, manifest, scopePath);
30785
30826
  writeManifest(scopePath, manifest);
30786
30827
  fs42.rmSync(destination, { recursive: true, force: true });
30787
30828
  } else {
@@ -30849,29 +30890,8 @@ function isBackupablePath(targetPath) {
30849
30890
  }
30850
30891
  }
30851
30892
  function backupConflictingTarget(targetPath, scopePath) {
30852
- const ts2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/gu, "").replace("T", "T").replace(/\.\d+Z$/u, "Z");
30853
- const scopeRoot = path43.dirname(scopePath);
30854
- let relativeToRoot = path43.relative(scopeRoot, targetPath);
30855
- if (relativeToRoot.startsWith("..") || path43.isAbsolute(relativeToRoot)) {
30856
- const digest = crypto11.createHash("sha256").update(path43.dirname(targetPath)).digest("hex").slice(0, 12);
30857
- relativeToRoot = path43.join(`_outside-${digest}`, path43.basename(targetPath));
30858
- }
30859
- const backupPath = path43.join(scopePath, "backups", ts2, relativeToRoot);
30860
- fs43.mkdirSync(path43.dirname(backupPath), { recursive: true });
30861
- const stat = fs43.lstatSync(targetPath);
30862
- if (stat.isSymbolicLink()) {
30863
- fs43.symlinkSync(fs43.readlinkSync(targetPath), backupPath);
30864
- } else if (stat.isDirectory()) {
30865
- fs43.cpSync(targetPath, backupPath, {
30866
- recursive: true,
30867
- // Preserve links as links: following them would copy the kit into the
30868
- // backup and, worse, make the backup look like the user's own content.
30869
- dereference: false,
30870
- verbatimSymlinks: true
30871
- });
30872
- } else {
30873
- fs43.copyFileSync(targetPath, backupPath);
30874
- }
30893
+ const backupPath = resolveBackupPath(targetPath, scopePath);
30894
+ copyForBackup(targetPath, backupPath);
30875
30895
  return backupPath;
30876
30896
  }
30877
30897
  async function runInstallCommand(options, logger) {
@@ -32488,7 +32508,7 @@ function commandProbeRows(target, key, command, args) {
32488
32508
  }
32489
32509
 
32490
32510
  // src/commands/status.ts
32491
- import crypto12 from "crypto";
32511
+ import crypto11 from "crypto";
32492
32512
  import fs45 from "fs";
32493
32513
  import path46 from "path";
32494
32514
  function runStatusCommand(options, logger) {
@@ -33612,7 +33632,7 @@ function containsTangyrSource(value) {
33612
33632
  return typeof value._tangyr_source === "string" || typeof value._proteus_source === "string" || Object.values(value).some((entry) => containsTangyrSource(entry));
33613
33633
  }
33614
33634
  function hashString6(value) {
33615
- return `sha256:${crypto12.createHash("sha256").update(value).digest("hex")}`;
33635
+ return `sha256:${crypto11.createHash("sha256").update(value).digest("hex")}`;
33616
33636
  }
33617
33637
 
33618
33638
  // src/commands/sync.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alessandroraffa/tangyr",
3
- "version": "1.0.3",
3
+ "version": "2.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": {