@alessandroraffa/tangyr 1.0.0 → 1.0.2

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 (2) hide show
  1. package/dist/index.js +179 -24
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -17378,21 +17378,33 @@ function collectProjectScopeFindings(projectRoot, tools, options) {
17378
17378
  ) : void 0;
17379
17379
  const probePaths = buildProjectProbes(tool, projectRoot, profile);
17380
17380
  for (const { resolvedPath, description, classification } of probePaths) {
17381
- if (!fs11.existsSync(resolvedPath)) {
17381
+ let stat;
17382
+ try {
17383
+ stat = fs11.lstatSync(resolvedPath);
17384
+ } catch {
17382
17385
  continue;
17383
17386
  }
17384
- const stat = fs11.lstatSync(resolvedPath);
17387
+ const type = stat.isSymbolicLink() ? "symlink" : stat.isDirectory() ? "directory" : "file";
17388
+ const ours = type === "file" && carriesTangyrMarker(resolvedPath);
17385
17389
  findings.push({
17386
17390
  path: resolvedPath,
17387
17391
  tool,
17388
- type: stat.isSymbolicLink() ? "symlink" : stat.isDirectory() ? "directory" : "file",
17389
- classification,
17390
- description
17392
+ type,
17393
+ classification: ours ? "compatible" : classification,
17394
+ description: ours ? `${description} (written by Tangyr)` : description
17391
17395
  });
17392
17396
  }
17393
17397
  }
17394
17398
  return findings;
17395
17399
  }
17400
+ function carriesTangyrMarker(filePath) {
17401
+ try {
17402
+ const firstNonEmpty = fs11.readFileSync(filePath, "utf8").split("\n").find((line) => line.trim().length > 0) ?? "";
17403
+ return extractProvenanceMarker(firstNonEmpty) !== null;
17404
+ } catch {
17405
+ return false;
17406
+ }
17407
+ }
17396
17408
  function requestedAssessTools(toolFilter) {
17397
17409
  return toolFilter ?? [...DEFAULT_ASSESS_TARGETS];
17398
17410
  }
@@ -21351,6 +21363,19 @@ function writeSharedFile(filePath, parsed, serializer, isFirstWrite, logger) {
21351
21363
  `tangyr: first modification of ${filePath} \u2014 unmanaged data will be preserved but exact whitespace or comments may not be.`
21352
21364
  );
21353
21365
  }
21366
+ let realTarget = null;
21367
+ try {
21368
+ if (fs19.lstatSync(filePath).isSymbolicLink()) {
21369
+ realTarget = fs19.realpathSync(filePath);
21370
+ }
21371
+ } catch {
21372
+ realTarget = null;
21373
+ }
21374
+ if (realTarget && realTarget !== filePath) {
21375
+ logger.warn(
21376
+ `tangyr: ${filePath} is a symlink \u2014 the file being modified is ${realTarget}`
21377
+ );
21378
+ }
21354
21379
  fs19.mkdirSync(path18.dirname(filePath), { recursive: true });
21355
21380
  fs19.writeFileSync(filePath, serialized);
21356
21381
  return true;
@@ -22169,11 +22194,23 @@ async function runUninstallCommand(options, logger) {
22169
22194
  }
22170
22195
  const claudeRoots = scope === "global" && selectedTools.has("claude-code") ? resolveClaudeRootsForUninstall(manifest, runtime) : [];
22171
22196
  let removedCount = 0;
