@alessandroraffa/tangyr 0.10.1 → 0.11.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 +13 -2
  2. package/dist/index.js +153 -12
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -107,13 +107,24 @@ claudeCodeGlobalPaths:
107
107
  - ~/.claude-work
108
108
  ```
109
109
 
110
- **Via CLI flag** (repeatable, overrides the config field):
110
+ **Via environment variable** (`TANGYR_CLAUDE_CONFIG_DIRS`):
111
+
112
+ A colon-separated list of Claude Code global root directories. Entries support `~/` expansion. This layer is designed for machine-specific configuration that must not be committed to a shared kit config.
113
+
114
+ ```sh
115
+ export TANGYR_CLAUDE_CONFIG_DIRS="~/.claude:~/.claude-work"
116
+ tangyr sync
117
+ ```
118
+
119
+ Auto-sync use case: if a kit-change hook or automation runs `tangyr sync`, exporting `TANGYR_CLAUDE_CONFIG_DIRS` in the shell environment keeps multiple Claude configs in sync automatically — without putting machine-specific paths in a shared `tangyr.config.yaml`.
120
+
121
+ **Via CLI flag** (repeatable):
111
122
 
112
123
  ```sh
113
124
  tangyr sync --claude-config-dir ~/.claude --claude-config-dir ~/.claude-work
