@phnx-labs/agents-cli 1.18.1 → 1.18.3
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/CHANGELOG.md +22 -0
- package/dist/commands/doctor.js +19 -5
- package/dist/commands/exec.js +9 -4
- package/dist/commands/plugins.js +58 -14
- package/dist/commands/view.js +16 -7
- package/dist/index.js +30 -0
- package/dist/lib/hooks.js +21 -3
- package/dist/lib/migrate.js +35 -12
- package/dist/lib/plugin-marketplace.d.ts +93 -0
- package/dist/lib/plugin-marketplace.js +239 -0
- package/dist/lib/plugins.d.ts +25 -13
- package/dist/lib/plugins.js +350 -566
- package/dist/lib/shims.d.ts +3 -1
- package/dist/lib/shims.js +81 -7
- package/dist/lib/staleness/checkers/commands.d.ts +7 -0
- package/dist/lib/staleness/checkers/commands.js +27 -0
- package/dist/lib/staleness/checkers/hooks.d.ts +13 -0
- package/dist/lib/staleness/checkers/hooks.js +63 -0
- package/dist/lib/staleness/checkers/mcp.d.ts +12 -0
- package/dist/lib/staleness/checkers/mcp.js +38 -0
- package/dist/lib/staleness/checkers/permissions.d.ts +17 -0
- package/dist/lib/staleness/checkers/permissions.js +73 -0
- package/dist/lib/staleness/checkers/plugins.d.ts +11 -0
- package/dist/lib/staleness/checkers/plugins.js +39 -0
- package/dist/lib/staleness/checkers/rules.d.ts +19 -0
- package/dist/lib/staleness/checkers/rules.js +86 -0
- package/dist/lib/staleness/checkers/skills.d.ts +7 -0
- package/dist/lib/staleness/checkers/skills.js +34 -0
- package/dist/lib/staleness/checkers/subagents.d.ts +12 -0
- package/dist/lib/staleness/checkers/subagents.js +39 -0
- package/dist/lib/staleness/checkers/types.d.ts +44 -0
- package/dist/lib/staleness/checkers/types.js +20 -0
- package/dist/lib/staleness/checkers/workflows.d.ts +10 -0
- package/dist/lib/staleness/checkers/workflows.js +37 -0
- package/dist/lib/staleness/fingerprint.d.ts +38 -0
- package/dist/lib/staleness/fingerprint.js +154 -0
- package/dist/lib/staleness/index.d.ts +26 -0
- package/dist/lib/staleness/index.js +122 -0
- package/dist/lib/staleness/layers.d.ts +37 -0
- package/dist/lib/staleness/layers.js +100 -0
- package/dist/lib/staleness/types.d.ts +56 -0
- package/dist/lib/staleness/types.js +6 -0
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +2 -0
- package/dist/lib/teams/agents.d.ts +11 -20
- package/dist/lib/teams/agents.js +55 -202
- package/dist/lib/teams/index.d.ts +3 -2
- package/dist/lib/teams/index.js +2 -2
- package/dist/lib/teams/persistence.d.ts +0 -38
- package/dist/lib/teams/persistence.js +7 -329
- package/dist/lib/teams/registry.js +7 -5
- package/dist/lib/types.d.ts +6 -0
- package/dist/lib/versions.js +34 -12
- package/package.json +1 -1
- package/dist/lib/sync-manifest.d.ts +0 -81
- package/dist/lib/sync-manifest.js +0 -450
package/dist/lib/shims.d.ts
CHANGED
|
@@ -50,8 +50,10 @@ export interface ConflictInfo {
|
|
|
50
50
|
* (two-repo split: system = shipped defaults, user = operational state).
|
|
51
51
|
* v9 — claude shim exports CLAUDE_CODE_OAUTH_TOKEN from per-version
|
|
52
52
|
* .oauth_token file on Linux (keychain-less sandbox fallback).
|
|
53
|
+
* v11 — when no default is set or the configured version is not installed,
|
|
54
|
+
* interactively propose the latest already-installed version.
|
|
53
55
|
*/
|
|
54
|
-
export declare const SHIM_SCHEMA_VERSION =
|
|
56
|
+
export declare const SHIM_SCHEMA_VERSION = 11;
|
|
55
57
|
/**
|
|
56
58
|
* Generate the full bash shim script for the given agent. The returned string
|
|
57
59
|
* is written to ~/.agents/shims/{cliCommand} and made executable.
|
package/dist/lib/shims.js
CHANGED
|
@@ -173,8 +173,10 @@ async function promptConflictStrategy(conflictInfos) {
|
|
|
173
173
|
* (two-repo split: system = shipped defaults, user = operational state).
|
|
174
174
|
* v9 — claude shim exports CLAUDE_CODE_OAUTH_TOKEN from per-version
|
|
175
175
|
* .oauth_token file on Linux (keychain-less sandbox fallback).
|
|
176
|
+
* v11 — when no default is set or the configured version is not installed,
|
|
177
|
+
* interactively propose the latest already-installed version.
|
|
176
178
|
*/
|
|
177
|
-
export const SHIM_SCHEMA_VERSION =
|
|
179
|
+
export const SHIM_SCHEMA_VERSION = 11;
|
|
178
180
|
/** Internal marker string used to embed the schema version in shim scripts. */
|
|
179
181
|
const SHIM_VERSION_MARKER = 'agents-shim-version:';
|
|
180
182
|
/**
|
|
@@ -281,6 +283,31 @@ find_project_agents_dir() {
|
|
|
281
283
|
return 1
|
|
282
284
|
}
|
|
283
285
|
|
|
286
|
+
# Find the latest installed version by numeric component comparison.
|
|
287
|
+
# Handles both semver (2.1.138) and date-based (2026.5.7) version strings.
|
|
288
|
+
find_latest_installed() {
|
|
289
|
+
local versions_dir="$AGENTS_USER_DIR/.history/versions/$AGENT"
|
|
290
|
+
[ -d "$versions_dir" ] || return
|
|
291
|
+
ls "$versions_dir" 2>/dev/null | awk '
|
|
292
|
+
BEGIN { best="" }
|
|
293
|
+
{
|
|
294
|
+
cur = $0
|
|
295
|
+
n = split(cur, a, /[^0-9]+/)
|
|
296
|
+
m = split(best, b, /[^0-9]+/)
|
|
297
|
+
maxn = (n > m) ? n : m
|
|
298
|
+
winner = cur
|
|
299
|
+
for (i=1; i<=maxn; i++) {
|
|
300
|
+
ai = (i<=n) ? a[i]+0 : 0
|
|
301
|
+
bi = (i<=m) ? b[i]+0 : 0
|
|
302
|
+
if (ai > bi) { winner=cur; break }
|
|
303
|
+
if (ai < bi) { winner=best; break }
|
|
304
|
+
}
|
|
305
|
+
best = winner
|
|
306
|
+
}
|
|
307
|
+
END { print best }
|
|
308
|
+
'
|
|
309
|
+
}
|
|
310
|
+
|
|
284
311
|
# Try project version first, then global default
|
|
285
312
|
VERSION=$(find_project_version)
|
|
286
313
|
VERSION_SOURCE="project"
|
|
@@ -290,9 +317,32 @@ if [ -z "$VERSION" ]; then
|
|
|
290
317
|
fi
|
|
291
318
|
|
|
292
319
|
if [ -z "$VERSION" ]; then
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
320
|
+
LATEST=$(find_latest_installed)
|
|
321
|
+
if [ -n "$LATEST" ]; then
|
|
322
|
+
echo "agents: no default set for $AGENT — found $AGENT@$LATEST installed" >&2
|
|
323
|
+
if [ -t 2 ]; then
|
|
324
|
+
printf " Set as default and continue? [Y/n] " >&2
|
|
325
|
+
read -r _ans </dev/tty
|
|
326
|
+
case "$_ans" in
|
|
327
|
+
""|y|Y)
|
|
328
|
+
agents use "$AGENT" "$LATEST" >/dev/null 2>&1
|
|
329
|
+
VERSION="$LATEST"
|
|
330
|
+
VERSION_SOURCE="default"
|
|
331
|
+
;;
|
|
332
|
+
*)
|
|
333
|
+
echo " Run: agents use $AGENT <version>" >&2
|
|
334
|
+
exit 1
|
|
335
|
+
;;
|
|
336
|
+
esac
|
|
337
|
+
else
|
|
338
|
+
echo " Run: agents use $AGENT <version>" >&2
|
|
339
|
+
exit 1
|
|
340
|
+
fi
|
|
341
|
+
else
|
|
342
|
+
echo "agents: no version of $AGENT configured" >&2
|
|
343
|
+
echo " Run: agents add $AGENT@<version>" >&2
|
|
344
|
+
exit 1
|
|
345
|
+
fi
|
|
296
346
|
fi
|
|
297
347
|
|
|
298
348
|
VERSION_DIR="$AGENTS_USER_DIR/.history/versions/$AGENT/$VERSION"
|
|
@@ -329,9 +379,33 @@ if [ ! -x "$BINARY" ]; then
|
|
|
329
379
|
exit 1
|
|
330
380
|
fi
|
|
331
381
|
else
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
382
|
+
LATEST=$(find_latest_installed)
|
|
383
|
+
if [ -n "$LATEST" ] && [ "$LATEST" != "$VERSION" ]; then
|
|
384
|
+
echo "agents: $AGENT@$VERSION not installed — found $AGENT@$LATEST installed" >&2
|
|
385
|
+
if [ -t 2 ]; then
|
|
386
|
+
printf " Switch default to $AGENT@$LATEST and continue? [Y/n] " >&2
|
|
387
|
+
read -r _ans </dev/tty
|
|
388
|
+
case "$_ans" in
|
|
389
|
+
""|y|Y)
|
|
390
|
+
agents use "$AGENT" "$LATEST" >/dev/null 2>&1
|
|
391
|
+
VERSION="$LATEST"
|
|
392
|
+
VERSION_DIR="$AGENTS_USER_DIR/.history/versions/$AGENT/$VERSION"
|
|
393
|
+
BINARY="$VERSION_DIR/node_modules/.bin/$CLI_COMMAND"
|
|
394
|
+
;;
|
|
395
|
+
*)
|
|
396
|
+
echo " Run: agents add $AGENT@$VERSION" >&2
|
|
397
|
+
exit 1
|
|
398
|
+
;;
|
|
399
|
+
esac
|
|
400
|
+
else
|
|
401
|
+
echo " Run: agents add $AGENT@$VERSION" >&2
|
|
402
|
+
exit 1
|
|
403
|
+
fi
|
|
404
|
+
else
|
|
405
|
+
echo "agents: $AGENT@$VERSION not installed" >&2
|
|
406
|
+
echo " Run: agents add $AGENT@$VERSION" >&2
|
|
407
|
+
exit 1
|
|
408
|
+
fi
|
|
335
409
|
fi
|
|
336
410
|
fi
|
|
337
411
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commands staleness — one `.md` file per command, first-wins across
|
|
3
|
+
* project > user > system > extras.
|
|
4
|
+
*/
|
|
5
|
+
import type { FileEntry } from '../types.js';
|
|
6
|
+
import type { TypedResourceChecker } from './types.js';
|
|
7
|
+
export declare const commandsChecker: TypedResourceChecker<FileEntry>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commands staleness — one `.md` file per command, first-wins across
|
|
3
|
+
* project > user > system > extras.
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import { firstWinsLayers, listAcrossLayers, resolveByName } from '../layers.js';
|
|
8
|
+
import { fingerprintFile, isFileStale } from '../fingerprint.js';
|
|
9
|
+
export const commandsChecker = {
|
|
10
|
+
type: 'commands',
|
|
11
|
+
listNames(cwd) {
|
|
12
|
+
return listAcrossLayers(firstWinsLayers(cwd), 'commands', (name) => name.endsWith('.md')).map((n) => n.replace(/\.md$/, ''));
|
|
13
|
+
},
|
|
14
|
+
build(name, cwd) {
|
|
15
|
+
const resolved = resolveByName(firstWinsLayers(cwd), path.join('commands', `${name}.md`), (p) => fs.existsSync(p));
|
|
16
|
+
if (!resolved)
|
|
17
|
+
return null;
|
|
18
|
+
const fp = fingerprintFile(resolved.path);
|
|
19
|
+
return fp ? { source: fp } : null;
|
|
20
|
+
},
|
|
21
|
+
isFresh(name, stored, cwd) {
|
|
22
|
+
const resolved = resolveByName(firstWinsLayers(cwd), path.join('commands', `${name}.md`), (p) => fs.existsSync(p));
|
|
23
|
+
if (!resolved)
|
|
24
|
+
return false;
|
|
25
|
+
return !isFileStale(stored.source, resolved.path);
|
|
26
|
+
},
|
|
27
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hooks staleness — one executable file per hook. Project layer is EXCLUDED
|
|
3
|
+
* by design: a cloned public repo with `.agents/hooks/foo` must not plant a
|
|
4
|
+
* hook that fires next time the user runs an agent inside it (see
|
|
5
|
+
* `src/lib/versions.ts:1832-1836`). Only user + system + extras count.
|
|
6
|
+
*
|
|
7
|
+
* Auxiliary files (README.md, promptcuts.yaml) live in hooks/ but are not
|
|
8
|
+
* hooks — the executable bit on the source distinguishes them. This matches
|
|
9
|
+
* the filter in `getAvailableResources`.
|
|
10
|
+
*/
|
|
11
|
+
import type { FileEntry } from '../types.js';
|
|
12
|
+
import type { TypedResourceChecker } from './types.js';
|
|
13
|
+
export declare const hooksChecker: TypedResourceChecker<FileEntry>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hooks staleness — one executable file per hook. Project layer is EXCLUDED
|
|
3
|
+
* by design: a cloned public repo with `.agents/hooks/foo` must not plant a
|
|
4
|
+
* hook that fires next time the user runs an agent inside it (see
|
|
5
|
+
* `src/lib/versions.ts:1832-1836`). Only user + system + extras count.
|
|
6
|
+
*
|
|
7
|
+
* Auxiliary files (README.md, promptcuts.yaml) live in hooks/ but are not
|
|
8
|
+
* hooks — the executable bit on the source distinguishes them. This matches
|
|
9
|
+
* the filter in `getAvailableResources`.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import { hookLayers, listAcrossLayers, resolveByName } from '../layers.js';
|
|
14
|
+
import { fingerprintFile, isFileStale } from '../fingerprint.js';
|
|
15
|
+
/** Extensions that are NEVER hooks — docs, configuration, plain data. */
|
|
16
|
+
const NON_SCRIPT_EXTENSIONS = new Set([
|
|
17
|
+
'.md', '.markdown', '.rst', '.txt',
|
|
18
|
+
'.yaml', '.yml', '.json', '.toml', '.ini', '.conf',
|
|
19
|
+
]);
|
|
20
|
+
/** Extensions that explicitly mark a file as a script regardless of exec bit. */
|
|
21
|
+
const SCRIPT_EXTENSIONS = new Set([
|
|
22
|
+
'.sh', '.bash', '.zsh',
|
|
23
|
+
'.py', '.js', '.ts', '.mjs', '.cjs',
|
|
24
|
+
'.rb', '.pl', '.ps1',
|
|
25
|
+
]);
|
|
26
|
+
function isHookScript(full) {
|
|
27
|
+
try {
|
|
28
|
+
const stat = fs.statSync(full);
|
|
29
|
+
if (!stat.isFile())
|
|
30
|
+
return false;
|
|
31
|
+
const ext = path.extname(full).toLowerCase();
|
|
32
|
+
if (SCRIPT_EXTENSIONS.has(ext))
|
|
33
|
+
return true;
|
|
34
|
+
// Otherwise require exec bit AND a non-data extension. Older sync runs
|
|
35
|
+
// chmod 0o755'd everything including `promptcuts.yaml` / `README.md`,
|
|
36
|
+
// so exec bit alone can't be trusted.
|
|
37
|
+
if ((stat.mode & 0o111) === 0)
|
|
38
|
+
return false;
|
|
39
|
+
return !NON_SCRIPT_EXTENSIONS.has(ext);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export const hooksChecker = {
|
|
46
|
+
type: 'hooks',
|
|
47
|
+
listNames(_cwd) {
|
|
48
|
+
return listAcrossLayers(hookLayers(), 'hooks', (_, full) => isHookScript(full));
|
|
49
|
+
},
|
|
50
|
+
build(name, _cwd) {
|
|
51
|
+
const resolved = resolveByName(hookLayers(), path.join('hooks', name), isHookScript);
|
|
52
|
+
if (!resolved)
|
|
53
|
+
return null;
|
|
54
|
+
const fp = fingerprintFile(resolved.path);
|
|
55
|
+
return fp ? { source: fp } : null;
|
|
56
|
+
},
|
|
57
|
+
isFresh(name, stored, _cwd) {
|
|
58
|
+
const resolved = resolveByName(hookLayers(), path.join('hooks', name), isHookScript);
|
|
59
|
+
if (!resolved)
|
|
60
|
+
return false;
|
|
61
|
+
return !isFileStale(stored.source, resolved.path);
|
|
62
|
+
},
|
|
63
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP server staleness — one `.yaml`/`.yml` file per server, first-wins
|
|
3
|
+
* across project > user > system > extras. Name is the `name:` field inside
|
|
4
|
+
* the YAML, NOT the filename (per `getAvailableResources`).
|
|
5
|
+
*
|
|
6
|
+
* We delegate name/path discovery to `listMcpServerConfigs(cwd)` which
|
|
7
|
+
* already handles parsing — keeps a single source of truth for "what counts
|
|
8
|
+
* as a discoverable MCP server."
|
|
9
|
+
*/
|
|
10
|
+
import type { FileEntry } from '../types.js';
|
|
11
|
+
import type { TypedResourceChecker } from './types.js';
|
|
12
|
+
export declare const mcpChecker: TypedResourceChecker<FileEntry>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP server staleness — one `.yaml`/`.yml` file per server, first-wins
|
|
3
|
+
* across project > user > system > extras. Name is the `name:` field inside
|
|
4
|
+
* the YAML, NOT the filename (per `getAvailableResources`).
|
|
5
|
+
*
|
|
6
|
+
* We delegate name/path discovery to `listMcpServerConfigs(cwd)` which
|
|
7
|
+
* already handles parsing — keeps a single source of truth for "what counts
|
|
8
|
+
* as a discoverable MCP server."
|
|
9
|
+
*/
|
|
10
|
+
import { fingerprintFile, isFileStale } from '../fingerprint.js';
|
|
11
|
+
import { listMcpServerConfigs } from '../../mcp.js';
|
|
12
|
+
function indexByName(cwd) {
|
|
13
|
+
const map = new Map();
|
|
14
|
+
for (const cfg of listMcpServerConfigs(cwd)) {
|
|
15
|
+
if (!map.has(cfg.name))
|
|
16
|
+
map.set(cfg.name, cfg.path);
|
|
17
|
+
}
|
|
18
|
+
return map;
|
|
19
|
+
}
|
|
20
|
+
export const mcpChecker = {
|
|
21
|
+
type: 'mcp',
|
|
22
|
+
listNames(cwd) {
|
|
23
|
+
return Array.from(indexByName(cwd).keys());
|
|
24
|
+
},
|
|
25
|
+
build(name, cwd) {
|
|
26
|
+
const src = indexByName(cwd).get(name);
|
|
27
|
+
if (!src)
|
|
28
|
+
return null;
|
|
29
|
+
const fp = fingerprintFile(src);
|
|
30
|
+
return fp ? { source: fp } : null;
|
|
31
|
+
},
|
|
32
|
+
isFresh(name, stored, cwd) {
|
|
33
|
+
const src = indexByName(cwd).get(name);
|
|
34
|
+
if (!src)
|
|
35
|
+
return false;
|
|
36
|
+
return !isFileStale(stored.source, src);
|
|
37
|
+
},
|
|
38
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permissions staleness — every `groups/*.yaml` across user + system
|
|
3
|
+
* contributes to the merged permission set (project layer not consulted by
|
|
4
|
+
* the current sync writer). First-wins on name collision (user > system).
|
|
5
|
+
*
|
|
6
|
+
* The active preset env value (`AGENTS_PERMISSION_PRESET`) is part of the
|
|
7
|
+
* fingerprint too — preset selection changes which groups get applied to
|
|
8
|
+
* the agent config, so a preset switch without a content change still
|
|
9
|
+
* counts as stale.
|
|
10
|
+
*/
|
|
11
|
+
import type { PermEntry } from '../types.js';
|
|
12
|
+
/** Walk user + system permissions/groups/. First-wins user > system on names. */
|
|
13
|
+
export declare function collectPermissionGroupFiles(): Record<string, string>;
|
|
14
|
+
/** Build the permissions section of the manifest. */
|
|
15
|
+
export declare function buildPermissions(): PermEntry;
|
|
16
|
+
/** True when the stored permissions section no longer matches current state. */
|
|
17
|
+
export declare function isPermissionsStale(stored: PermEntry): boolean;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permissions staleness — every `groups/*.yaml` across user + system
|
|
3
|
+
* contributes to the merged permission set (project layer not consulted by
|
|
4
|
+
* the current sync writer). First-wins on name collision (user > system).
|
|
5
|
+
*
|
|
6
|
+
* The active preset env value (`AGENTS_PERMISSION_PRESET`) is part of the
|
|
7
|
+
* fingerprint too — preset selection changes which groups get applied to
|
|
8
|
+
* the agent config, so a preset switch without a content change still
|
|
9
|
+
* counts as stale.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import { getUserPermissionsDir, getPermissionsDir } from '../../state.js';
|
|
14
|
+
import { fingerprintFile, isFileStale } from '../fingerprint.js';
|
|
15
|
+
import { getActivePermissionPresetName } from '../../permissions.js';
|
|
16
|
+
/** Walk user + system permissions/groups/. First-wins user > system on names. */
|
|
17
|
+
export function collectPermissionGroupFiles() {
|
|
18
|
+
const seen = new Map();
|
|
19
|
+
for (const baseDir of [getUserPermissionsDir(), getPermissionsDir()]) {
|
|
20
|
+
const groupsDir = path.join(baseDir, 'groups');
|
|
21
|
+
let entries;
|
|
22
|
+
try {
|
|
23
|
+
entries = fs.readdirSync(groupsDir, { withFileTypes: true });
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
if (!entry.isFile())
|
|
30
|
+
continue;
|
|
31
|
+
if (!entry.name.endsWith('.yaml') && !entry.name.endsWith('.yml'))
|
|
32
|
+
continue;
|
|
33
|
+
const name = entry.name.replace(/\.(yaml|yml)$/, '');
|
|
34
|
+
if (!seen.has(name))
|
|
35
|
+
seen.set(name, path.join(groupsDir, entry.name));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return Object.fromEntries(seen);
|
|
39
|
+
}
|
|
40
|
+
/** Build the permissions section of the manifest. */
|
|
41
|
+
export function buildPermissions() {
|
|
42
|
+
const groupFiles = collectPermissionGroupFiles();
|
|
43
|
+
const groups = {};
|
|
44
|
+
for (const [name, filePath] of Object.entries(groupFiles)) {
|
|
45
|
+
const fp = fingerprintFile(filePath);
|
|
46
|
+
if (fp)
|
|
47
|
+
groups[name] = { source: fp };
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
groups,
|
|
51
|
+
permissionPreset: getActivePermissionPresetName(),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** True when the stored permissions section no longer matches current state. */
|
|
55
|
+
export function isPermissionsStale(stored) {
|
|
56
|
+
if (stored.permissionPreset !== getActivePermissionPresetName())
|
|
57
|
+
return true;
|
|
58
|
+
const currentGroups = collectPermissionGroupFiles();
|
|
59
|
+
const storedNames = Object.keys(stored.groups).sort();
|
|
60
|
+
const currentNames = Object.keys(currentGroups).sort();
|
|
61
|
+
if (storedNames.length !== currentNames.length)
|
|
62
|
+
return true;
|
|
63
|
+
for (let i = 0; i < storedNames.length; i++) {
|
|
64
|
+
if (storedNames[i] !== currentNames[i])
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
for (const [name, filePath] of Object.entries(currentGroups)) {
|
|
68
|
+
const entry = stored.groups[name];
|
|
69
|
+
if (!entry || isFileStale(entry.source, filePath))
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugins staleness — one directory per plugin, marker file is
|
|
3
|
+
* `.claude-plugin/plugin.json`. First-wins across project > user > system
|
|
4
|
+
* > extras. Fingerprints the entire plugin root (skills, commands, hooks,
|
|
5
|
+
* etc. live INSIDE the plugin dir, so a content fingerprint covers them).
|
|
6
|
+
*
|
|
7
|
+
* Not tracked in v1 manifests; same one-time re-sync trade-off as workflows.
|
|
8
|
+
*/
|
|
9
|
+
import type { PluginEntry } from '../types.js';
|
|
10
|
+
import type { TypedResourceChecker } from './types.js';
|
|
11
|
+
export declare const pluginsChecker: TypedResourceChecker<PluginEntry>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugins staleness — one directory per plugin, marker file is
|
|
3
|
+
* `.claude-plugin/plugin.json`. First-wins across project > user > system
|
|
4
|
+
* > extras. Fingerprints the entire plugin root (skills, commands, hooks,
|
|
5
|
+
* etc. live INSIDE the plugin dir, so a content fingerprint covers them).
|
|
6
|
+
*
|
|
7
|
+
* Not tracked in v1 manifests; same one-time re-sync trade-off as workflows.
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import { firstWinsLayers, listAcrossLayers, resolveByName } from '../layers.js';
|
|
12
|
+
import { fingerprintDir, isDirStale } from '../fingerprint.js';
|
|
13
|
+
function isPluginDir(full) {
|
|
14
|
+
try {
|
|
15
|
+
return fs.statSync(full).isDirectory()
|
|
16
|
+
&& fs.existsSync(path.join(full, '.claude-plugin', 'plugin.json'));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export const pluginsChecker = {
|
|
23
|
+
type: 'plugins',
|
|
24
|
+
listNames(cwd) {
|
|
25
|
+
return listAcrossLayers(firstWinsLayers(cwd), 'plugins', (_, full) => isPluginDir(full));
|
|
26
|
+
},
|
|
27
|
+
build(name, cwd) {
|
|
28
|
+
const resolved = resolveByName(firstWinsLayers(cwd), path.join('plugins', name), isPluginDir);
|
|
29
|
+
if (!resolved)
|
|
30
|
+
return null;
|
|
31
|
+
return { dirPath: resolved.path, files: fingerprintDir(resolved.path) };
|
|
32
|
+
},
|
|
33
|
+
isFresh(name, stored, cwd) {
|
|
34
|
+
const resolved = resolveByName(firstWinsLayers(cwd), path.join('plugins', name), isPluginDir);
|
|
35
|
+
if (!resolved)
|
|
36
|
+
return false;
|
|
37
|
+
return !isDirStale(stored.dirPath, stored.files, resolved.path);
|
|
38
|
+
},
|
|
39
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rules staleness — composed from a layered `rules.yaml` preset definition
|
|
3
|
+
* plus per-layer `subrules/<name>.md` fragments. Fingerprints exactly the
|
|
4
|
+
* source files that contribute to the active preset's composed output.
|
|
5
|
+
*
|
|
6
|
+
* Bug-fixed from v1: the old `resolveRuleFile` looked for `rules/<preset>.md`,
|
|
7
|
+
* a path that never exists (presets live in `rules.yaml`, fragments live in
|
|
8
|
+
* `subrules/`). That made the rules section always report stale. This module
|
|
9
|
+
* uses `composeRulesFromState` to discover the actual source file set per
|
|
10
|
+
* preset/cwd, so freshness reflects real source changes.
|
|
11
|
+
*
|
|
12
|
+
* Special-cased vs. the other checkers: agent + version are needed to read
|
|
13
|
+
* the active preset, so this module doesn't conform to ResourceChecker. The
|
|
14
|
+
* aggregator wires it up explicitly.
|
|
15
|
+
*/
|
|
16
|
+
import type { AgentId } from '../../types.js';
|
|
17
|
+
import type { RulesEntry } from '../types.js';
|
|
18
|
+
export declare function buildRules(agent: AgentId, version: string, cwd: string): RulesEntry;
|
|
19
|
+
export declare function isRulesStale(stored: RulesEntry, agent: AgentId, version: string, cwd: string): boolean;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rules staleness — composed from a layered `rules.yaml` preset definition
|
|
3
|
+
* plus per-layer `subrules/<name>.md` fragments. Fingerprints exactly the
|
|
4
|
+
* source files that contribute to the active preset's composed output.
|
|
5
|
+
*
|
|
6
|
+
* Bug-fixed from v1: the old `resolveRuleFile` looked for `rules/<preset>.md`,
|
|
7
|
+
* a path that never exists (presets live in `rules.yaml`, fragments live in
|
|
8
|
+
* `subrules/`). That made the rules section always report stale. This module
|
|
9
|
+
* uses `composeRulesFromState` to discover the actual source file set per
|
|
10
|
+
* preset/cwd, so freshness reflects real source changes.
|
|
11
|
+
*
|
|
12
|
+
* Special-cased vs. the other checkers: agent + version are needed to read
|
|
13
|
+
* the active preset, so this module doesn't conform to ResourceChecker. The
|
|
14
|
+
* aggregator wires it up explicitly.
|
|
15
|
+
*/
|
|
16
|
+
import * as fs from 'fs';
|
|
17
|
+
import * as path from 'path';
|
|
18
|
+
import { fingerprintFile, isFileStale } from '../fingerprint.js';
|
|
19
|
+
import { composeRulesFromState } from '../../rules/compose.js';
|
|
20
|
+
import { getActiveRulesPreset, getUserRulesDir, getResolvedRulesDir, getProjectAgentsDir, getEnabledExtraRepos, } from '../../state.js';
|
|
21
|
+
function rulesDirForLayer(scope, cwd) {
|
|
22
|
+
if (scope === 'project') {
|
|
23
|
+
const proj = getProjectAgentsDir(cwd);
|
|
24
|
+
return proj ? path.join(proj, 'rules') : null;
|
|
25
|
+
}
|
|
26
|
+
if (scope === 'user')
|
|
27
|
+
return getUserRulesDir();
|
|
28
|
+
if (scope === 'system')
|
|
29
|
+
return getResolvedRulesDir();
|
|
30
|
+
// extra: first registered extra repo's rules dir. The composer doesn't
|
|
31
|
+
// disambiguate multi-extra rules.yaml today, so we don't either.
|
|
32
|
+
const extras = getEnabledExtraRepos();
|
|
33
|
+
return extras.length > 0 ? path.join(extras[0].dir, 'rules') : null;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the set of source files contributing to the active preset's output.
|
|
37
|
+
* Keys are relative paths within the rules dir (stable across machines).
|
|
38
|
+
* Values are absolute current paths.
|
|
39
|
+
*/
|
|
40
|
+
function activeSources(agent, version, cwd) {
|
|
41
|
+
const result = {};
|
|
42
|
+
let compose;
|
|
43
|
+
try {
|
|
44
|
+
const preset = getActiveRulesPreset(agent, version);
|
|
45
|
+
compose = composeRulesFromState({ preset, cwd });
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
const yamlDir = rulesDirForLayer(compose.presetLayer, cwd);
|
|
51
|
+
if (yamlDir) {
|
|
52
|
+
const yamlPath = path.join(yamlDir, 'rules.yaml');
|
|
53
|
+
if (fs.existsSync(yamlPath))
|
|
54
|
+
result['rules.yaml'] = yamlPath;
|
|
55
|
+
}
|
|
56
|
+
for (const sub of compose.subrules) {
|
|
57
|
+
result[`subrules/${sub.name}.md`] = sub.sourcePath;
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
export function buildRules(agent, version, cwd) {
|
|
62
|
+
const files = {};
|
|
63
|
+
for (const [key, srcPath] of Object.entries(activeSources(agent, version, cwd))) {
|
|
64
|
+
const fp = fingerprintFile(srcPath);
|
|
65
|
+
if (fp)
|
|
66
|
+
files[key] = { source: fp };
|
|
67
|
+
}
|
|
68
|
+
return { files };
|
|
69
|
+
}
|
|
70
|
+
export function isRulesStale(stored, agent, version, cwd) {
|
|
71
|
+
const current = activeSources(agent, version, cwd);
|
|
72
|
+
const storedKeys = Object.keys(stored.files).sort();
|
|
73
|
+
const currentKeys = Object.keys(current).sort();
|
|
74
|
+
if (storedKeys.length !== currentKeys.length)
|
|
75
|
+
return true;
|
|
76
|
+
for (let i = 0; i < storedKeys.length; i++) {
|
|
77
|
+
if (storedKeys[i] !== currentKeys[i])
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
for (const [key, srcPath] of Object.entries(current)) {
|
|
81
|
+
const entry = stored.files[key];
|
|
82
|
+
if (!entry || isFileStale(entry.source, srcPath))
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills staleness — one directory per skill (must contain SKILL.md),
|
|
3
|
+
* first-wins across project > user > system > extras.
|
|
4
|
+
*/
|
|
5
|
+
import type { DirEntry } from '../types.js';
|
|
6
|
+
import type { TypedResourceChecker } from './types.js';
|
|
7
|
+
export declare const skillsChecker: TypedResourceChecker<DirEntry>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills staleness — one directory per skill (must contain SKILL.md),
|
|
3
|
+
* first-wins across project > user > system > extras.
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import { firstWinsLayers, listAcrossLayers, resolveByName } from '../layers.js';
|
|
8
|
+
import { fingerprintDir, isDirStale } from '../fingerprint.js';
|
|
9
|
+
function isSkillDir(full) {
|
|
10
|
+
try {
|
|
11
|
+
return fs.statSync(full).isDirectory() && fs.existsSync(path.join(full, 'SKILL.md'));
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export const skillsChecker = {
|
|
18
|
+
type: 'skills',
|
|
19
|
+
listNames(cwd) {
|
|
20
|
+
return listAcrossLayers(firstWinsLayers(cwd), 'skills', (_, full) => isSkillDir(full));
|
|
21
|
+
},
|
|
22
|
+
build(name, cwd) {
|
|
23
|
+
const resolved = resolveByName(firstWinsLayers(cwd), path.join('skills', name), isSkillDir);
|
|
24
|
+
if (!resolved)
|
|
25
|
+
return null;
|
|
26
|
+
return { dirPath: resolved.path, files: fingerprintDir(resolved.path) };
|
|
27
|
+
},
|
|
28
|
+
isFresh(name, stored, cwd) {
|
|
29
|
+
const resolved = resolveByName(firstWinsLayers(cwd), path.join('skills', name), isSkillDir);
|
|
30
|
+
if (!resolved)
|
|
31
|
+
return false;
|
|
32
|
+
return !isDirStale(stored.dirPath, stored.files, resolved.path);
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subagents staleness — one directory per subagent (must contain AGENT.md),
|
|
3
|
+
* first-wins across project > user > system > extras.
|
|
4
|
+
*
|
|
5
|
+
* Bug-fixed from v1: the old manifest derived its name list from
|
|
6
|
+
* `listInstalledSubagents()` which only walks user + system. With project
|
|
7
|
+
* subagents in `available.subagents`, the name-set diff always flipped to
|
|
8
|
+
* "stale". This checker walks all four layers consistently.
|
|
9
|
+
*/
|
|
10
|
+
import type { DirEntry } from '../types.js';
|
|
11
|
+
import type { TypedResourceChecker } from './types.js';
|
|
12
|
+
export declare const subagentsChecker: TypedResourceChecker<DirEntry>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subagents staleness — one directory per subagent (must contain AGENT.md),
|
|
3
|
+
* first-wins across project > user > system > extras.
|
|
4
|
+
*
|
|
5
|
+
* Bug-fixed from v1: the old manifest derived its name list from
|
|
6
|
+
* `listInstalledSubagents()` which only walks user + system. With project
|
|
7
|
+
* subagents in `available.subagents`, the name-set diff always flipped to
|
|
8
|
+
* "stale". This checker walks all four layers consistently.
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import { firstWinsLayers, listAcrossLayers, resolveByName } from '../layers.js';
|
|
13
|
+
import { fingerprintDir, isDirStale } from '../fingerprint.js';
|
|
14
|
+
function isSubagentDir(full) {
|
|
15
|
+
try {
|
|
16
|
+
return fs.statSync(full).isDirectory() && fs.existsSync(path.join(full, 'AGENT.md'));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export const subagentsChecker = {
|
|
23
|
+
type: 'subagents',
|
|
24
|
+
listNames(cwd) {
|
|
25
|
+
return listAcrossLayers(firstWinsLayers(cwd), 'subagents', (_, full) => isSubagentDir(full));
|
|
26
|
+
},
|
|
27
|
+
build(name, cwd) {
|
|
28
|
+
const resolved = resolveByName(firstWinsLayers(cwd), path.join('subagents', name), isSubagentDir);
|
|
29
|
+
if (!resolved)
|
|
30
|
+
return null;
|
|
31
|
+
return { dirPath: resolved.path, files: fingerprintDir(resolved.path) };
|
|
32
|
+
},
|
|
33
|
+
isFresh(name, stored, cwd) {
|
|
34
|
+
const resolved = resolveByName(firstWinsLayers(cwd), path.join('subagents', name), isSubagentDir);
|
|
35
|
+
if (!resolved)
|
|
36
|
+
return false;
|
|
37
|
+
return !isDirStale(stored.dirPath, stored.files, resolved.path);
|
|
38
|
+
},
|
|
39
|
+
};
|