@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.
- package/README.md +13 -2
- package/dist/index.js +153 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -107,13 +107,24 @@ claudeCodeGlobalPaths:
|
|
|
107
107
|
- ~/.claude-work
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
-
**Via
|
|
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
|
|
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
|
|
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) &&
|
|
20149
|
-
const
|
|
20150
|
-
if (Array.isArray(
|
|
20151
|
-
return
|
|
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) {
|