22197
+ const removedParentDirs = /* @__PURE__ */ new Set();
22172
22198
  for (const file of useManifestPathRemoval ? manifestArtifacts : []) {
22173
22199
  if (isMergeSurfaceKey(file.relativePath)) {
22174
- logger.verbose(
22175
- ` left in place (merge surface \u2014 the block is stripped, not the file): ${file.path ?? file.relativePath}`
22200
+ const surfacePath = file.path ?? resolveManifestKeyToPath(
22201
+ file.relativePath,
22202
+ scope,
22203
+ runtime,
22204
+ uninstallProjectRoot
22176
22205
  );
22206
+ if (surfacePath && stripTangyrFromMergeSurface(surfacePath, logger)) {
22207
+ removedParentDirs.add(path24.dirname(surfacePath));
22208
+ removedCount++;
22209
+ } else {
22210
+ logger.verbose(
22211
+ ` left in place (merge surface, nothing of ours in it): ${surfacePath ?? file.relativePath}`
22212
+ );
22213
+ }
22177
22214
  continue;
22178
22215
  }
22179
22216
  const isClaudeGlobal = file.relativePath.startsWith("claude-code/") && scope === "global";
@@ -22186,6 +22223,7 @@ async function runUninstallCommand(options, logger) {
22186
22223
  for (const actualPath of paths) {
22187
22224
  if (actualPath && (fs27.existsSync(actualPath) || isSymlink(actualPath))) {
22188
22225
  removeManagedPath(actualPath);
22226
+ removedParentDirs.add(path24.dirname(actualPath));
22189
22227
  logger.verbose(` removed: ${actualPath}`);
22190
22228
  removedCount++;
22191
22229
  } else {
@@ -22200,7 +22238,14 @@ async function runUninstallCommand(options, logger) {
22200
22238
  uninstallProjectRoot
22201
22239
  );
22202
22240
  if (actualPath && (fs27.existsSync(actualPath) || isSymlink(actualPath))) {
22241
+ if (!stillMatchesRecord(actualPath, file.hash)) {
22242
+ logger.warn(
22243
+ ` left in place (no longer what was installed): ${actualPath}`
22244
+ );
22245
+ continue;
22246
+ }
22203
22247
  removeManagedPath(actualPath);
22248
+ removedParentDirs.add(path24.dirname(actualPath));
22204
22249
  logger.verbose(` removed: ${actualPath}`);
22205
22250
  removedCount++;
22206
22251
  } else {
@@ -22228,7 +22273,16 @@ async function runUninstallCommand(options, logger) {
22228
22273
  }
22229
22274
  }
22230
22275
  if (fullUninstall) {
22231
- for (const backup of manifest.backups) {
22276
+ const newestFirst = [...manifest.backups].reverse();
22277
+ const restored = /* @__PURE__ */ new Set();
22278
+ for (const backup of newestFirst) {
22279
+ if (restored.has(backup.originalPath)) {
22280
+ logger.verbose(
22281
+ ` superseded by a newer backup, not restored: ${backup.backupLocation}`
22282
+ );
22283
+ continue;
22284
+ }
22285
+ restored.add(backup.originalPath);
22232
22286
  try {
22233
22287
  restoreBackup(backup, logger);
22234
22288
  } catch (err) {
@@ -22242,6 +22296,11 @@ async function runUninstallCommand(options, logger) {
22242
22296
  fs27.unlinkSync(manifestPath);
22243
22297
  logger.verbose(` removed manifest: ${manifestPath}`);
22244
22298
  }
22299
+ pruneEmptyDirs(path24.join(scopePath, "backups"), logger);
22300
+ const pruneBoundary = path24.dirname(scopePath);
22301
+ for (const dir of removedParentDirs) {
22302
+ pruneEmptyAncestors(dir, pruneBoundary, logger);
22303
+ }
22245
22304
  try {
22246
22305
  const remaining = fs27.readdirSync(scopePath);
22247
22306
  if (remaining.length === 0) {
@@ -22621,6 +22680,86 @@ var MERGE_SURFACE_SLOTS = /* @__PURE__ */ new Set([
22621
22680
  "mcp_shared_file",
22622
22681
  "mcp_file"
22623
22682
  ]);
22683
+ function stripTangyrFromMergeSurface(filePath, logger) {
22684
+ const parsed = readJsonObject2(filePath);
22685
+ if (!parsed) {
22686
+ return false;
22687
+ }
22688
+ const cleaned = { ...parsed };
22689
+ const hooks = removeManagedClaudeHookEntries(
22690
+ parsed.hooks,
22691
+ (entry) => entry._tangyr === true || entry._proteus === true
22692
+ );
22693
+ if (hooks === void 0) {
22694
+ delete cleaned.hooks;
22695
+ } else {
22696
+ cleaned.hooks = hooks;
22697
+ }
22698
+ if (isRecord(parsed.mcpServers)) {
22699
+ const kept = Object.fromEntries(
22700
+ Object.entries(parsed.mcpServers).filter(
22701
+ ([, server]) => !isRecord(server) || !isRecord(server._tangyr) && !isRecord(server._proteus)
22702
+ )
22703
+ );
22704
+ if (Object.keys(kept).length === 0) {
22705
+ delete cleaned.mcpServers;
22706
+ } else {
22707
+ cleaned.mcpServers = kept;
22708
+ }
22709
+ }
22710
+ delete cleaned._tangyr;
22711
+ delete cleaned._proteus;
22712
+ if (JSON.stringify(cleaned) === JSON.stringify(parsed)) {
22713
+ return false;
22714
+ }
22715
+ writeJsonOrRemove(filePath, cleaned);
22716
+ logger.verbose(` stripped Tangyr's block from: ${filePath}`);
22717
+ return true;
22718
+ }
22719
+ function pruneEmptyAncestors(startDir, stopAt, logger) {
22720
+ const boundary = path24.resolve(stopAt);
22721
+ let current = path24.resolve(startDir);
22722
+ while (current !== boundary && current.startsWith(`${boundary}${path24.sep}`)) {
22723
+ try {
22724
+ if (fs27.readdirSync(current).length > 0) {
22725
+ return;
22726
+ }
22727
+ fs27.rmdirSync(current);
22728
+ logger.verbose(` removed empty directory: ${current}`);
22729
+ } catch {
22730
+ return;
22731
+ }
22732
+ current = path24.dirname(current);
22733
+ }
22734
+ }
22735
+ function pruneEmptyDirs(dir, logger) {
22736
+ let stat;
22737
+ try {
22738
+ stat = fs27.lstatSync(dir);
22739
+ } catch {
22740
+ return;
22741
+ }
22742
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
22743
+ return;
22744
+ }
22745
+ for (const entry of fs27.readdirSync(dir, { withFileTypes: true })) {
22746
+ if (entry.isDirectory() && !entry.isSymbolicLink()) {
22747
+ pruneEmptyDirs(path24.join(dir, entry.name), logger);
22748
+ }
22749
+ }
22750
+ try {
22751
+ fs27.rmdirSync(dir);
22752
+ logger.verbose(` removed empty directory: ${dir}`);
22753
+ } catch {
22754
+ }
22755
+ }
22756
+ function stillMatchesRecord(artifactPath, recorded) {
22757
+ if (isLegacyArtifactState(recorded)) {
22758
+ return true;
22759
+ }
22760
+ const actual = describeArtifactState(artifactPath);
22761
+ return actual === null || actual === recorded;
22762
+ }
22624
22763
  function isMergeSurfaceKey(relativePath) {
22625
22764
  const [, ...rest] = relativePath.split("/");
22626
22765
  return MERGE_SURFACE_SLOTS.has(rest.join("/"));
@@ -24952,6 +25091,7 @@ function toConflictPolicy2(value) {
24952
25091
  }
24953
25092
 
24954
25093
  // src/commands/install.ts
25094
+ import crypto11 from "crypto";
24955
25095
  import fs43 from "fs";
24956
25096
  import path43 from "path";
24957
25097
 
@@ -26363,6 +26503,12 @@ function applyClaudeCodeShimUpgrade(shimPath, agentsMdPath, manifest, scopePath,
26363
26503
  const firstNonEmptyLine = existing.split("\n").find((l) => l.trim().length > 0) ?? "";
26364
26504
  const marker = extractProvenanceMarker(firstNonEmptyLine);
26365
26505
  if (marker !== null && marker.hash === expectedBodyHash) {
26506
+ addArtifactToManifest(manifest, {
26507
+ relativePath: SHIM_MANIFEST_KEY,
26508
+ path: shimPath,
26509
+ hash: expectedBodyHash,
26510
+ origin: "tangyr-managed"
26511
+ });
26366
26512
  logger?.verbose(
26367
26513
  `instructions-shim: already current, skipped -> ${shimPath}`
26368
26514
  );
@@ -30689,20 +30835,25 @@ function parseCommandFile(commandFile) {
30689
30835
  function isBackupablePath(targetPath) {
30690
30836
  try {
30691
30837
  const lstat = fs43.lstatSync(targetPath);
30692
- if (lstat.isSymbolicLink()) {
30693
- return false;
30694
- }
30695
- return lstat.isFile() || lstat.isDirectory();
30838
+ return lstat.isSymbolicLink() || lstat.isFile() || lstat.isDirectory();
30696
30839
  } catch {
30697
30840
  return false;
30698
30841
  }
30699
30842
  }
30700
30843
  function backupConflictingTarget(targetPath, scopePath) {
30701
30844
  const ts2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/gu, "").replace("T", "T").replace(/\.\d+Z$/u, "Z");
30702
- const relativeToRoot = path43.relative("/", targetPath).replace(/^\.\.\/+/gu, "");
30845
+ const scopeRoot = path43.dirname(scopePath);
30846
+ let relativeToRoot = path43.relative(scopeRoot, targetPath);
30847
+ if (relativeToRoot.startsWith("..") || path43.isAbsolute(relativeToRoot)) {
30848
+ const digest = crypto11.createHash("sha256").update(path43.dirname(targetPath)).digest("hex").slice(0, 12);
30849
+ relativeToRoot = path43.join(`_outside-${digest}`, path43.basename(targetPath));
30850
+ }
30703
30851
  const backupPath = path43.join(scopePath, "backups", ts2, relativeToRoot);
30704
30852
  fs43.mkdirSync(path43.dirname(backupPath), { recursive: true });
30705
- if (fs43.lstatSync(targetPath).isDirectory()) {
30853
+ const stat = fs43.lstatSync(targetPath);
30854
+ if (stat.isSymbolicLink()) {
30855
+ fs43.symlinkSync(fs43.readlinkSync(targetPath), backupPath);
30856
+ } else if (stat.isDirectory()) {
30706
30857
  fs43.cpSync(targetPath, backupPath, {
30707
30858
  recursive: true,
30708
30859
  // Preserve links as links: following them would copy the kit into the
@@ -30918,14 +31069,14 @@ async function runInstallCommand(options, logger) {
30918
31069
  return;
30919
31070
  }
30920
31071
  if (conflictingFindings.length > 0) {
31072
+ const toolsByPath = /* @__PURE__ */ new Map();
31073
+ for (const finding of conflictingFindings) {
31074
+ const tools2 = toolsByPath.get(finding.path) ?? [];
31075
+ tools2.push(finding.tool);
31076
+ toolsByPath.set(finding.path, tools2);
31077
+ }
30921
31078
  if (options.yes) {
30922
31079
  const decidedPaths = /* @__PURE__ */ new Set();
30923
- const toolsByPath = /* @__PURE__ */ new Map();
30924
- for (const finding of conflictingFindings) {
30925
- const tools2 = toolsByPath.get(finding.path) ?? [];
30926
- tools2.push(finding.tool);
30927
- toolsByPath.set(finding.path, tools2);
30928
- }
30929
31080
  const posture = config.onConflict === "ask" ? "skip" : config.onConflict;
30930
31081
  const replaceable = Array.from(toolsByPath.keys()).filter(
30931
31082
  (p) => !foreignSkillsSlots.has(p)
@@ -31004,10 +31155,11 @@ ACCEPTED CONFLICTS (posture: ${posture}; ${replaceable.length} target(s)):`
31004
31155
  } else {
31005
31156
  logger.info(
31006
31157
  `
31007
- Found ${conflictingFindings.length} conflicting target(s). Please choose an action for each:
31158
+ Found ${toolsByPath.size} conflicting target(s). Please choose an action for each:
31008
31159
  `
31009
31160
  );
31010
- for (const finding of conflictingFindings) {
31161
+ for (const [conflictPath, claimingTools] of toolsByPath) {
31162
+ const finding = { path: conflictPath, tool: claimingTools.join(", ") };
31011
31163
  const choice = await dist_default8({
31012
31164
  message: `Conflict: ${finding.path} (${finding.tool})
31013
31165
  Choose an action:`,
@@ -31058,6 +31210,9 @@ SKIPPED CONFLICTS (${skippedTargetPaths.size} target(s) left unchanged):`
31058
31210
  for (const backup of backupsToAdd) {
31059
31211
  manifest.backups.push(backup);
31060
31212
  }
31213
+ if (backupsToAdd.length > 0) {
31214
+ writeManifest(scopePath, manifest);
31215
+ }
31061
31216
  const mappings = loadMappings(kitInfo.path, config, configDir);
31062
31217
  const installLossReport = createLossReport();
31063
31218
  let installedCount = 0;
@@ -32320,7 +32475,7 @@ function commandProbeRows(target, key, command, args) {
32320
32475
  }
32321
32476
 
32322
32477
  // src/commands/status.ts
32323
- import crypto11 from "crypto";
32478
+ import crypto12 from "crypto";
32324
32479
  import fs45 from "fs";
32325
32480
  import path46 from "path";
32326
32481
  function runStatusCommand(options, logger) {
@@ -33444,7 +33599,7 @@ function containsTangyrSource(value) {
33444
33599
  return typeof value._tangyr_source === "string" || typeof value._proteus_source === "string" || Object.values(value).some((entry) => containsTangyrSource(entry));
33445
33600
  }
33446
33601
  function hashString6(value) {
33447
- return `sha256:${crypto11.createHash("sha256").update(value).digest("hex")}`;
33602
+ return `sha256:${crypto12.createHash("sha256").update(value).digest("hex")}`;
33448
33603
  }
33449
33604
 
33450
33605
  // src/commands/sync.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alessandroraffa/tangyr",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "CLI for the Tangyr discipline — install and manage operating kits for AI coding tools",
5
5
  "license": "MIT",
6
6
  "engines": {