114
125
  ```
115
126
 
116
- Precedence: `--claude-config-dir` flag > `claudeCodeGlobalPaths` in config > default `~/.claude`.
127
+ Precedence: `--claude-config-dir` flag > `TANGYR_CLAUDE_CONFIG_DIRS` env > `claudeCodeGlobalPaths` in config > default `~/.claude`.
117
128
 
118
129
  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.
119
130
 
package/dist/index.js CHANGED
@@ -15644,8 +15644,12 @@ function resolveClineProfile(scope, projectRoot, mappings) {
15644
15644
  };
15645
15645
  }
15646
15646
  var DEFAULT_CLAUDE_ROOT_RAW = "~/.claude";
15647
+ function parseEnvDirs(raw) {
15648
+ return raw.split(":").map((s) => s.trim()).filter((s) => s.length > 0);
15649
+ }
15647
15650
  function resolveClaudeGlobalRoots(input) {
15648
- const rawRoots = (input.flag?.length ?? 0) > 0 ? input.flag : (input.config?.length ?? 0) > 0 ? input.config : [DEFAULT_CLAUDE_ROOT_RAW];
15651
+ const envEntries = input.env !== void 0 ? parseEnvDirs(input.env) : void 0;
15652
+ const rawRoots = (input.flag?.length ?? 0) > 0 ? input.flag : envEntries !== void 0 && envEntries.length > 0 ? envEntries : (input.config?.length ?? 0) > 0 ? input.config : [DEFAULT_CLAUDE_ROOT_RAW];
15649
15653
  const seen = /* @__PURE__ */ new Set();
15650
15654
  const roots = [];
15651
15655
  for (const raw of rawRoots) {
@@ -20135,7 +20139,7 @@ import path17 from "path";
20135
20139
  var import_yaml5 = __toESM(require_dist(), 1);
20136
20140
  import fs16 from "fs";
20137
20141
  import path16 from "path";
20138
- function resolveKitHookNames(sourceRoot) {
20142
+ function resolveKitComponentNames(sourceRoot, category) {
20139
20143
  const manifestPath = path16.resolve(sourceRoot, "tangyr-kit.yaml");
20140
20144
  if (!fs16.existsSync(manifestPath)) {
20141
20145
  return void 0;
@@ -20145,10 +20149,10 @@ function resolveKitHookNames(sourceRoot) {
20145
20149
  const doc = (0, import_yaml5.parse)(raw);
20146
20150
  if (doc && typeof doc === "object" && !Array.isArray(doc) && "components" in doc) {
20147
20151
  const components = doc.components;
20148
- if (components && typeof components === "object" && !Array.isArray(components) && "hooks" in components) {
20149
- const hooks = components.hooks;
20150
- if (Array.isArray(hooks) && hooks.every((h) => typeof h === "string")) {
20151
- return hooks;
20152
+ if (components && typeof components === "object" && !Array.isArray(components) && category in components) {
20153
+ const names = components[category];
20154
+ if (Array.isArray(names) && names.every((n7) => typeof n7 === "string")) {
20155
+ return names;
20152
20156
  }
20153
20157
  }
20154
20158
  }
@@ -20156,6 +20160,12 @@ function resolveKitHookNames(sourceRoot) {
20156
20160
  }
20157
20161
  return void 0;
20158
20162
  }
20163
+ function resolveKitHookNames(sourceRoot) {
20164
+ return resolveKitComponentNames(sourceRoot, "hooks");
20165
+ }
20166
+ function resolveKitSkillNames(sourceRoot) {
20167
+ return resolveKitComponentNames(sourceRoot, "skills");
20168
+ }
20159
20169
  function resolveHooksSourceFile(sourceRoot, config) {
20160
20170
  return path16.resolve(sourceRoot, config.source.hooks, "hooks.json");
20161
20171
  }
@@ -26497,6 +26507,7 @@ function installForTool(tool, config, kitInfo, manifest, options, mappings, logg
26497
26507
  );
26498
26508
  const claudeRoots = scope === "global" && mappings ? resolveClaudeGlobalRoots({
26499
26509
  flag: options.claudeConfigDir,
26510
+ env: process.env.TANGYR_CLAUDE_CONFIG_DIRS,
26500
26511
  config: config.claudeCodeGlobalPaths
26501
26512
  }) : void 0;
26502
26513
  let count = 0;
@@ -28414,12 +28425,6 @@ function buildDirectComponents(profile) {
28414
28425
  destination: profile.agents,
28415
28426
  type: "dir"
28416
28427
  },
28417
- {
28418
- component: "skills",
28419
- sourceKey: "skills",
28420
- destination: profile.skills,
28421
- type: "dir"
28422
- },
28423
28428
  {
28424
28429
  component: "commands",
28425
28430
  sourceKey: "commands",
@@ -28485,6 +28490,19 @@ function syncClaudeCode(config, sourceRoot, options, logger, mappings, manifest,
28485
28490
  )
28486
28491
  );
28487
28492
  }
28493
+ if (!shouldSkipComponent6(options, "skills")) {
28494
+ outcomes.push(
28495
+ ...syncClaudeCodeSkills(
28496
+ config,
28497
+ sourceRoot,
28498
+ options,
28499
+ logger,
28500
+ profile.skills,
28501
+ manifest,
28502
+ scopePath
28503
+ )
28504
+ );
28505
+ }
28488
28506
  if (!shouldSkipComponent6(options, "instructions") && manifest && scopePath) {
28489
28507
  const shimPath = profile.instructions_shim;
28490
28508
  const agentsMdPath = path36.resolve(sourceRoot, config.source.instructions);
@@ -28657,6 +28675,128 @@ function syncDirectComponent(config, sourceRoot, options, logger, directComponen
28657
28675
  path: destination
28658
28676
  };
28659
28677
  }
28678
+ function syncClaudeCodeSkills(config, sourceRoot, options, logger, destination, manifest, scopePath) {
28679
+ const source = path36.resolve(sourceRoot, config.source.skills);
28680
+ if (!fs34.existsSync(source)) {
28681
+ return [skippedMissingSource6("skills", source)];
28682
+ }
28683
+ const declaredNames = resolveKitSkillNames(sourceRoot);
28684
+ const isLegacyWholesaleSymlink = isSymlinkPath(destination);
28685
+ if (options.dryRun) {
28686
+ const outcomes2 = [];
28687
+ if (isLegacyWholesaleSymlink) {
28688
+ for (const name of foreignSkillNames(source, declaredNames)) {
28689
+ logger.info(
28690
+ `[dry-run] skills: migrate foreign skill ${path36.join(source, name)} -> ${path36.join(destination, name)}`
28691
+ );
28692
+ }
28693
+ logger.info(
28694
+ `[dry-run] skills: replace wholesale symlink with real directory ${destination}`
28695
+ );
28696
+ }
28697
+ for (const name of declaredNames ?? listSkillDirNames(source)) {
28698
+ const skillDestination = path36.join(destination, name);
28699
+ logger.info(
28700
+ `[dry-run] skills: symlink ${path36.join(source, name)} -> ${skillDestination}`
28701
+ );
28702
+ outcomes2.push({
28703
+ component: "skills",
28704
+ status: "dry-run",
28705
+ path: skillDestination
28706
+ });
28707
+ }
28708
+ return outcomes2;
28709
+ }
28710
+ if (isLegacyWholesaleSymlink) {
28711
+ migrateWholesaleSkillsSymlink(source, destination, declaredNames, logger);
28712
+ }
28713
+ ensureDir(destination);
28714
+ const outcomes = [];
28715
+ for (const name of declaredNames ?? listSkillDirNames(source)) {
28716
+ outcomes.push(
28717
+ syncSingleSkill(
28718
+ config,
28719
+ sourceRoot,
28720
+ options,
28721
+ logger,
28722
+ source,
28723
+ destination,
28724
+ name,
28725
+ manifest,
28726
+ scopePath
28727
+ )
28728
+ );
28729
+ }
28730
+ return outcomes;
28731
+ }
28732
+ function syncSingleSkill(config, sourceRoot, options, logger, source, destination, name, manifest, scopePath) {
28733
+ const skillSource = path36.join(source, name);
28734
+ const skillDestination = path36.join(destination, name);
28735
+ if (!fs34.existsSync(skillSource)) {
28736
+ return skippedMissingSource6("skills", skillSource);
28737
+ }
28738
+ const artifactClass = classifyArtifact(skillDestination, config, sourceRoot);
28739
+ if (artifactClass === "managed-symlink" && isSymlinkTo6(skillDestination, skillSource)) {
28740
+ return {
28741
+ component: "skills",
28742
+ status: "skipped-current",
28743
+ path: skillDestination
28744
+ };
28745
+ }
28746
+ if (artifactClass === "managed-stale" && isOurStaleSymlink(skillDestination, /* @__PURE__ */ new Set([skillDestination]), config)) {
28747
+ fs34.unlinkSync(skillDestination);
28748
+ } else if (artifactClass === "unmanaged-conflict" && !handleConflict6(
28749
+ skillDestination,
28750
+ config,
28751
+ options,
28752
+ logger,
28753
+ manifest,
28754
+ scopePath
28755
+ )) {
28756
+ return { component: "skills", status: "conflict", path: skillDestination };
28757
+ } else if (artifactClass === "managed-symlink") {
28758
+ fs34.unlinkSync(skillDestination);
28759
+ }
28760
+ createRelativeSymlink(skillSource, skillDestination, "dir");
28761
+ return { component: "skills", status: "success", path: skillDestination };
28762
+ }
28763
+ function migrateWholesaleSkillsSymlink(source, destination, declaredNames, logger) {
28764
+ const foreignNames = foreignSkillNames(source, declaredNames);
28765
+ fs34.unlinkSync(destination);
28766
+ ensureDir(destination);
28767
+ for (const name of foreignNames) {
28768
+ const from = path36.join(source, name);
28769
+ const to = path36.join(destination, name);
28770
+ if (fs34.existsSync(to)) {
28771
+ logger.warn(
28772
+ `skills migration: ${to} already exists \u2014 skipping move of ${from} to avoid overwrite`
28773
+ );
28774
+ continue;
28775
+ }
28776
+ fs34.renameSync(from, to);
28777
+ logger.info(`skills migration: moved ${from} -> ${to}`);
28778
+ }
28779
+ }
28780
+ function foreignSkillNames(source, declaredNames) {
28781
+ if (!declaredNames) {
28782
+ return [];
28783
+ }
28784
+ const declaredSet = new Set(declaredNames);
28785
+ return listSkillDirNames(source).filter((name) => !declaredSet.has(name));
28786
+ }
28787
+ function listSkillDirNames(dir) {
28788
+ if (!fs34.existsSync(dir)) {
28789
+ return [];
28790
+ }
28791
+ return fs34.readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => entry.name);
28792
+ }
28793
+ function isSymlinkPath(filePath) {
28794
+ try {
28795
+ return fs34.lstatSync(filePath).isSymbolicLink();
28796
+ } catch {
28797
+ return false;
28798
+ }
28799
+ }
28660
28800
  function syncHookScriptsBridge(config, sourceRoot, options, logger, destination, manifest, scopePath) {
28661
28801
  const source = path36.resolve(sourceRoot, config.source.hooks);
28662
28802
  if (!fs34.existsSync(source)) {
@@ -28869,6 +29009,7 @@ function runSyncCommand(options, logger) {
28869
29009
  const allMappings = mappings ?? (targets.includes("claude-code") ? loadMappings(sourceRoot, config, configDir) : void 0);
28870
29010
  const claudeRoots = targets.includes("claude-code") && syncOptions.scope === "global" && allMappings ? resolveClaudeGlobalRoots({
28871
29011
  flag: options.claudeConfigDir,
29012
+ env: process.env.TANGYR_CLAUDE_CONFIG_DIRS,
28872
29013
  config: config.claudeCodeGlobalPaths
28873
29014
  }) : void 0;
28874
29015
  for (const target of targets) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alessandroraffa/tangyr",
3
- "version": "0.10.1",
3
+ "version": "0.11.1",
4
4
  "description": "CLI for the Tangyr discipline — install and manage operating kits for AI coding tools",
5
5
  "license": "MIT",
6
6
  "type": "module",