mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
chore(root): refresh agent skills and docs (#1835)
This commit is contained in:
@@ -10,11 +10,14 @@
|
||||
* 3. Root tsconfig references — every composite project is referenced
|
||||
* 4. Package metadata — non-private packages have required fields
|
||||
* 5. Release-please config — every versioned package is registered
|
||||
* 6. Define imports — no bare side-effect imports from relative paths
|
||||
* 7. i18n locales — tag lists match locale files and generated stubs
|
||||
* 6. Bundled docs — package publishing wires include generated docs
|
||||
* 7. Define imports — no bare side-effect imports from relative paths
|
||||
* 8. i18n locales — tag lists match locale files and generated stubs
|
||||
* 9. Agent context — portable skill metadata, compatibility imports, and budgets
|
||||
* 10. Internal records — organized design docs, frontmatter, and lifecycle status
|
||||
*/
|
||||
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { existsSync, readdirSync, readFileSync, realpathSync } from 'node:fs';
|
||||
import { dirname, join, resolve, sep } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
@@ -325,7 +328,7 @@ function checkDefineImports() {
|
||||
return { ok: warnings.length === 0, warnings };
|
||||
}
|
||||
|
||||
// ── Check 7: i18n locale consistency ─────────────────────────────────────────
|
||||
// ── Check 8: i18n locale consistency ─────────────────────────────────────────
|
||||
|
||||
const GENERATED_I18N_HEADER = '/** Generated by packages/core/scripts/generate-i18n-locales.ts — do not edit. */';
|
||||
|
||||
@@ -451,6 +454,370 @@ function checkI18nLocales() {
|
||||
return { ok: warnings.length === 0, warnings };
|
||||
}
|
||||
|
||||
// ── Check 9: Agent context consistency ─────────────────────────────────────
|
||||
|
||||
// These are conservative repository budgets, below the host-level ceilings.
|
||||
// Token counts are estimates using four UTF-8 bytes per token; the byte limit
|
||||
// is the enforceable value and avoids adding a tokenizer dependency.
|
||||
const AGENT_DOC_MAX_LINES = 200;
|
||||
const AGENT_DOC_MAX_BYTES = 12_000;
|
||||
const AGENT_CHAIN_MAX_BYTES = 24_000;
|
||||
const SKILL_MAX_LINES = 200;
|
||||
const SKILL_MAX_BYTES = 10_000;
|
||||
const SKILL_RESOURCE_MAX_LINES = 500;
|
||||
const SKILL_RESOURCE_MAX_BYTES = 20_000;
|
||||
const RESTORED_SPF_RESOURCE_MAX_LINES = 1_200;
|
||||
const RESTORED_SPF_RESOURCE_MAX_BYTES = 60_000;
|
||||
const SKILL_METADATA_MAX_BYTES = 6_000;
|
||||
const RESTORED_SPF_SKILLS = new Set([
|
||||
'change-spf-behavior',
|
||||
'create-spf-behavior',
|
||||
'document-spf-feature',
|
||||
'document-spf-use-case',
|
||||
'implement-spf-feature',
|
||||
'implement-spf-use-case',
|
||||
]);
|
||||
const PORTABLE_SKILL_FIELDS = new Set(['name', 'description']);
|
||||
const SKILL_ACTIONS = new Set([
|
||||
'build',
|
||||
'change',
|
||||
'commit',
|
||||
'create',
|
||||
'design',
|
||||
'document',
|
||||
'implement',
|
||||
'investigate',
|
||||
'maintain',
|
||||
'migrate',
|
||||
'review',
|
||||
'write',
|
||||
]);
|
||||
|
||||
function lineCount(text) {
|
||||
return text === '' ? 0 : text.split(/\r?\n/).length;
|
||||
}
|
||||
|
||||
function estimatedTokens(bytes) {
|
||||
return Math.ceil(bytes / 4);
|
||||
}
|
||||
|
||||
function relativePath(path) {
|
||||
return path.slice(ROOT.length + 1);
|
||||
}
|
||||
|
||||
function listFiles(dir, predicate, results = []) {
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
if (['.git', '.agents', '.opencode', 'node_modules', 'dist', 'coverage'].includes(entry.name)) {
|
||||
continue;
|
||||
}
|
||||
const full = join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
listFiles(full, predicate, results);
|
||||
} else if (predicate(full)) {
|
||||
results.push(full);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function checkFileBudget(path, maxLines, maxBytes, warnings) {
|
||||
const source = readText(path);
|
||||
const lines = lineCount(source);
|
||||
const bytes = Buffer.byteLength(source);
|
||||
const relative = relativePath(path);
|
||||
|
||||
if (lines > maxLines) {
|
||||
warnings.push(`${relative}: ${lines} lines exceeds ${maxLines}`);
|
||||
}
|
||||
if (bytes > maxBytes) {
|
||||
warnings.push(
|
||||
`${relative}: ~${estimatedTokens(bytes)} tokens (${bytes} bytes) exceeds ~${estimatedTokens(maxBytes)} tokens`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function checkAgentContext() {
|
||||
const warnings = [];
|
||||
const agentDocs = listFiles(ROOT, (path) => path.endsWith('/AGENTS.md'));
|
||||
const claudeDocs = listFiles(ROOT, (path) => path.endsWith('/CLAUDE.md'));
|
||||
const gitignoreRules = new Set(
|
||||
readText(join(ROOT, '.gitignore'))
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line && !line.startsWith('#'))
|
||||
);
|
||||
|
||||
for (const rule of ['/.claude/skills', '/.claude/plans', '/.opencode']) {
|
||||
if (!gitignoreRules.has(rule)) {
|
||||
warnings.push(`.gitignore: missing generated agent path ${rule}`);
|
||||
}
|
||||
}
|
||||
for (const rule of ['/.agents/skills', '/.agents/skills/', '.agents/skills', '.agents/skills/']) {
|
||||
if (gitignoreRules.has(rule)) {
|
||||
warnings.push(`.gitignore: canonical .agents/skills catalog must not be ignored by ${rule}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!agentDocs.includes(join(ROOT, 'AGENTS.md'))) {
|
||||
warnings.push('Missing canonical root AGENTS.md');
|
||||
}
|
||||
|
||||
for (const path of agentDocs) {
|
||||
checkFileBudget(path, AGENT_DOC_MAX_LINES, AGENT_DOC_MAX_BYTES, warnings);
|
||||
|
||||
const directory = dirname(path);
|
||||
const chainBytes = agentDocs
|
||||
.filter((candidate) => {
|
||||
const candidateDirectory = dirname(candidate);
|
||||
return directory === candidateDirectory || directory.startsWith(`${candidateDirectory}${sep}`);
|
||||
})
|
||||
.reduce((total, candidate) => total + Buffer.byteLength(readText(candidate)), 0);
|
||||
if (chainBytes > AGENT_CHAIN_MAX_BYTES) {
|
||||
warnings.push(
|
||||
`${relativePath(path)} chain: ~${estimatedTokens(chainBytes)} tokens (${chainBytes} bytes) exceeds ` +
|
||||
`~${estimatedTokens(AGENT_CHAIN_MAX_BYTES)} tokens`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const path of claudeDocs) {
|
||||
const siblingAgents = join(dirname(path), 'AGENTS.md');
|
||||
const relative = relativePath(path);
|
||||
if (!existsSync(siblingAgents)) {
|
||||
warnings.push(`${relative}: missing sibling AGENTS.md`);
|
||||
}
|
||||
if (readText(path).trim() !== '@AGENTS.md') {
|
||||
warnings.push(`${relative}: must contain only \`@AGENTS.md\` to avoid duplicated instructions`);
|
||||
}
|
||||
}
|
||||
|
||||
const agentsDir = join(ROOT, '.agents');
|
||||
const skillsDir = join(agentsDir, 'skills');
|
||||
for (const alias of [join(ROOT, '.claude/skills'), join(ROOT, '.opencode/skills')]) {
|
||||
if (!existsSync(alias)) {
|
||||
warnings.push(`${relativePath(alias)}: missing compatibility alias to skills`);
|
||||
} else if (realpathSync(alias) !== realpathSync(skillsDir)) {
|
||||
warnings.push(`${relativePath(alias)}: must resolve to skills`);
|
||||
}
|
||||
}
|
||||
|
||||
const plansDir = join(agentsDir, 'plans');
|
||||
const claudePlans = join(ROOT, '.claude/plans');
|
||||
if (!existsSync(claudePlans)) {
|
||||
warnings.push('.claude/plans: missing compatibility alias to .agents/plans');
|
||||
} else if (realpathSync(claudePlans) !== realpathSync(plansDir)) {
|
||||
warnings.push('.claude/plans: must resolve to .agents/plans');
|
||||
}
|
||||
|
||||
const canonicalSkillDirs = [];
|
||||
const skillNames = new Set();
|
||||
for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory()) {
|
||||
warnings.push(`.agents/skills/${entry.name}: only skill directories are allowed at the catalog root`);
|
||||
continue;
|
||||
}
|
||||
const skillDir = join(skillsDir, entry.name);
|
||||
if (!existsSync(join(skillDir, 'SKILL.md'))) {
|
||||
warnings.push(`.agents/skills/${entry.name}: missing SKILL.md`);
|
||||
continue;
|
||||
}
|
||||
skillNames.add(entry.name);
|
||||
canonicalSkillDirs.push(skillDir);
|
||||
}
|
||||
|
||||
const canonicalSkillFiles = new Set(canonicalSkillDirs.map((dir) => join(dir, 'SKILL.md')));
|
||||
for (const path of listFiles(skillsDir, (path) => path.endsWith('/SKILL.md'))) {
|
||||
if (!canonicalSkillFiles.has(path)) {
|
||||
warnings.push(`${relativePath(path)}: skills must be direct children of .agents/skills/`);
|
||||
}
|
||||
}
|
||||
|
||||
const skillFiles = canonicalSkillDirs.map((dir) => join(dir, 'SKILL.md'));
|
||||
let metadataBytes = 0;
|
||||
|
||||
for (const path of skillFiles) {
|
||||
checkFileBudget(path, SKILL_MAX_LINES, SKILL_MAX_BYTES, warnings);
|
||||
const source = readText(path);
|
||||
const relative = relativePath(path);
|
||||
const frontmatterMatch = source.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
||||
if (!frontmatterMatch) {
|
||||
warnings.push(`${relative}: missing YAML frontmatter`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const frontmatter = frontmatterMatch[1];
|
||||
metadataBytes += Buffer.byteLength(frontmatter);
|
||||
const fields = [...frontmatter.matchAll(/^([A-Za-z][A-Za-z0-9-]*):/gm)].map((match) => match[1]);
|
||||
for (const field of fields) {
|
||||
if (!PORTABLE_SKILL_FIELDS.has(field)) {
|
||||
warnings.push(`${relative}: non-portable frontmatter field "${field}"`);
|
||||
}
|
||||
}
|
||||
|
||||
const name = frontmatter
|
||||
.match(/^name:\s*([^\r\n]+)$/m)?.[1]
|
||||
.trim()
|
||||
.replace(/^['"]|['"]$/g, '');
|
||||
const directoryName = relative.split('/').at(-2);
|
||||
if (!name) {
|
||||
warnings.push(`${relative}: missing skill name`);
|
||||
} else if (name !== directoryName) {
|
||||
warnings.push(`${relative}: name "${name}" must match directory "${directoryName}"`);
|
||||
} else if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name) || name.length > 64) {
|
||||
warnings.push(`${relative}: invalid Agent Skills name "${name}"`);
|
||||
} else if (!SKILL_ACTIONS.has(name.split('-')[0])) {
|
||||
warnings.push(`${relative}: skill name "${name}" must start with a clear action verb`);
|
||||
}
|
||||
|
||||
const description = frontmatter.match(/^description:\s*(.+)$/m)?.[1].trim();
|
||||
if (!description) {
|
||||
warnings.push(`${relative}: missing skill description`);
|
||||
} else if (description.length > 1024 || !/\bUse (?:for|when)\b/.test(description)) {
|
||||
warnings.push(`${relative}: description must concisely say what the skill does and when to use it`);
|
||||
}
|
||||
|
||||
if (!/^## Example\r?$/m.test(source) || !/^Input: /m.test(source) || !/^Output: /m.test(source)) {
|
||||
warnings.push(`${relative}: include one compact Example with Input and Output`);
|
||||
}
|
||||
}
|
||||
|
||||
if (metadataBytes > SKILL_METADATA_MAX_BYTES) {
|
||||
warnings.push(
|
||||
`.agents/skills metadata: ~${estimatedTokens(metadataBytes)} tokens (${metadataBytes} bytes) exceeds ` +
|
||||
`~${estimatedTokens(SKILL_METADATA_MAX_BYTES)} tokens`
|
||||
);
|
||||
}
|
||||
|
||||
for (const skillDir of canonicalSkillDirs) {
|
||||
const owner = relativePath(skillDir).split('/').at(-1);
|
||||
const restoredSpfWorkflow = RESTORED_SPF_SKILLS.has(owner);
|
||||
const resourceMaxLines = restoredSpfWorkflow ? RESTORED_SPF_RESOURCE_MAX_LINES : SKILL_RESOURCE_MAX_LINES;
|
||||
const resourceMaxBytes = restoredSpfWorkflow ? RESTORED_SPF_RESOURCE_MAX_BYTES : SKILL_RESOURCE_MAX_BYTES;
|
||||
|
||||
for (const path of listFiles(skillDir, (path) => path.endsWith('.md') && !path.endsWith('/SKILL.md'))) {
|
||||
checkFileBudget(path, resourceMaxLines, resourceMaxBytes, warnings);
|
||||
}
|
||||
|
||||
for (const path of listFiles(skillDir, (path) => path.endsWith('.md'))) {
|
||||
const source = readText(path);
|
||||
for (const name of skillNames) {
|
||||
if (name !== owner && source.includes(`\`${name}\``)) {
|
||||
warnings.push(`${relativePath(path)}: must not explicitly load or route to sibling skill "${name}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { ok: warnings.length === 0, warnings };
|
||||
}
|
||||
|
||||
// ── Check 10: Internal record consistency ──────────────────────────────────
|
||||
|
||||
const DESIGN_STATUSES = new Set(['draft', 'decided', 'active', 'partial', 'implemented', 'superseded', 'reference']);
|
||||
const INTERNAL_RECORD_MAX_LINES = 160;
|
||||
const RESTORED_SPF_RECORD_MAX_LINES = 1_000;
|
||||
|
||||
function recordFrontmatter(path, warnings) {
|
||||
const relative = relativePath(path);
|
||||
const match = readText(path).match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
||||
if (!match) {
|
||||
warnings.push(`${relative}: missing YAML frontmatter`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
[...match[1].matchAll(/^([A-Za-z][A-Za-z0-9-]*):\s*(.*?)\s*$/gm)].map((field) => [field[1], field[2]])
|
||||
);
|
||||
}
|
||||
|
||||
function checkLocalMarkdownLinks(path, warnings) {
|
||||
const source = readText(path);
|
||||
|
||||
for (const match of source.matchAll(/!?\[[^\]]*\]\(([^)]+)\)/g)) {
|
||||
let target = match[1].trim();
|
||||
if (target.startsWith('<') && target.endsWith('>')) target = target.slice(1, -1);
|
||||
if (!target || target.startsWith('#') || /^[a-z][a-z0-9+.-]*:/i.test(target)) continue;
|
||||
|
||||
target = target
|
||||
.split(/\s+["']/)[0]
|
||||
.split('#')[0]
|
||||
.split('?')[0];
|
||||
if (!target) continue;
|
||||
|
||||
try {
|
||||
target = decodeURIComponent(target);
|
||||
} catch {
|
||||
warnings.push(`${relativePath(path)}: invalid encoded Markdown link "${match[1]}"`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const resolved = target.startsWith('/') ? join(ROOT, target.slice(1)) : resolve(dirname(path), target);
|
||||
if (!existsSync(resolved)) {
|
||||
warnings.push(`${relativePath(path)}: broken local Markdown link "${match[1]}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkInternalRecords() {
|
||||
const warnings = [];
|
||||
const designDir = join(ROOT, 'internal/design');
|
||||
const designReadme = join(designDir, 'README.md');
|
||||
|
||||
for (const entry of readdirSync(designDir, { withFileTypes: true })) {
|
||||
if (entry.isFile() && entry.name.endsWith('.md') && join(designDir, entry.name) !== designReadme) {
|
||||
warnings.push(`internal/design/${entry.name}: place design records in an area directory`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const path of listFiles(designDir, (path) => path.endsWith('.md'))) {
|
||||
if (path === designReadme) continue;
|
||||
const recordMaxLines = path.startsWith(`${join(designDir, 'spf')}${sep}`)
|
||||
? RESTORED_SPF_RECORD_MAX_LINES
|
||||
: INTERNAL_RECORD_MAX_LINES;
|
||||
checkFileBudget(path, recordMaxLines, Number.POSITIVE_INFINITY, warnings);
|
||||
checkLocalMarkdownLinks(path, warnings);
|
||||
const frontmatter = recordFrontmatter(path, warnings);
|
||||
if (!frontmatter) continue;
|
||||
if (!DESIGN_STATUSES.has(frontmatter.status)) {
|
||||
warnings.push(`${relativePath(path)}: unknown design status "${frontmatter.status ?? 'missing'}"`);
|
||||
}
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(frontmatter.date ?? '')) {
|
||||
warnings.push(`${relativePath(path)}: date must use YYYY-MM-DD`);
|
||||
}
|
||||
}
|
||||
|
||||
const decisionsDir = join(ROOT, 'internal/decisions');
|
||||
const decisionsReadme = join(decisionsDir, 'README.md');
|
||||
for (const entry of readdirSync(decisionsDir, { withFileTypes: true })) {
|
||||
if (entry.isFile() && entry.name.endsWith('.md') && join(decisionsDir, entry.name) !== decisionsReadme) {
|
||||
warnings.push(`internal/decisions/${entry.name}: place decision records in an area directory`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const path of listFiles(decisionsDir, (path) => path.endsWith('.md') && !path.endsWith('/README.md'))) {
|
||||
const recordMaxLines = path.startsWith(`${join(decisionsDir, 'spf')}${sep}`)
|
||||
? RESTORED_SPF_RECORD_MAX_LINES
|
||||
: INTERNAL_RECORD_MAX_LINES;
|
||||
checkFileBudget(path, recordMaxLines, Number.POSITIVE_INFINITY, warnings);
|
||||
checkLocalMarkdownLinks(path, warnings);
|
||||
const frontmatter = recordFrontmatter(path, warnings);
|
||||
if (!frontmatter) continue;
|
||||
if (frontmatter.status !== 'decided') {
|
||||
warnings.push(`${relativePath(path)}: tactical decisions must use status "decided"`);
|
||||
}
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(frontmatter.date ?? '')) {
|
||||
warnings.push(`${relativePath(path)}: date must use YYYY-MM-DD`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const path of listFiles(join(ROOT, 'rfc'), (path) => path.endsWith('.md'))) {
|
||||
checkFileBudget(path, INTERNAL_RECORD_MAX_LINES, Number.POSITIVE_INFINITY, warnings);
|
||||
checkLocalMarkdownLinks(path, warnings);
|
||||
}
|
||||
|
||||
return { ok: warnings.length === 0, warnings };
|
||||
}
|
||||
|
||||
// ── Main ────────────────────────────────────────────────────────────────────
|
||||
|
||||
const checks = [
|
||||
@@ -462,6 +829,8 @@ const checks = [
|
||||
{ name: 'Bundled docs publishing', fn: checkBundledDocs },
|
||||
{ name: 'Define imports', fn: checkDefineImports },
|
||||
{ name: 'i18n locales', fn: checkI18nLocales },
|
||||
{ name: 'Agent context', fn: checkAgentContext },
|
||||
{ name: 'Internal records', fn: checkInternalRecords },
|
||||
];
|
||||
|
||||
let failed = 0;
|
||||
|
||||
@@ -1,37 +1,56 @@
|
||||
/**
|
||||
* Creates symlink aliases so that AI coding tools other than Claude Code
|
||||
* (e.g., OpenCode, Cursor) can discover project instructions.
|
||||
* Exposes the checked-in, host-neutral skill catalog through client-specific
|
||||
* discovery paths:
|
||||
*
|
||||
* Aliases created:
|
||||
* .opencode → .claude (directory)
|
||||
* .agents → .claude (directory)
|
||||
* AGENTS.md → CLAUDE.md (file)
|
||||
* .agents/skills/<skill-name>/SKILL.md (source)
|
||||
* .claude/skills (generated junction)
|
||||
* .claude/plans (generated junction)
|
||||
* .opencode (generated junction)
|
||||
*
|
||||
* Cross-platform notes:
|
||||
* - Directory symlinks use 'junction' type, which works on Windows without
|
||||
* elevated privileges or Developer Mode.
|
||||
* - File symlinks ('file' type) require elevated privileges or Developer Mode
|
||||
* on Windows. If creation fails, we log a warning instead of crashing
|
||||
* `pnpm install`. The alias is optional — the canonical files still work.
|
||||
* Directory junctions work on Windows without elevated privileges. Failures
|
||||
* warn instead of breaking `pnpm install`.
|
||||
*/
|
||||
import { existsSync, symlinkSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { lstatSync, mkdirSync, readlinkSync, symlinkSync, unlinkSync } from 'node:fs';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
|
||||
const root = resolve(import.meta.dirname, '../..');
|
||||
const agentsDir = join(root, '.agents');
|
||||
const skillsDir = join(agentsDir, 'skills');
|
||||
|
||||
const aliases = [
|
||||
{ target: '.claude', path: '.opencode', type: 'junction' },
|
||||
{ target: '.claude', path: '.agents', type: 'junction' },
|
||||
{ target: 'CLAUDE.md', path: 'AGENTS.md', type: 'file' },
|
||||
];
|
||||
|
||||
for (const alias of aliases) {
|
||||
const fullPath = resolve(root, alias.path);
|
||||
if (existsSync(fullPath)) continue;
|
||||
|
||||
function linkState(path) {
|
||||
try {
|
||||
symlinkSync(alias.target, fullPath, alias.type);
|
||||
return lstatSync(path);
|
||||
} catch {
|
||||
console.warn(`warning: could not create symlink ${alias.path} → ${alias.target}`);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function ensureAlias(relativePath, target) {
|
||||
const path = resolve(root, relativePath);
|
||||
const state = linkState(path);
|
||||
|
||||
if (state?.isSymbolicLink()) {
|
||||
try {
|
||||
if (resolve(dirname(path), readlinkSync(path)) === resolve(target)) return;
|
||||
} catch {
|
||||
// Replace a dangling generated link below.
|
||||
}
|
||||
unlinkSync(path);
|
||||
} else if (state) {
|
||||
console.warn(`warning: refusing to replace non-generated ${relativePath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
try {
|
||||
symlinkSync(target, path, 'junction');
|
||||
} catch {
|
||||
console.warn(`warning: could not create alias ${relativePath} → ${target}`);
|
||||
}
|
||||
}
|
||||
|
||||
mkdirSync(join(agentsDir, 'plans'), { recursive: true });
|
||||
|
||||
ensureAlias('.claude/skills', skillsDir);
|
||||
ensureAlias('.claude/plans', join(agentsDir, 'plans'));
|
||||
ensureAlias('.opencode', agentsDir);
|
||||
|
||||
Reference in New Issue
Block a user