devlyn-cli 4.3.11 → 4.3.13
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/bin/cleanup-history.json +499 -1
- package/bin/devlyn.js +235 -34
- package/bin/skill-history.json +2 -0
- package/native/claude/.claude-plugin/plugin.json +1 -1
- package/native/codex/.agents/plugins/marketplace.json +1 -1
- package/native/codex/plugins/devlyn/.codex-plugin/plugin.json +1 -1
- package/node_modules/jsonc-parser/CHANGELOG.md +76 -0
- package/node_modules/jsonc-parser/LICENSE.md +21 -0
- package/node_modules/jsonc-parser/README.md +364 -0
- package/node_modules/jsonc-parser/SECURITY.md +41 -0
- package/node_modules/jsonc-parser/lib/esm/impl/edit.js +185 -0
- package/node_modules/jsonc-parser/lib/esm/impl/format.js +261 -0
- package/node_modules/jsonc-parser/lib/esm/impl/parser.js +659 -0
- package/node_modules/jsonc-parser/lib/esm/impl/scanner.js +443 -0
- package/node_modules/jsonc-parser/lib/esm/impl/string-intern.js +29 -0
- package/node_modules/jsonc-parser/lib/esm/main.d.ts +351 -0
- package/node_modules/jsonc-parser/lib/esm/main.js +178 -0
- package/node_modules/jsonc-parser/lib/umd/impl/edit.js +201 -0
- package/node_modules/jsonc-parser/lib/umd/impl/format.js +275 -0
- package/node_modules/jsonc-parser/lib/umd/impl/parser.js +682 -0
- package/node_modules/jsonc-parser/lib/umd/impl/scanner.js +456 -0
- package/node_modules/jsonc-parser/lib/umd/impl/string-intern.js +42 -0
- package/node_modules/jsonc-parser/lib/umd/main.d.ts +351 -0
- package/node_modules/jsonc-parser/lib/umd/main.js +194 -0
- package/node_modules/jsonc-parser/package.json +37 -0
- package/package.json +3 -1
- package/release.json +104 -9
package/bin/devlyn.js
CHANGED
|
@@ -5,6 +5,7 @@ const path = require('path');
|
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const crypto = require('crypto');
|
|
7
7
|
const { execSync, spawnSync } = require('child_process');
|
|
8
|
+
const { parseTree, findNodeAtLocation, applyEdits } = require('jsonc-parser');
|
|
8
9
|
|
|
9
10
|
const CONFIG_SOURCE = path.join(__dirname, '..', 'config');
|
|
10
11
|
const OPTIONAL_SKILLS_SOURCE = path.join(__dirname, '..', 'optional-skills');
|
|
@@ -887,8 +888,8 @@ function replaceFile(file, text) {
|
|
|
887
888
|
}
|
|
888
889
|
}
|
|
889
890
|
|
|
890
|
-
function cleanRetiredSettings(settings) {
|
|
891
|
-
let changed =
|
|
891
|
+
function cleanRetiredSettings(settings, hookScripts = []) {
|
|
892
|
+
let changed = cleanJarvisSettings(settings, hookScripts);
|
|
892
893
|
// Remove exactly the retired Stop hook command, and its entry only when nothing else is left in it.
|
|
893
894
|
const retiredHook = (hook) => hook?.type === 'command' && hook.command === RETIRED_STOP_HOOK;
|
|
894
895
|
const stop = Array.isArray(settings.hooks?.Stop) ? settings.hooks.Stop : [];
|
|
@@ -903,6 +904,91 @@ function cleanRetiredSettings(settings) {
|
|
|
903
904
|
return changed;
|
|
904
905
|
}
|
|
905
906
|
|
|
907
|
+
// A hook's event, matcher and complete command object must be shipped bytes (JSON key order aside).
|
|
908
|
+
// Extra owner hooks and wrapper keys survive. Script names alone never authorize a settings edit.
|
|
909
|
+
function cleanJarvisSettings(settings, scripts) {
|
|
910
|
+
let changed = false;
|
|
911
|
+
for (const [event, entries] of Object.entries(settings?.hooks || {})) {
|
|
912
|
+
if (!Array.isArray(entries)) continue;
|
|
913
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
914
|
+
const entry = entries[i];
|
|
915
|
+
if (!Array.isArray(entry?.hooks)) continue;
|
|
916
|
+
const kept = entry.hooks.filter((hook) => {
|
|
917
|
+
const hash = crypto.createHash('sha256').update(cleanupCanonical([event, entry.matcher, hook])).digest('hex');
|
|
918
|
+
return !scripts.some((script) => CLEANUP_HISTORY.hooks[script]?.entries.includes(hash));
|
|
919
|
+
});
|
|
920
|
+
if (kept.length === entry.hooks.length) continue;
|
|
921
|
+
changed = true;
|
|
922
|
+
entry.hooks = kept;
|
|
923
|
+
if (!kept.length && Object.keys(entry).every((key) => ['matcher', 'hooks'].includes(key))) entries.splice(i, 1);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
return changed;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function cleanupHookReferenced(settings, script) {
|
|
930
|
+
// Conservative even for shell wrappers, absolute paths, Windows separators and malformed entries.
|
|
931
|
+
return JSON.stringify(settings).toLowerCase().includes(path.posix.basename(script).toLowerCase());
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
function cleanupReadSettings(base, name = 'settings.json') {
|
|
935
|
+
const file = path.join(base, '.claude', name);
|
|
936
|
+
if (!cleanupRealParents(base, file)) return { safe: false };
|
|
937
|
+
const stat = fs.lstatSync(file, { throwIfNoEntry: false });
|
|
938
|
+
if (!stat) return { safe: true, settings: {} };
|
|
939
|
+
if (!stat.isFile() || stat.nlink !== 1) return { safe: false };
|
|
940
|
+
try {
|
|
941
|
+
const text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(fs.readFileSync(file));
|
|
942
|
+
const settings = JSON.parse(text.replace(/^\uFEFF/, ''));
|
|
943
|
+
const tree = parseTree(text.replace(/^\uFEFF/, ' '));
|
|
944
|
+
const hooks = tree.children?.filter((node) => node.children?.[0]?.value === 'hooks') || [];
|
|
945
|
+
const duplicates = (node) => {
|
|
946
|
+
if (node.type === 'object') {
|
|
947
|
+
const names = node.children.map((property) => property.children[0].value);
|
|
948
|
+
if (new Set(names).size !== names.length) return true;
|
|
949
|
+
}
|
|
950
|
+
return node.children?.some(duplicates) || false;
|
|
951
|
+
};
|
|
952
|
+
// Duplicate owner keys elsewhere survive; duplicate hook paths make a surgical edit ambiguous.
|
|
953
|
+
if (hooks.length > 1 || (hooks.length && duplicates(hooks[0]))) return { safe: false };
|
|
954
|
+
return settings && typeof settings === 'object' && !Array.isArray(settings)
|
|
955
|
+
? { safe: true, settings, text } : { safe: false };
|
|
956
|
+
} catch (error) {
|
|
957
|
+
if (error instanceof SyntaxError || error.code === 'ERR_ENCODING_INVALID_ENCODED_DATA') return { safe: false };
|
|
958
|
+
throw error;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function cleanupSettingsText(text, settings, scripts) {
|
|
963
|
+
const before = Object.entries(settings.hooks || {}).flatMap(([event, entries]) => !Array.isArray(entries) ? []
|
|
964
|
+
: entries.map((entry, index) => ({ event, entries, entry, index, hooks: Array.isArray(entry?.hooks) ? [...entry.hooks] : [] })));
|
|
965
|
+
if (!cleanRetiredSettings(settings, scripts)) return text;
|
|
966
|
+
const removals = [];
|
|
967
|
+
for (const { event, entries, entry, index, hooks } of before.reverse()) {
|
|
968
|
+
const location = ['hooks', event, index];
|
|
969
|
+
if (!entries.includes(entry)) removals.push(location);
|
|
970
|
+
else for (let i = hooks.length - 1; i >= 0; i--) if (!entry.hooks.includes(hooks[i])) removals.push([...location, 'hooks', i]);
|
|
971
|
+
}
|
|
972
|
+
// Only prune containers this cleanup emptied; pre-existing owner empties are not leftovers.
|
|
973
|
+
const emptiedEvents = [...new Set(before.map(({ event }) => event))].filter((event) => settings.hooks[event].length === 0);
|
|
974
|
+
removals.push(...emptiedEvents.map((event) => ['hooks', event]));
|
|
975
|
+
if (emptiedEvents.length === Object.keys(settings.hooks).length) removals.push(['hooks']);
|
|
976
|
+
// Remove array elements and object properties by AST ranges, including the adjacent comma, without serializing owner
|
|
977
|
+
// values. jsonc-parser 3.3.1's modify() truncates a last-element deletion without formatting.
|
|
978
|
+
for (const location of removals) {
|
|
979
|
+
let node = findNodeAtLocation(parseTree(text.replace(/^\uFEFF/, ' ')), location);
|
|
980
|
+
if (node?.parent?.type === 'property') node = node.parent;
|
|
981
|
+
if (!node || !['array', 'object'].includes(node.parent?.type)) throw new Error('Settings edit path changed');
|
|
982
|
+
const siblings = node.parent.children, index = siblings.indexOf(node);
|
|
983
|
+
const previous = siblings[index - 1], next = siblings[index + 1];
|
|
984
|
+
const offset = previous ? previous.offset + previous.length : node.offset;
|
|
985
|
+
const end = !previous && next ? next.offset : node.offset + node.length;
|
|
986
|
+
text = applyEdits(text, [{ offset, length: end - offset, content: '' }]);
|
|
987
|
+
}
|
|
988
|
+
const after = JSON.parse(text.replace(/^\uFEFF/, '')); // Refuse an invalid edit before changing any bytes.
|
|
989
|
+
return Object.keys(after).length === 0 ? null : text;
|
|
990
|
+
}
|
|
991
|
+
|
|
906
992
|
// Shared user roots need one writer from ownership checks through the last addon write.
|
|
907
993
|
// Never reap a foreign lock: PID reuse and slow installs make age/liveness unsafe evidence.
|
|
908
994
|
const heldSkillLocks = new Set();
|
|
@@ -1545,20 +1631,22 @@ function cleanupSkillRoot(base, root, current) {
|
|
|
1545
1631
|
}
|
|
1546
1632
|
|
|
1547
1633
|
function cleanupCommands(base) {
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1634
|
+
return CLEANUP_HISTORY.commandDirectories.flatMap((directory) => {
|
|
1635
|
+
const commands = path.join(base, directory);
|
|
1636
|
+
if (!cleanupRealParents(base, path.join(commands, 'x')) || !fs.lstatSync(commands, { throwIfNoEntry: false })?.isDirectory()) return [];
|
|
1637
|
+
const names = new Set([...DEPRECATED_FILES.map((file) => path.basename(file).toLowerCase()), ...Object.keys(CLEANUP_HISTORY.commands)]);
|
|
1638
|
+
return fs.readdirSync(commands).sort().filter((name) => names.has(name.toLowerCase())
|
|
1639
|
+
&& fs.lstatSync(path.join(commands, name)).isFile())
|
|
1640
|
+
.map((name) => {
|
|
1641
|
+
const file = path.join(commands, name);
|
|
1642
|
+
const proven = CLEANUP_HISTORY.commands[name.toLowerCase()]?.includes(contentHash(file));
|
|
1643
|
+
return { kind: 'legacy-command', path: file, base, action: proven ? 'remove' : 'skip',
|
|
1644
|
+
reasons: proven ? [] : [cleanupReason('modified-copy')] };
|
|
1645
|
+
});
|
|
1646
|
+
});
|
|
1559
1647
|
}
|
|
1560
1648
|
|
|
1561
|
-
function cleanupProjectFiles(base) {
|
|
1649
|
+
function cleanupProjectFiles(base, includedHooks = new Set()) {
|
|
1562
1650
|
const items = [];
|
|
1563
1651
|
const hash = (file) => crypto.createHash('sha256')
|
|
1564
1652
|
.update(fs.readFileSync(file).toString('latin1').replace(/\r\n/g, '\n'), 'latin1').digest('hex');
|
|
@@ -1569,14 +1657,41 @@ function cleanupProjectFiles(base) {
|
|
|
1569
1657
|
}
|
|
1570
1658
|
}
|
|
1571
1659
|
const settingsFile = path.join(base, '.claude', 'settings.json');
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1660
|
+
const { safe, settings } = cleanupReadSettings(base);
|
|
1661
|
+
const localSettings = cleanupReadSettings(base, 'settings.local.json');
|
|
1662
|
+
const hookScripts = [], hookCandidates = [];
|
|
1663
|
+
for (const [name, history] of Object.entries(CLEANUP_HISTORY.hooks)) {
|
|
1664
|
+
const file = path.join(base, name);
|
|
1665
|
+
const stat = fs.lstatSync(file, { throwIfNoEntry: false });
|
|
1666
|
+
if (!stat) continue;
|
|
1667
|
+
const item = { kind: 'legacy-hook', path: file, base, action: 'skip', reasons: [cleanupReason('outside-link')] };
|
|
1668
|
+
items.push(item);
|
|
1669
|
+
if (!cleanupRealParents(base, file) || !stat.isFile() || stat.nlink !== 1) continue;
|
|
1670
|
+
const proven = history.hashes.includes(contentHash(file));
|
|
1671
|
+
Object.assign(item, { action: proven ? 'remove' : 'skip', reasons: proven ? [] : [cleanupReason('modified-copy')] });
|
|
1672
|
+
if (!safe || !localSettings.safe || cleanupHookReferenced(localSettings.settings, name)) {
|
|
1673
|
+
Object.assign(item, { action: 'skip', reasons: [cleanupReason('dependent-copy')] });
|
|
1674
|
+
continue;
|
|
1675
|
+
}
|
|
1676
|
+
if (!cleanupHookReferenced(settings, name)) continue;
|
|
1677
|
+
const after = structuredClone(settings);
|
|
1678
|
+
cleanJarvisSettings(after, [name]);
|
|
1679
|
+
// Unknown commands/matchers/extra command keys retain the script. Routes cannot split a pair:
|
|
1680
|
+
// a PR leaves the caller's tracked copy in place until the base is taken.
|
|
1681
|
+
if (cleanupHookReferenced(after, name) || cleanupTracked(file) !== cleanupTracked(settingsFile)) {
|
|
1682
|
+
Object.assign(item, { action: 'skip', reasons: [cleanupReason('dependent-copy')] });
|
|
1683
|
+
continue;
|
|
1684
|
+
}
|
|
1685
|
+
item.hookSettings = settingsFile;
|
|
1686
|
+
hookCandidates.push(name);
|
|
1687
|
+
if (proven || includedHooks.has(file)) hookScripts.push(name);
|
|
1688
|
+
}
|
|
1689
|
+
const stop = Array.isArray(settings?.hooks?.Stop) ? settings.hooks.Stop : [];
|
|
1690
|
+
const retiredStop = stop.some((entry) => Array.isArray(entry?.hooks) && entry.hooks.some((hook) => hook?.type === 'command' && hook.command === RETIRED_STOP_HOOK));
|
|
1691
|
+
if (safe && (retiredStop || hookCandidates.length)) {
|
|
1692
|
+
const edit = retiredStop || hookScripts.length > 0;
|
|
1693
|
+
items.push({ kind: 'settings-entry', path: settingsFile, base, action: edit ? 'edit' : 'skip',
|
|
1694
|
+
reasons: edit ? [] : [cleanupReason('dependent-copy')], hookScripts, hookCandidates });
|
|
1580
1695
|
}
|
|
1581
1696
|
return items;
|
|
1582
1697
|
}
|
|
@@ -1690,7 +1805,7 @@ function cleanupRetainSupport(items) {
|
|
|
1690
1805
|
return items;
|
|
1691
1806
|
}
|
|
1692
1807
|
|
|
1693
|
-
function cleanupProjectScope(project, current, includeModified = false) {
|
|
1808
|
+
function cleanupProjectScope(project, current, includeModified = false, includedHooks = new Set()) {
|
|
1694
1809
|
const { repositoryIdentity } = require('./harness-status');
|
|
1695
1810
|
const { physicalRoot } = require('./release');
|
|
1696
1811
|
const root = physicalRoot(project);
|
|
@@ -1703,7 +1818,7 @@ function cleanupProjectScope(project, current, includeModified = false) {
|
|
|
1703
1818
|
if (cleanupSameFile(base, os.homedir())) return { ...scope, items: whole('home-folder') };
|
|
1704
1819
|
const items = [
|
|
1705
1820
|
...cleanupSkillRoots(['.agents', '.claude'].map((dir) => [base, path.join(base, dir, 'skills')]), current, includeModified),
|
|
1706
|
-
...cleanupCommands(base), ...cleanupProjectFiles(base), ...cleanupInstructions(base),
|
|
1821
|
+
...cleanupCommands(base), ...cleanupProjectFiles(base, includedHooks), ...cleanupInstructions(base),
|
|
1707
1822
|
];
|
|
1708
1823
|
const busy = identity && items.some((item) => cleanupAction(item, includeModified) !== 'skip') ? cleanupQueueBusy(base) : null;
|
|
1709
1824
|
for (const item of items) {
|
|
@@ -1739,13 +1854,13 @@ function cleanupMachineScope(home, accountHomes, current, includeModified = fals
|
|
|
1739
1854
|
return { kind: 'machine', root: home, name: null, remote: null, items: cleanupRetainSupport(items) };
|
|
1740
1855
|
}
|
|
1741
1856
|
|
|
1742
|
-
function cleanupScopes(inputs, current, includeModified = false) {
|
|
1857
|
+
function cleanupScopes(inputs, current, includeModified = false, includedHooks = new Set()) {
|
|
1743
1858
|
const { sha256 } = require('./release');
|
|
1744
1859
|
const scopes = [];
|
|
1745
1860
|
if (inputs.machine) scopes.push({ id: 'machine', ...cleanupMachineScope(os.homedir(), inputs.accountHomes, current, includeModified) });
|
|
1746
1861
|
for (const project of inputs.projects) {
|
|
1747
1862
|
let scope;
|
|
1748
|
-
try { scope = cleanupProjectScope(project, current, includeModified); }
|
|
1863
|
+
try { scope = cleanupProjectScope(project, current, includeModified, includedHooks); }
|
|
1749
1864
|
catch (error) {
|
|
1750
1865
|
scope = { kind: 'project', root: project, name: path.basename(project), remote: null, items: [{ kind: 'project', path: project, base: project,
|
|
1751
1866
|
relative: '', route: 'local', action: 'skip', reasons: [cleanupReason(error.reason?.code === 'repository-unreadable' ? 'repository-unreadable' : 'project-unreadable', { detail: error.message })] }] };
|
|
@@ -1789,17 +1904,30 @@ function cleanupDigest(plan) {
|
|
|
1789
1904
|
}
|
|
1790
1905
|
|
|
1791
1906
|
// Apply one item a fresh plan proves right now; returns a reason when it is not what was proven.
|
|
1792
|
-
function
|
|
1907
|
+
function cleanupValidateItem(item) {
|
|
1793
1908
|
const now = cleanupPreimage(item.path, Boolean(item.included));
|
|
1794
1909
|
if (now.type !== item.before.type || now.sha256 !== item.before.sha256) return cleanupReason('stale-plan');
|
|
1795
1910
|
if (!cleanupRealParents(item.base, item.path)) return cleanupReason('outside-link');
|
|
1911
|
+
return null;
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
function cleanupApplyItem(item) {
|
|
1915
|
+
const invalid = cleanupValidateItem(item);
|
|
1916
|
+
if (invalid) return invalid;
|
|
1796
1917
|
if (cleanupAction(item) === 'remove') {
|
|
1918
|
+
if (item.kind === 'legacy-hook') {
|
|
1919
|
+
const { safe, settings } = cleanupReadSettings(item.base);
|
|
1920
|
+
if (!safe || cleanupHookReferenced(settings, item.relative)) return cleanupReason('dependent-copy');
|
|
1921
|
+
}
|
|
1797
1922
|
fs.rmSync(item.path, { recursive: true }); // lstat/realParents never follow a link; rm removes the entry itself.
|
|
1798
1923
|
return null;
|
|
1799
1924
|
}
|
|
1800
1925
|
if (item.kind === 'settings-entry') {
|
|
1801
|
-
const settings =
|
|
1802
|
-
if (
|
|
1926
|
+
const { safe, settings, text } = cleanupReadSettings(item.base);
|
|
1927
|
+
if (!safe || text === undefined) return cleanupReason('stale-plan');
|
|
1928
|
+
const after = cleanupSettingsText(text, settings, item.hookScripts);
|
|
1929
|
+
if (after === null) fs.unlinkSync(item.path);
|
|
1930
|
+
else if (after !== text) replaceFile(item.path, after);
|
|
1803
1931
|
return null;
|
|
1804
1932
|
}
|
|
1805
1933
|
if (item.kind === 'instructions') {
|
|
@@ -1822,11 +1950,56 @@ function cleanupProven(reviewed, fresh) {
|
|
|
1822
1950
|
const same = match && match.action === item.action && (!item.included || (cleanupCanInclude(match) && item.before.sha256 !== null))
|
|
1823
1951
|
&& match.relative === item.relative && match.route === item.route
|
|
1824
1952
|
&& match.before.type === item.before.type && match.before.sha256 === item.before.sha256
|
|
1953
|
+
&& cleanupCanonical(match.hookScripts || []) === cleanupCanonical(item.hookScripts || [])
|
|
1825
1954
|
&& !(cleanupSupport(item) && narrowed.has(path.dirname(item.path)));
|
|
1826
1955
|
return { reviewed: item, fresh: same ? { ...match, included: item.included } : null };
|
|
1827
1956
|
});
|
|
1828
1957
|
}
|
|
1829
1958
|
|
|
1959
|
+
// A narrowed or stale review may not split a settings edit from any script it disables.
|
|
1960
|
+
function cleanupCoupledPairs(pairs) {
|
|
1961
|
+
const byPath = new Map(pairs.map((pair) => [pair.reviewed.path, pair]));
|
|
1962
|
+
for (const pair of pairs.filter(({ reviewed }) => reviewed.kind === 'settings-entry')) {
|
|
1963
|
+
const scripts = (pair.reviewed.hookScripts || []).map((name) => byPath.get(path.join(pair.reviewed.base, name)));
|
|
1964
|
+
if (!pair.fresh || scripts.some((script) => !script?.fresh || cleanupAction(script.fresh) !== 'remove')) {
|
|
1965
|
+
pair.fresh = null;
|
|
1966
|
+
for (const script of scripts) if (script) script.fresh = null;
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
for (const pair of pairs) {
|
|
1970
|
+
if (pair.reviewed.hookSettings && (!byPath.get(pair.reviewed.hookSettings)?.fresh
|
|
1971
|
+
|| !byPath.get(pair.reviewed.hookSettings).fresh.hookScripts.includes(pair.reviewed.relative))) pair.fresh = null;
|
|
1972
|
+
}
|
|
1973
|
+
return pairs;
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
// Stage the scripts reversibly, then replace settings. A failed rename/write restores the scripts;
|
|
1977
|
+
// settings never references a script we permanently removed. Temp bytes are never source evidence.
|
|
1978
|
+
function cleanupApplyHookGroup(settings, scripts) {
|
|
1979
|
+
for (const item of [settings, ...scripts]) {
|
|
1980
|
+
const invalid = cleanupValidateItem(item);
|
|
1981
|
+
if (invalid) return invalid;
|
|
1982
|
+
}
|
|
1983
|
+
const staged = [];
|
|
1984
|
+
try {
|
|
1985
|
+
for (const item of scripts) {
|
|
1986
|
+
const temp = `${item.path}.${crypto.randomUUID()}.tmp`;
|
|
1987
|
+
fs.renameSync(item.path, temp);
|
|
1988
|
+
staged.push({ item, temp });
|
|
1989
|
+
}
|
|
1990
|
+
const error = cleanupApplyItem(settings);
|
|
1991
|
+
if (error) throw new Error(error.code);
|
|
1992
|
+
} catch (error) {
|
|
1993
|
+
const errors = [error];
|
|
1994
|
+
for (const { item, temp } of staged.reverse()) {
|
|
1995
|
+
try { fs.renameSync(temp, item.path); } catch (rollback) { errors.push(rollback); }
|
|
1996
|
+
}
|
|
1997
|
+
throw new AggregateError(errors, errors.map((e) => e.message).join('; '));
|
|
1998
|
+
}
|
|
1999
|
+
for (const { temp } of staged) fs.unlinkSync(temp);
|
|
2000
|
+
return null;
|
|
2001
|
+
}
|
|
2002
|
+
|
|
1830
2003
|
// Support entries (`_shared`, the marker) go last, and only when nothing beside them was refused.
|
|
1831
2004
|
function cleanupSupport(item) {
|
|
1832
2005
|
return path.basename(item.path) === '_shared' || item.kind === 'install-marker';
|
|
@@ -1855,7 +2028,16 @@ function cleanupLocal(scope, items) {
|
|
|
1855
2028
|
catch (error) { results.set(item.id, cleanupReason('apply-failed', { detail: error.message })); }
|
|
1856
2029
|
};
|
|
1857
2030
|
const run = () => {
|
|
1858
|
-
for (const
|
|
2031
|
+
for (const settings of items.filter((item) => item.hookScripts?.length)) {
|
|
2032
|
+
const scripts = items.filter((item) => item.hookSettings === settings.path);
|
|
2033
|
+
const group = [settings, ...scripts];
|
|
2034
|
+
let error;
|
|
2035
|
+
try {
|
|
2036
|
+
error = group.some((item) => cleanupTracked(item.path)) ? cleanupReason('tracked-copy') : cleanupApplyHookGroup(settings, scripts);
|
|
2037
|
+
} catch (failure) { error = cleanupReason('apply-failed', { detail: failure.message }); }
|
|
2038
|
+
for (const item of group) results.set(item.id, error);
|
|
2039
|
+
}
|
|
2040
|
+
for (const item of items.filter((item) => !cleanupSupport(item) && !results.has(item.id))) act(item);
|
|
1859
2041
|
for (const item of items.filter(cleanupSupport)) {
|
|
1860
2042
|
const refused = items.some((other) => !cleanupSupport(other) && path.dirname(other.path) === path.dirname(item.path)
|
|
1861
2043
|
&& results.get(other.id) !== null);
|
|
@@ -2001,7 +2183,8 @@ function cleanupUpdateCheckout(scope) {
|
|
|
2001
2183
|
// With every reviewed item already settled by the remote base, the worktree is released with
|
|
2002
2184
|
// nothing to deliver.
|
|
2003
2185
|
function cleanupDeliveryCommit(worktree, items, current, release, settledUpstream) {
|
|
2004
|
-
const
|
|
2186
|
+
const includedHooks = new Set(items.filter((item) => item.kind === 'legacy-hook' && item.included).map((item) => path.join(worktree, item.relative)));
|
|
2187
|
+
const fresh = cleanupProjectScope(worktree, current, items.some((item) => item.included), includedHooks);
|
|
2005
2188
|
const available = new Map(fresh.items.map((item) => [`${item.kind}\u0000${item.relative}`, item]));
|
|
2006
2189
|
const pairs = items.map((reviewed) => {
|
|
2007
2190
|
const fresh = available.get(`${reviewed.kind}\u0000${reviewed.relative}`);
|
|
@@ -2011,13 +2194,23 @@ function cleanupDeliveryCommit(worktree, items, current, release, settledUpstrea
|
|
|
2011
2194
|
? fresh.action !== reviewed.action || fresh.route !== 'delivery'
|
|
2012
2195
|
|| (reviewed.included && (!cleanupCanInclude(fresh) || before.sha256 === null))
|
|
2013
2196
|
|| before.type !== reviewed.before.type || before.sha256 !== reviewed.before.sha256
|
|
2197
|
+
|| cleanupCanonical(fresh.hookScripts || []) !== cleanupCanonical(reviewed.hookScripts || [])
|
|
2014
2198
|
: !settledUpstream(reviewed.relative)));
|
|
2015
2199
|
// Check the entire batch before mutating even one item; the remote base cannot replace the reviewed preimage.
|
|
2016
2200
|
if (stale.length) return release(cleanupReason('stale-plan', { detail: stale.map(({ reviewed }) => reviewed.relative).join(', ') }));
|
|
2201
|
+
if (cleanupCoupledPairs(pairs.filter(({ fresh }) => fresh)).some((pair) => !pair.fresh)) return release(cleanupReason('stale-plan'));
|
|
2017
2202
|
const present = pairs.filter(({ fresh }) => fresh);
|
|
2018
2203
|
if (!present.length) return release();
|
|
2019
2204
|
const applied = [];
|
|
2205
|
+
for (const { reviewed, fresh: settings } of present.filter(({ fresh }) => fresh.hookScripts?.length)) {
|
|
2206
|
+
const scripts = present.filter(({ fresh }) => fresh.hookSettings === settings.path)
|
|
2207
|
+
.map(({ reviewed, fresh }) => ({ ...fresh, before: reviewed.before, included: reviewed.included }));
|
|
2208
|
+
const error = cleanupApplyHookGroup({ ...settings, before: reviewed.before }, scripts);
|
|
2209
|
+
if (error) return release(error);
|
|
2210
|
+
applied.push(settings.relative, ...scripts.map((item) => item.relative));
|
|
2211
|
+
}
|
|
2020
2212
|
for (const { reviewed, fresh } of present) {
|
|
2213
|
+
if (applied.includes(fresh.relative)) continue;
|
|
2021
2214
|
const error = cleanupApplyItem({ ...fresh, before: reviewed.before, included: reviewed.included });
|
|
2022
2215
|
if (error) return release(cleanupReason(error.code, { ...error.params, detail: reviewed.relative }));
|
|
2023
2216
|
applied.push(fresh.relative);
|
|
@@ -2027,7 +2220,7 @@ function cleanupDeliveryCommit(worktree, items, current, release, settledUpstrea
|
|
|
2027
2220
|
if (fresh.items.some((item) => item.keepRoot && cleanupSameFile(path.dirname(item.path), full))) continue;
|
|
2028
2221
|
try { if (fs.lstatSync(full).isDirectory() && fs.readdirSync(full).length === 0) fs.rmdirSync(full); } catch {}
|
|
2029
2222
|
}
|
|
2030
|
-
const add = cleanupGit(worktree, ['add', '-
|
|
2223
|
+
const add = cleanupGit(worktree, ['add', '-u', '--', ...applied]);
|
|
2031
2224
|
const commit = add.status === 0 && cleanupGit(worktree, ['-c', 'user.useConfigOnly=true', 'commit', '-q', '-m',
|
|
2032
2225
|
`chore(devlyn): remove legacy devlyn-cli copies\n\nDevlyn now delivers devlyn-cli ${PKG.version} to each session it starts, so the copies earlier\ninstalls committed here are no longer used. The managed principles block stays for teammates.`]);
|
|
2033
2226
|
if (!commit || commit.status !== 0) return release(cleanupReason('delivery-failed', { detail: (commit || add).stderr.trim() }));
|
|
@@ -2052,12 +2245,20 @@ function cleanupDeliveryCommit(worktree, items, current, release, settledUpstrea
|
|
|
2052
2245
|
function cleanupApply(plan, { events = false } = {}) {
|
|
2053
2246
|
const current = cleanupCurrentFingerprints();
|
|
2054
2247
|
const includeModified = plan.scopes.some((scope) => scope.items.some((item) => item.included));
|
|
2055
|
-
const
|
|
2248
|
+
const includedHooks = new Set(plan.scopes.flatMap((scope) => scope.items)
|
|
2249
|
+
.filter((item) => item.kind === 'legacy-hook' && item.included).map((item) => item.path));
|
|
2250
|
+
// Explicit selection of a reviewed edited script also selects its reviewed settings preimage.
|
|
2251
|
+
for (const scope of plan.scopes) for (const item of scope.items) {
|
|
2252
|
+
if (!item.hookCandidates || (item.action === 'skip' && item.reasons[0]?.code !== 'dependent-copy')) continue;
|
|
2253
|
+
const selected = item.hookCandidates.filter((name) => includedHooks.has(path.join(item.base, name)));
|
|
2254
|
+
if (selected.length) Object.assign(item, { action: 'edit', reasons: [], hookScripts: [...new Set([...item.hookScripts, ...selected])].sort() });
|
|
2255
|
+
}
|
|
2256
|
+
const fresh = new Map(cleanupScopes(plan.inputs, current, includeModified, includedHooks).map((scope) => [scope.id, scope]));
|
|
2056
2257
|
let seq = 0;
|
|
2057
2258
|
const emit = (scope) => { if (events) process.stdout.write(JSON.stringify({ schemaVersion: 1, event: 'scope', seq: ++seq, scope }) + '\n'); };
|
|
2058
2259
|
const scopes = [];
|
|
2059
2260
|
for (const reviewed of plan.scopes) {
|
|
2060
|
-
const pairs = cleanupProven(reviewed, fresh.get(reviewed.id) || { items: [] });
|
|
2261
|
+
const pairs = cleanupCoupledPairs(cleanupProven(reviewed, fresh.get(reviewed.id) || { items: [] }));
|
|
2061
2262
|
const progress = { scopeId: reviewed.id, kind: reviewed.kind, name: reviewed.name, total: pairs.length, state: 'running', delivery: null, error: null };
|
|
2062
2263
|
if (!pairs.length) { scopes.push({ ...progress, state: 'skipped' }); emit(scopes.at(-1)); continue; }
|
|
2063
2264
|
emit(progress);
|
package/bin/skill-history.json
CHANGED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
3.3.0 2022-06-24
|
|
2
|
+
=================
|
|
3
|
+
- `JSONVisitor.onObjectBegin` and `JSONVisitor.onArrayBegin` can now return `false` to instruct the visitor that no children should be visited.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
3.2.0 2022-08-30
|
|
7
|
+
=================
|
|
8
|
+
- update the version of the bundled Javascript files to `es2020`.
|
|
9
|
+
- include all `const enum` values in the bundled JavaScript files (`ScanError`, `SyntaxKind`, `ParseErrorCode`).
|
|
10
|
+
|
|
11
|
+
3.1.0 2022-07-07
|
|
12
|
+
==================
|
|
13
|
+
* added new API `FormattingOptions.keepLines` : It leaves the initial line positions in the formatting.
|
|
14
|
+
|
|
15
|
+
3.0.0 2020-11-13
|
|
16
|
+
==================
|
|
17
|
+
* fixed API spec for `parseTree`. Can return `undefine` for empty input.
|
|
18
|
+
* added new API `FormattingOptions.insertFinalNewline`.
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
2.3.0 2020-07-03
|
|
22
|
+
==================
|
|
23
|
+
* new API `ModificationOptions.isArrayInsertion`: If `JSONPath` refers to an index of an array and `isArrayInsertion` is `true`, then `modify` will insert a new item at that location instead of overwriting its contents.
|
|
24
|
+
* `ModificationOptions.formattingOptions` is now optional. If not set, newly inserted content will not be formatted.
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
2.2.0 2019-10-25
|
|
28
|
+
==================
|
|
29
|
+
* added `ParseOptions.allowEmptyContent`. Default is `false`.
|
|
30
|
+
* new API `getNodeType`: Returns the type of a value returned by parse.
|
|
31
|
+
* `parse`: Fix issue with empty property name
|
|
32
|
+
|
|
33
|
+
2.1.0 2019-03-29
|
|
34
|
+
==================
|
|
35
|
+
* `JSONScanner` and `JSONVisitor` return lineNumber / character.
|
|
36
|
+
|
|
37
|
+
2.0.0 2018-04-12
|
|
38
|
+
==================
|
|
39
|
+
* renamed `Node.columnOffset` to `Node.colonOffset`
|
|
40
|
+
* new API `getNodePath`: Gets the JSON path of the given JSON DOM node
|
|
41
|
+
* new API `findNodeAtOffset`: Finds the most inner node at the given offset. If `includeRightBound` is set, also finds nodes that end at the given offset.
|
|
42
|
+
|
|
43
|
+
1.0.3 2018-03-07
|
|
44
|
+
==================
|
|
45
|
+
* provide ems modules
|
|
46
|
+
|
|
47
|
+
1.0.2 2018-03-05
|
|
48
|
+
==================
|
|
49
|
+
* added the `visit.onComment` API, reported when comments are allowed.
|
|
50
|
+
* added the `ParseErrorCode.InvalidCommentToken` enum value, reported when comments are disallowed.
|
|
51
|
+
|
|
52
|
+
1.0.1
|
|
53
|
+
==================
|
|
54
|
+
* added the `format` API: computes edits to format a JSON document.
|
|
55
|
+
* added the `modify` API: computes edits to insert, remove or replace a property or value in a JSON document.
|
|
56
|
+
* added the `allyEdits` API: applies edits to a document
|
|
57
|
+
|
|
58
|
+
1.0.0
|
|
59
|
+
==================
|
|
60
|
+
* remove nls dependency (remove `getParseErrorMessage`)
|
|
61
|
+
|
|
62
|
+
0.4.2 / 2017-05-05
|
|
63
|
+
==================
|
|
64
|
+
* added `ParseError.offset` & `ParseError.length`
|
|
65
|
+
|
|
66
|
+
0.4.1 / 2017-04-02
|
|
67
|
+
==================
|
|
68
|
+
* added `ParseOptions.allowTrailingComma`
|
|
69
|
+
|
|
70
|
+
0.4.0 / 2017-02-23
|
|
71
|
+
==================
|
|
72
|
+
* fix for `getLocation`. Now `getLocation` inside an object will always return a property from inside that property. Can be empty string if the object has no properties or if the offset is before a actual property `{ "a": { | }} will return location ['a', ' ']`
|
|
73
|
+
|
|
74
|
+
0.3.0 / 2017-01-17
|
|
75
|
+
==================
|
|
76
|
+
* Updating to typescript 2.0
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) Microsoft
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|