devlyn-cli 4.2.7 → 4.2.8
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 +1 -1
- package/bin/devlyn.js +84 -28
- package/bin/skill-ownership.js +10 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ That's it. The installer asks:
|
|
|
35
35
|
|
|
36
36
|
In a project, AGENTS.md readers load the skills from `.agents/skills/`; Claude Code loads only `.claude/skills/` and reads AGENTS.md when the project has no CLAUDE.md; a CLAUDE.md the installer creates imports an existing AGENTS.md (`@AGENTS.md`). Global installs skills only: `~/.agents/skills/` (omp, Pi, Grok) and `~/.codex/skills/` (Codex), plus `~/.claude/skills/` for Claude Code. Every target gets the `devlyn-ideate` skill and the `devlyn-engines` utility. In Codex / omp / Pi, invoke them as skills (`$devlyn-ideate`); in Claude Code and Grok they're slash commands (`/devlyn-ideate`). Rerunning refreshes skills and the managed instruction block while preserving project rules outside it. See [Migration from earlier versions](#migration-from-earlier-versions) for legacy migration and merge recovery.
|
|
37
37
|
|
|
38
|
-
Without prompts, `npx devlyn-cli -y` installs AGENTS.md + `.agents/skills/` plus every target this project already has; add `--claude` for Claude Code. With `--global` it installs for every project on this machine, plus `~/.claude/skills/` with `--claude` or when it already has a devlyn install.
|
|
38
|
+
Without prompts, `npx devlyn-cli -y` installs AGENTS.md + `.agents/skills/` plus every target this project already has; add `--claude` for Claude Code. With `--global` it installs for every project on this machine, plus `~/.claude/skills/` with `--claude` or when it already has a devlyn install. After a project install or update, the installer names the changed files to commit so teammates and `/devlyn-ideate` drains can use them.
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
package/bin/devlyn.js
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const os = require('os');
|
|
6
|
-
const { execSync } = require('child_process');
|
|
6
|
+
const { execSync, spawnSync } = require('child_process');
|
|
7
7
|
|
|
8
8
|
const CONFIG_SOURCE = path.join(__dirname, '..', 'config');
|
|
9
9
|
const OPTIONAL_SKILLS_SOURCE = path.join(__dirname, '..', 'optional-skills');
|
|
10
10
|
const PKG = require('../package.json');
|
|
11
11
|
const { updateInstructions, InstructionError, holdsDevlynDefaults, importsAgentsMd } = require('./instructions');
|
|
12
|
-
const { contentHash, fingerprint, isIgnoredEntry, trackedPaths } = require('./skill-ownership');
|
|
12
|
+
const { contentHash, fingerprint, gitEnvironment, isIgnoredEntry, trackedPaths } = require('./skill-ownership');
|
|
13
13
|
const SKILL_HISTORY = require('./skill-history.json');
|
|
14
14
|
|
|
15
15
|
// The devlyn skill bundle installed into every skill-capable agent's loader
|
|
@@ -346,13 +346,14 @@ function listContents() {
|
|
|
346
346
|
}
|
|
347
347
|
|
|
348
348
|
// Old and retired skills in one skill root; nothing outside it.
|
|
349
|
-
function cleanupDeprecated(skillsDir) {
|
|
349
|
+
function cleanupDeprecated(skillsDir, written) {
|
|
350
350
|
let removed = 0;
|
|
351
351
|
const deprecated = DEPRECATED_DIRS.flatMap((relPath) =>
|
|
352
352
|
legacySkillPaths(skillsDir, path.basename(relPath)).map((fullPath) => [relPath, fullPath]));
|
|
353
353
|
for (const [relPath, fullPath] of deprecated) {
|
|
354
354
|
if (fs.existsSync(fullPath)) {
|
|
355
355
|
fs.rmSync(fullPath, { recursive: true });
|
|
356
|
+
written.push(fullPath);
|
|
356
357
|
log(` ✕ ${relPath}/ (removed)`, 'dim');
|
|
357
358
|
removed++;
|
|
358
359
|
}
|
|
@@ -361,6 +362,7 @@ function cleanupDeprecated(skillsDir) {
|
|
|
361
362
|
const fullPath = path.join(skillsDir, name);
|
|
362
363
|
if (isShippedCopy(fullPath, hashes)) {
|
|
363
364
|
fs.rmSync(fullPath, { recursive: true });
|
|
365
|
+
written.push(fullPath);
|
|
364
366
|
log(` ✕ skills/${name}/ (removed)`, 'dim');
|
|
365
367
|
removed++;
|
|
366
368
|
}
|
|
@@ -372,20 +374,22 @@ function cleanupDeprecated(skillsDir) {
|
|
|
372
374
|
// are in place. An optional skill found under an old spelling is installed under its new name
|
|
373
375
|
// before the old copy goes, so an interrupted migration never loses it. An unedited 0.6.0-0.7.1
|
|
374
376
|
// copy is refreshed in place. Any other folder under a 4.0 name is the user's own; `-y` leaves it.
|
|
375
|
-
function retireRenamedSkills(skillsDir) {
|
|
377
|
+
function retireRenamedSkills(skillsDir, written) {
|
|
376
378
|
const optional = new Set(OPTIONAL_ADDONS.filter((addon) => addon.type === 'local').map((addon) => addon.name));
|
|
377
379
|
const refreshed = [];
|
|
378
380
|
for (const [oldName, newName] of Object.entries(RENAMED_SKILLS)) {
|
|
379
381
|
const found = legacySkillPaths(skillsDir, oldName).filter((fullPath) => fs.existsSync(fullPath));
|
|
380
382
|
if (found.length > 0 && optional.has(newName)) {
|
|
381
|
-
installOptionalSkillInto(skillsDir, newName);
|
|
383
|
+
installOptionalSkillInto(skillsDir, newName, written);
|
|
382
384
|
refreshed.push(newName);
|
|
383
385
|
} else if (isShippedCopy(path.join(skillsDir, newName), PRE_STANDARD_SKILL_MD_SHA256)) {
|
|
384
386
|
refreshPreStandardCopy(skillsDir, newName);
|
|
387
|
+
written.push(path.join(skillsDir, newName));
|
|
385
388
|
refreshed.push(newName);
|
|
386
389
|
}
|
|
387
390
|
for (const fullPath of found) {
|
|
388
391
|
fs.rmSync(fullPath, { recursive: true, force: true });
|
|
392
|
+
written.push(fullPath);
|
|
389
393
|
log(` ✕ ${path.basename(fullPath)}/ (renamed to ${newName})`, 'dim');
|
|
390
394
|
}
|
|
391
395
|
}
|
|
@@ -610,30 +614,34 @@ function singleSelect(message, items, initial) {
|
|
|
610
614
|
});
|
|
611
615
|
}
|
|
612
616
|
|
|
613
|
-
// After
|
|
614
|
-
function installLocalSkill(skillName, roots) {
|
|
617
|
+
// After preflight has cleared every selected addon before any change.
|
|
618
|
+
function installLocalSkill(skillName, roots, written = []) {
|
|
615
619
|
if (!fs.existsSync(path.join(OPTIONAL_SKILLS_SOURCE, skillName))) {
|
|
616
620
|
log(` ⚠️ Skill "${skillName}" not found`, 'yellow');
|
|
617
621
|
return false;
|
|
618
622
|
}
|
|
619
623
|
log(`\n🛠️ Installing ${skillName}...`, 'cyan');
|
|
620
624
|
for (const root of roots) {
|
|
621
|
-
installOptionalSkillInto(root, skillName);
|
|
625
|
+
installOptionalSkillInto(root, skillName, written);
|
|
622
626
|
writeInstallMarker(root, [skillName], readInstallManifest(root));
|
|
627
|
+
written.push(path.join(root, DEVLYN_INSTALL_MARKER));
|
|
623
628
|
}
|
|
624
629
|
return true;
|
|
625
630
|
}
|
|
626
631
|
|
|
627
632
|
// One optional skill into one skill-loader directory, replacing any older copy of it. A
|
|
628
633
|
// copy under its name before the 4.0.0 rename goes only once the new one is complete.
|
|
629
|
-
function installOptionalSkillInto(target, skillName) {
|
|
634
|
+
function installOptionalSkillInto(target, skillName, written) {
|
|
630
635
|
const dest = path.join(target, skillName);
|
|
631
636
|
fs.rmSync(dest, { recursive: true, force: true });
|
|
632
637
|
copyRecursive(path.join(OPTIONAL_SKILLS_SOURCE, skillName), dest, target);
|
|
633
638
|
assertCompleteSkillInstall(OPTIONAL_SKILLS_SOURCE, target, [skillName]);
|
|
639
|
+
written.push(dest);
|
|
634
640
|
const oldName = Object.keys(RENAMED_SKILLS).find((name) => RENAMED_SKILLS[name] === skillName);
|
|
635
641
|
for (const fullPath of oldName ? legacySkillPaths(target, oldName) : []) {
|
|
642
|
+
if (!fs.existsSync(fullPath)) continue;
|
|
636
643
|
fs.rmSync(fullPath, { recursive: true, force: true });
|
|
644
|
+
written.push(fullPath);
|
|
637
645
|
}
|
|
638
646
|
}
|
|
639
647
|
|
|
@@ -679,10 +687,7 @@ function installSkillPack(packName) {
|
|
|
679
687
|
}
|
|
680
688
|
}
|
|
681
689
|
|
|
682
|
-
function installAddon(addon
|
|
683
|
-
if (addon.type === 'local') {
|
|
684
|
-
return installLocalSkill(addon.name, roots);
|
|
685
|
-
}
|
|
690
|
+
function installAddon(addon) {
|
|
686
691
|
if (addon.type === 'mcp') {
|
|
687
692
|
return installMcpServer(addon.name, addon.command);
|
|
688
693
|
}
|
|
@@ -691,13 +696,13 @@ function installAddon(addon, roots) {
|
|
|
691
696
|
|
|
692
697
|
// The core skills into one skill root, replacing older copies and retired skills, then the
|
|
693
698
|
// marker that records a complete install.
|
|
694
|
-
function installCoreSkills(skillsDir) {
|
|
699
|
+
function installCoreSkills(skillsDir, written = []) {
|
|
695
700
|
const sourceSkillsDir = path.join(CONFIG_SOURCE, 'skills');
|
|
696
701
|
log(`\n📁 Installing devlyn skills to ${skillsDir.replace(os.homedir(), '~')}`, 'green');
|
|
697
702
|
fs.mkdirSync(skillsDir, { recursive: true });
|
|
698
703
|
const previous = readInstallManifest(skillsDir);
|
|
699
704
|
clearInstallMarker(skillsDir);
|
|
700
|
-
const removed = cleanupDeprecated(skillsDir);
|
|
705
|
+
const removed = cleanupDeprecated(skillsDir, written);
|
|
701
706
|
if (removed > 0) {
|
|
702
707
|
log(`\n🧹 Cleaned up ${removed} deprecated file${removed > 1 ? 's' : ''}`, 'yellow');
|
|
703
708
|
}
|
|
@@ -708,10 +713,12 @@ function installCoreSkills(skillsDir) {
|
|
|
708
713
|
// Full replace: copyRecursive is an overlay, so stale files would otherwise persist.
|
|
709
714
|
fs.rmSync(dest, { recursive: true, force: true });
|
|
710
715
|
copyRecursive(src, dest, skillsDir);
|
|
716
|
+
written.push(dest);
|
|
711
717
|
}
|
|
712
718
|
assertCompleteSkillInstall(sourceSkillsDir, skillsDir, DEVLYN_CORE_SKILLS);
|
|
713
|
-
const refreshed = retireRenamedSkills(skillsDir);
|
|
719
|
+
const refreshed = retireRenamedSkills(skillsDir, written);
|
|
714
720
|
writeInstallMarker(skillsDir, [...DEVLYN_CORE_SKILLS, ...refreshed], previous);
|
|
721
|
+
written.push(path.join(skillsDir, DEVLYN_INSTALL_MARKER));
|
|
715
722
|
}
|
|
716
723
|
|
|
717
724
|
function readProjectGitignore() {
|
|
@@ -763,7 +770,9 @@ function updateProjectGitignore(markerEntry) {
|
|
|
763
770
|
if (!gitignoreContent.equals(original)) {
|
|
764
771
|
fs.writeFileSync(gitignorePath, gitignoreContent);
|
|
765
772
|
log(' → .gitignore (updated project install rules)', 'dim');
|
|
773
|
+
return true;
|
|
766
774
|
}
|
|
775
|
+
return false;
|
|
767
776
|
}
|
|
768
777
|
|
|
769
778
|
// With the Claude target in the same run, an AGENTS.md that links to this project's CLAUDE.md (a
|
|
@@ -784,16 +793,18 @@ function agentsMdIsClaudeMd() {
|
|
|
784
793
|
}
|
|
785
794
|
|
|
786
795
|
function installAgentsProject(withClaude) {
|
|
796
|
+
const written = [];
|
|
787
797
|
if (!agentsMdIsClaudeMd()) {
|
|
788
|
-
updateInstructions('AGENTS.md');
|
|
798
|
+
if (updateInstructions('AGENTS.md')) written.push('AGENTS.md');
|
|
789
799
|
} else if (withClaude) {
|
|
790
800
|
log(' → AGENTS.md is CLAUDE.md here, so it gets the devlyn block written there', 'dim');
|
|
791
801
|
} else {
|
|
792
802
|
throw new InstructionError('AGENTS.md is CLAUDE.md under another name here. Choose CLAUDE.md as well '
|
|
793
803
|
+ '(npx devlyn-cli -y --claude) so the devlyn block is written once, into CLAUDE.md.');
|
|
794
804
|
}
|
|
795
|
-
installCoreSkills(skillRoots('agents', false)[0]);
|
|
796
|
-
updateProjectGitignore('.agents/skills/.devlyn-install.json');
|
|
805
|
+
installCoreSkills(skillRoots('agents', false)[0], written);
|
|
806
|
+
if (updateProjectGitignore('.agents/skills/.devlyn-install.json')) written.push('.gitignore');
|
|
807
|
+
return written;
|
|
797
808
|
}
|
|
798
809
|
|
|
799
810
|
// Whether Claude Code reads the devlyn defaults of AGENTS.md through an `@AGENTS.md` import in this CLAUDE.md text.
|
|
@@ -817,8 +828,9 @@ function installClaudeInstructions() {
|
|
|
817
828
|
throw new InstructionError('Claude Code reads AGENTS.md here, and its devlyn defaults need an update. Codex, omp, Pi and Grok '
|
|
818
829
|
+ 'read them too, with .agents/skills, so choose AGENTS.md as well (npx devlyn-cli -y --claude) to update both together.');
|
|
819
830
|
}
|
|
820
|
-
updateInstructions('CLAUDE.md', initial, !imported);
|
|
831
|
+
const changed = updateInstructions('CLAUDE.md', initial, !imported);
|
|
821
832
|
if (imported) log(' → CLAUDE.md imports AGENTS.md, which holds the devlyn block', 'dim');
|
|
833
|
+
return changed;
|
|
822
834
|
}
|
|
823
835
|
|
|
824
836
|
// Project CLAUDE.md and .claude/: skills and settings.
|
|
@@ -845,16 +857,17 @@ function installClaudeCore() {
|
|
|
845
857
|
&& (!settings.env || typeof settings.env !== 'object' || Array.isArray(settings.env))) {
|
|
846
858
|
throw unmergeable('env must be a JSON object');
|
|
847
859
|
}
|
|
848
|
-
installClaudeInstructions();
|
|
860
|
+
const written = installClaudeInstructions() ? ['CLAUDE.md'] : [];
|
|
849
861
|
for (const relPath of DEPRECATED_FILES) {
|
|
850
862
|
const fullPath = path.join(targetDir, relPath);
|
|
851
863
|
if (fs.existsSync(fullPath)) {
|
|
852
864
|
fs.unlinkSync(fullPath);
|
|
853
865
|
log(` ✕ ${relPath} (deprecated)`, 'dim');
|
|
866
|
+
written.push(`.claude/${relPath}`);
|
|
854
867
|
}
|
|
855
868
|
}
|
|
856
|
-
installCoreSkills(skillsDir);
|
|
857
|
-
updateProjectGitignore('.claude/skills/.devlyn-install.json');
|
|
869
|
+
installCoreSkills(skillsDir, written);
|
|
870
|
+
if (updateProjectGitignore('.claude/skills/.devlyn-install.json')) written.push('.gitignore');
|
|
858
871
|
|
|
859
872
|
// One-hour foreground Bash calls and one-hour prompt caching serve a drain host that waits in the foreground.
|
|
860
873
|
let settingsChanged = false;
|
|
@@ -885,6 +898,7 @@ function installClaudeCore() {
|
|
|
885
898
|
if (settingsChanged) {
|
|
886
899
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
887
900
|
log(' → settings.json', 'dim');
|
|
901
|
+
written.push('.claude/settings.json');
|
|
888
902
|
}
|
|
889
903
|
// Their ownership cannot be established, so the other retired settings stay; name the ones present.
|
|
890
904
|
const retired = [
|
|
@@ -894,6 +908,7 @@ function installClaudeCore() {
|
|
|
894
908
|
if (retired.length > 0) {
|
|
895
909
|
log(` Retired devlyn settings may remain in .claude/settings.json: ${retired.join(', ')} — delete those you did not add yourself.`, 'yellow');
|
|
896
910
|
}
|
|
911
|
+
return written;
|
|
897
912
|
}
|
|
898
913
|
|
|
899
914
|
// Installs the targets in one scope; returns the skill roots written.
|
|
@@ -908,21 +923,62 @@ function install(targets, global, optionalNames = []) {
|
|
|
908
923
|
+ 'would apply to every project. Run from a project folder, or use --global for skills only.');
|
|
909
924
|
}
|
|
910
925
|
const roots = targets.flatMap((target) => skillRoots(target, global));
|
|
926
|
+
const written = [];
|
|
911
927
|
if (!global) readProjectGitignore();
|
|
912
928
|
preflightSkills(roots, optionalNames,
|
|
913
929
|
!global && targets.includes('claude') ? path.dirname(skillRoots('claude', false)[0]) : null);
|
|
914
930
|
// AGENTS.md first, whatever order the menu gave: a new CLAUDE.md holds the block only when AGENTS.md does not.
|
|
915
931
|
for (const target of ['agents', 'claude'].filter((name) => targets.includes(name))) {
|
|
916
932
|
if (global) skillRoots(target, true).forEach((root) => installCoreSkills(root));
|
|
917
|
-
else if (target === 'agents') installAgentsProject(targets.includes('claude'));
|
|
918
|
-
else installClaudeCore();
|
|
933
|
+
else if (target === 'agents') written.push(...installAgentsProject(targets.includes('claude')));
|
|
934
|
+
else written.push(...installClaudeCore());
|
|
919
935
|
}
|
|
936
|
+
for (const name of optionalNames) installLocalSkill(name, roots, written);
|
|
920
937
|
log(`\n✅ devlyn ${PKG.version} installed`, 'green');
|
|
921
938
|
if (global) log('The principles block installs per project: run npx devlyn-cli in the project without --global.');
|
|
939
|
+
else noticeProjectChanges(written);
|
|
922
940
|
noticeGlobalDrift(roots);
|
|
923
941
|
return roots;
|
|
924
942
|
}
|
|
925
943
|
|
|
944
|
+
// Advisory only: a successful install stays successful when Git is unavailable or fails.
|
|
945
|
+
function noticeProjectChanges(paths) {
|
|
946
|
+
if (!paths.length) return;
|
|
947
|
+
const cwd = projectDir();
|
|
948
|
+
paths = [...new Set(paths.map((name) => path.relative(cwd, path.resolve(cwd, name)).split(path.sep).join('/')))];
|
|
949
|
+
const env = { ...gitEnvironment(), GIT_OPTIONAL_LOCKS: '0' };
|
|
950
|
+
const git = (args) => spawnSync('git', ['-C', cwd, ...args], { encoding: 'utf8', env });
|
|
951
|
+
const repo = git(['rev-parse', '--show-toplevel']);
|
|
952
|
+
if (repo.error || repo.status !== 0) return;
|
|
953
|
+
const status = git(['status', '--porcelain=v1', '-z', '--untracked-files=all', '--no-renames',
|
|
954
|
+
'--', ...paths.map((name) => `:(icase,literal)${name}`)]);
|
|
955
|
+
if (status.error || status.status !== 0) return;
|
|
956
|
+
// Porcelain paths are repository-relative, even when installing in a nested project.
|
|
957
|
+
const changed = status.stdout.split('\0').filter(Boolean).map((entry) =>
|
|
958
|
+
path.relative(cwd, path.resolve(repo.stdout.replace(/\r?\n$/, ''), entry.slice(3))).split(path.sep).join('/'));
|
|
959
|
+
// Keep the installed folder/file scope, using Git's spelling on case-insensitive volumes.
|
|
960
|
+
const pending = [...new Set(paths.flatMap((name) => [...new Set(changed
|
|
961
|
+
.map((file) => file.split('/').slice(0, name.split('/').length).join('/')))]
|
|
962
|
+
.filter((file) => {
|
|
963
|
+
if (file === name) return true;
|
|
964
|
+
if (file.toLowerCase() !== name.toLowerCase()) return false;
|
|
965
|
+
// Case folding alone can include independently edited files on case-sensitive volumes.
|
|
966
|
+
// Removed paths have no identity left to compare, so require their exact spelling.
|
|
967
|
+
try {
|
|
968
|
+
const written = fs.lstatSync(path.resolve(cwd, name), { bigint: true, throwIfNoEntry: false });
|
|
969
|
+
const reported = fs.lstatSync(path.resolve(cwd, file), { bigint: true, throwIfNoEntry: false });
|
|
970
|
+
return written && reported && written.dev === reported.dev && written.ino === reported.ino;
|
|
971
|
+
} catch (error) {
|
|
972
|
+
log(`Cannot compare Git path ${file} with installed path ${name}: ${error.message}`, 'yellow');
|
|
973
|
+
return false;
|
|
974
|
+
}
|
|
975
|
+
})))];
|
|
976
|
+
if (!pending.length) return;
|
|
977
|
+
const quote = (name) => /^[a-zA-Z0-9_./-]+$/.test(name) ? name : `'${name.replace(/'/g, "'\\''")}'`;
|
|
978
|
+
log('Commit these changes for teammates and /devlyn-ideate drains to use them:', 'yellow');
|
|
979
|
+
log(` git add -- ${pending.map(quote).join(' ')} && git commit -m "chore(devlyn): update devlyn-cli to ${PKG.version}"`, 'dim');
|
|
980
|
+
}
|
|
981
|
+
|
|
926
982
|
// A devlyn install in a user root this run does not refresh drifts and can shadow the
|
|
927
983
|
// project's copy; name it without touching it.
|
|
928
984
|
function noticeGlobalDrift(installed) {
|
|
@@ -982,11 +1038,11 @@ async function init({ yes, claude, global }) {
|
|
|
982
1038
|
const scope = await singleSelect('Where', ['This project', 'Global — every project on this machine'], global ? 1 : 0);
|
|
983
1039
|
// Ask about optional addons (local skills + external packs; MCP servers belong to Claude Code)
|
|
984
1040
|
const selectedAddons = await multiSelect('Optional skills & packs', OPTIONAL_ADDONS.filter((addon) => addon.type !== 'mcp' || targets.includes('claude')));
|
|
985
|
-
|
|
1041
|
+
install(targets, scope === 1, selectedAddons.filter((addon) => addon.type === 'local').map((addon) => addon.name));
|
|
986
1042
|
|
|
987
1043
|
if (selectedAddons.length > 0) {
|
|
988
|
-
for (const addon of selectedAddons) {
|
|
989
|
-
installAddon(addon
|
|
1044
|
+
for (const addon of selectedAddons.filter((addon) => addon.type !== 'local')) {
|
|
1045
|
+
installAddon(addon);
|
|
990
1046
|
}
|
|
991
1047
|
} else {
|
|
992
1048
|
log('💡 No optional addons selected', 'dim');
|
package/bin/skill-ownership.js
CHANGED
|
@@ -52,15 +52,20 @@ function fingerprint(dir) {
|
|
|
52
52
|
return crypto.createHash('sha256').update(JSON.stringify(entries)).digest('hex');
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
function gitEnvironment() {
|
|
56
|
+
const env = { ...process.env, LC_ALL: 'C' };
|
|
57
|
+
for (const key of ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_COMMON_DIR',
|
|
58
|
+
'GIT_LITERAL_PATHSPECS', 'GIT_GLOB_PATHSPECS', 'GIT_NOGLOB_PATHSPECS', 'GIT_ICASE_PATHSPECS',
|
|
59
|
+
'GIT_CEILING_DIRECTORIES', 'GIT_DISCOVERY_ACROSS_FILESYSTEM']) delete env[key];
|
|
60
|
+
return env;
|
|
61
|
+
}
|
|
62
|
+
|
|
55
63
|
// Use the index, not status: even files deleted locally need ownership checks.
|
|
56
64
|
// Inspect the repository containing this root, including global dotfiles repos.
|
|
57
65
|
function trackedPaths(root) {
|
|
58
66
|
let cwd = root;
|
|
59
67
|
while (!fs.existsSync(cwd)) cwd = path.dirname(cwd);
|
|
60
|
-
const env =
|
|
61
|
-
for (const key of ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_COMMON_DIR',
|
|
62
|
-
'GIT_LITERAL_PATHSPECS', 'GIT_GLOB_PATHSPECS', 'GIT_NOGLOB_PATHSPECS', 'GIT_ICASE_PATHSPECS',
|
|
63
|
-
'GIT_CEILING_DIRECTORIES', 'GIT_DISCOVERY_ACROSS_FILESYSTEM']) delete env[key];
|
|
68
|
+
const env = gitEnvironment();
|
|
64
69
|
const git = (args) => spawnSync('git', ['-C', cwd, ...args], { encoding: 'utf8', env });
|
|
65
70
|
const repo = git(['rev-parse', '--show-toplevel']);
|
|
66
71
|
if (repo.error?.code === 'ENOENT') return [];
|
|
@@ -76,4 +81,4 @@ function trackedPaths(root) {
|
|
|
76
81
|
return files.stdout.split('\0').filter(Boolean).map((file) => path.resolve(repo.stdout.trim(), file));
|
|
77
82
|
}
|
|
78
83
|
|
|
79
|
-
module.exports = { contentHash, fingerprint, isIgnoredEntry, trackedPaths };
|
|
84
|
+
module.exports = { contentHash, fingerprint, gitEnvironment, isIgnoredEntry, trackedPaths };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devlyn-cli",
|
|
3
|
-
"version": "4.2.
|
|
3
|
+
"version": "4.2.8",
|
|
4
4
|
"description": "Loop & harness engineering for AI coding agents (Claude Code, Codex, and any AGENTS.md-driven agent) — core engineering principles for CLAUDE.md/AGENTS.md and a loop designer that splits one intent into tasks agents drain hands-free",
|
|
5
5
|
"homepage": "https://github.com/fysoul17/devlyn-cli#readme",
|
|
6
6
|
"bin": {
|