@pharn-dev/pharn 0.6.0 → 0.7.0

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.
Files changed (2) hide show
  1. package/dist/index.js +11 -9
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6,26 +6,28 @@ Wait for it to finish, or delete ${LOCK_FILE} if you are sure no pharn process i
6
6
  `)}finally{closeSync5(fd)}return!0}function sweepCorpses(cwd){let names;try{names=readdirSync3(cwd)}catch{return}for(let name of names)if(CORPSE_RE.test(name))try{let corpse=safeJoin(cwd,name);Date.now()-statSync(corpse).mtimeMs>STALE_MS&&rmSync2(corpse,{force:!0})}catch{}}function breakStaleLock(cwd,command,observedRaw){let corpse=safeJoin(cwd,`${LOCK_FILE}.${process.pid}.${randomBytes(4).toString("hex")}`);try{renameSync(lockPath(cwd),corpse)}catch{throw new ProjectLockedError(refusal(readPayload(cwd)))}try{if(readRawAt(corpse)!==observedRaw){try{linkSync(corpse,lockPath(cwd))}catch{}throw new ProjectLockedError(refusal(readPayload(cwd)))}if(!tryCreate(cwd,command))throw new ProjectLockedError(refusal(readPayload(cwd)));sweepCorpses(cwd)}finally{try{rmSync2(corpse,{force:!0})}catch{}}}function release(cwd){try{let held=readPayload(cwd);if(held!==null&&(held.pid!==process.pid||held.host!==hostname()))return;rmSync2(lockPath(cwd),{force:!0})}catch{}}async function withProjectLock(cwd,command,fn){if(!tryCreate(cwd,command)){let observedRaw=readRawAt(lockPath(cwd)),held=observedRaw===null?null:parsePayload(observedRaw);if(!isStale(held,lockAgeMs(cwd)))throw new ProjectLockedError(refusal(held));breakStaleLock(cwd,command,observedRaw)}let onExit=code=>{release(cwd),code===0&&(process.exitCode=130,sayInterrupted(command))};process.on("exit",onExit);let offSignal=onFatalSignal(()=>{release(cwd),sayInterrupted(command)});try{return await fn()}finally{offSignal(),process.off("exit",onExit),release(cwd)}}function sayInterrupted(command){try{process.stderr.write(`pharn ${command} was interrupted while writing to this project \u2014 it may be partially updated. Re-run \`pharn ${command}\`.
7
7
  `)}catch{}}var HTTPS_VARS=["https_proxy","HTTPS_PROXY"],HTTP_VARS=["http_proxy","HTTP_PROXY"],READ_SPELLINGS=new Set([...HTTPS_VARS,...HTTP_VARS]);function currentProxyRuntime(){return{supportsEnvProxy:process.allowedNodeEnvironmentFlags.has("--use-env-proxy"),execArgv:process.execArgv,nodeVersion:process.versions.node}}var FLAG_RE=/^--(no[-_])?use[-_]env[-_]proxy(?:=.*)?$/;function lastFlag(env,execArgv){let state=null;for(let token of[...(env.NODE_OPTIONS??"").split(/\s+/),...execArgv]){let m=FLAG_RE.exec(token);m&&(state=m[1]===void 0)}return state}function isEarly24(nodeVersion){let[major,minor]=nodeVersion.split(".").map(Number);return major===24&&minor!==void 0&&minor<=4}function envProxyState(env,runtime){if(runtime.supportsEnvProxy){let flag=lastFlag(env,runtime.execArgv);return flag===!1?{envProxy:"available",optedOut:!0}:{envProxy:flag===!0||env.NODE_USE_ENV_PROXY==="1"?"on":"available"}}return isEarly24(runtime.nodeVersion)?{envProxy:isSet(env.NODE_USE_ENV_PROXY)?"on":"available"}:{envProxy:"unsupported"}}function isSet(value){return value!==void 0&&value!==""}function firstPresent(env,names){for(let name of names){let value=env[name];if(value!==void 0)return{name,value}}return null}function readVariable(env){let https=firstPresent(env,HTTPS_VARS);if(https!==null&&https.value!=="")return https;let http=firstPresent(env,HTTP_VARS);return http!==null&&http.value!==""?http:null}function ignoredSpelling(env){let name=Object.keys(env).filter(key2=>!READ_SPELLINGS.has(key2)&&READ_SPELLINGS.has(key2.toLowerCase())&&isSet(env[key2])).sort()[0];return name===void 0?null:{name,value:env[name]}}function detectProxyNotice(env,runtime=currentProxyRuntime()){let read=readVariable(env);if(read!==null)return{...read,...envProxyState(env,runtime)};let ignored=ignoredSpelling(env);return ignored===null?null:{...ignored,envProxy:"ignored-spelling"}}var MAX_RENDERED=120;function redactProxyUrl(value){let url;try{url=new URL(value)}catch{return"(set)"}let auth=url.username!==""||url.password!==""?"***@":"",path=url.pathname==="/"?"":url.pathname,rendered=`${url.protocol}//${auth}${url.host}${path}`;return rendered.length>MAX_RENDERED?`${rendered.slice(0,MAX_RENDERED)}\u2026`:rendered}function proxyNoticeMessage(notice){let shown=`${notice.name} is set (${redactProxyUrl(notice.value)})`;if(notice.envProxy==="on")return`${shown} and NODE_USE_ENV_PROXY / --use-env-proxy is on, so pharn's downloads go through that proxy (Node's fetch then also honours NO_PROXY).`;let direct="The download connects DIRECTLY, and fails if direct egress is blocked (LIMITS.md \xA73a).";if(notice.envProxy==="ignored-spelling")return`${shown}, but pharn will not use it: Node reads only https_proxy, HTTPS_PROXY, http_proxy and HTTP_PROXY, spelled exactly so. ${direct}`;let base=`${shown}, but pharn will not use it: its network calls go through Node's global fetch, which reads no proxy environment variable by default. ${direct}`;return notice.envProxy==="available"?notice.optedOut?`${base} A --no-use-env-proxy flag (in NODE_OPTIONS or on the command line) turns Node's proxy support off \u2014 remove it to route fetch through the proxy.`:`${base} This Node can route fetch through the proxy \u2014 re-run with NODE_USE_ENV_PROXY=1 set.`:`${base} This Node (${process.version}) cannot route fetch through a proxy; Node 22.21+ and 24+ can, with NODE_USE_ENV_PROXY=1.`}import{existsSync as existsSync4}from"node:fs";import{resolve as resolve3}from"node:path";function runGitPrereq(){let cwd=process.cwd();existsSync4(resolve3(cwd,".git"))||fail(`git not found.
8
8
  Run: git init && git add -A && git commit -m 'init'
9
- Then re-run: npx @pharn-dev/pharn init`)}function fail(message){logError(message),process.exit(1)}import{confirm as confirm2,isCancel as isCancel2,log as log3}from"@clack/prompts";import{existsSync as existsSync5,lstatSync as lstatSync3,readdirSync as readdirSync4}from"node:fs";import{join as join3,resolve as resolve4}from"node:path";import{lstatSync as lstatSync2}from"node:fs";function findSymlinkComponent(base,rel){let current="";for(let segment of toPosix(rel).split("/"))if(segment&&(current=current?`${current}/${segment}`:segment,lstatSync2(safeJoin(base,current),{throwIfNoEntry:!1})?.isSymbolicLink()))return current;return null}function findTypeCollision(base,rel){let segments=toPosix(rel).split("/").filter(Boolean),current="";for(let[i,segment]of segments.entries()){current=current?`${current}/${segment}`:segment;let stat=lstatSync2(safeJoin(base,current),{throwIfNoEntry:!1});if(stat===void 0)return null;if(i===segments.length-1?!stat.isFile():!stat.isDirectory())return current}return null}var PHARN_CONFIG_FILE="pharn.config.json";function*walkFiles(dir,prefix=""){for(let entry of readdirSync4(dir,{withFileTypes:!0})){if(entry.isSymbolicLink())continue;let rel=prefix?`${prefix}/${entry.name}`:entry.name;entry.isDirectory()?yield*walkFiles(resolve4(dir,entry.name),rel):yield rel}}function collectExpectedInstallPaths(params){let{repoDir,capabilities,layout}=params,paths=layoutPaths(layout),expected=new Map,add=(rel,repoPath)=>{expected.set(toPosix(rel),repoPath)},addDir=(relDir,keep,validate)=>{let from=safeJoin(repoDir,relDir);if(findSymlinkComponent(repoDir,relDir)===null&&lstatSync3(from,{throwIfNoEntry:!1})?.isDirectory())for(let rel of walkFiles(from))keep&&!keep(rel)||(validate?.(rel),add(join3(relDir,rel),resolve4(from,rel)))},copyNameFloor=dir=>rel=>{let label=`${dir}/${rel}`;assertSafeString(rel,label,COPY_FILENAME_RE),assertNoDotDot(rel,label)};for(let cap of capabilities){let subtree=capabilitySubtree(paths,cap.role);addDir(`${subtree}/${cap.name}`)}addDir(CLAUDE_COMMANDS_DIR,rel=>!rel.includes("/")&&rel.endsWith(".md")&&rel.startsWith(PRODUCT_COMMAND_PREFIX)&&!rel.startsWith(DEV_COMMAND_PREFIX),copyNameFloor(CLAUDE_COMMANDS_DIR)),addDir(CLAUDE_HOOKS_DIR,rel=>!rel.includes("/")&&rel.endsWith(".cjs")&&!rel.endsWith(".test.cjs"),copyNameFloor(CLAUDE_HOOKS_DIR));for(let doc of paths.docs){let from=safeJoin(repoDir,doc);findSymlinkComponent(repoDir,doc)===null&&lstatSync3(from,{throwIfNoEntry:!1})?.isFile()&&add(doc,from)}if(findSymlinkComponent(repoDir,paths.license.from)===null){let licenseFrom=safeJoin(repoDir,paths.license.from);lstatSync3(licenseFrom,{throwIfNoEntry:!1})?.isFile()&&add(paths.license.to,licenseFrom)}let featuresRel=resolveFeaturesReadme(repoDir,layout);if(findSymlinkComponent(repoDir,featuresRel)===null){let featuresFrom=safeJoin(repoDir,featuresRel);lstatSync3(featuresFrom,{throwIfNoEntry:!1})?.isFile()&&add(featuresRel,featuresFrom)}return addDir(paths.contracts),addDir(paths.core),addDir(paths.floor,rel=>!/\.test\.(mjs|cjs)$/.test(rel)&&rel!==FLOOR_TEST_FIXTURES_DIR&&!rel.startsWith(`${FLOOR_TEST_FIXTURES_DIR}/`)),expected}function conflictingWriteTargets(params){let{repoDir,projectRoot,capabilities,layout}=params,expected=params.expected??collectExpectedInstallPaths({repoDir,capabilities,layout}),candidates=new Set(expected.keys());candidates.add(PHARN_CONFIG_FILE);let conflicts=[];for(let rel of candidates)existsSync5(safeJoin(projectRoot,rel))&&conflicts.push(rel);return conflicts.sort()}function capabilityCloneFiles(repoDir,paths,capability){let relDir=`${capabilitySubtree(paths,capability.role)}/${capability.name}`;if(findSymlinkComponent(repoDir,relDir)!==null)return[];let from=safeJoin(repoDir,relDir);return lstatSync3(from,{throwIfNoEntry:!1})?.isDirectory()?[...walkFiles(from)].map(rel=>`${relDir}/${rel}`).sort():[]}import{lstatSync as lstatSync4}from"node:fs";import{relative}from"node:path";import{createHash}from"node:crypto";import{readFileSync as readFileSync3}from"node:fs";function sha256File(path){return createHash("sha256").update(readFileSync3(path)).digest("hex")}function decideFileAction(input){let{diskHash,latestHash,recordedHash,recordsAvailable,force}=input;return diskHash===null?{action:"write",label:"restored",backup:!1,refreshRecord:!0,forced:!1}:diskHash===latestHash?{action:"noop",label:"ok",backup:!1,refreshRecord:!0,forced:!1}:recordsAvailable?recordedHash===null?skipOrForce("unrecorded",force):diskHash===recordedHash?{action:"write",label:"updated",backup:!1,refreshRecord:!0,forced:!1}:skipOrForce("modified",force):skipOrForce("unverifiable",force)}function skipOrForce(label,force){return force?{action:"write",label,backup:!0,refreshRecord:!0,forced:!0}:{action:"skip",label,backup:!1,refreshRecord:!1,forced:!1}}var SKIP_ORDER=["modified","unrecorded","unverifiable","unreadable"];function planUpdate(params){let{latestHashes,diskStates,records,force}=params,files=[],writes=[],backups=[],nextRecords={},groups=new Map,counts={updated:0,restored:0,ok:0,forced:0,skipped:0};for(let[rel,latestHash]of latestHashes){let disk=diskStates.get(rel)??{kind:"absent"},previous=records?.[rel];if(disk.kind==="unreadable"){files.push({rel,action:"skip",label:"unreadable",backup:!1,forced:!1}),pushGroup(groups,"unreadable",rel),counts.skipped+=1,previous!==void 0&&(nextRecords[rel]=previous);continue}let decision=decideFileAction({diskHash:disk.kind==="file"?disk.hash:null,latestHash,recordedHash:previous??null,recordsAvailable:records!==null,force});files.push({rel,action:decision.action,label:decision.label,backup:decision.backup,forced:decision.forced}),decision.action==="write"?(writes.push(rel),decision.backup&&backups.push(rel),decision.forced?counts.forced+=1:decision.label==="restored"?counts.restored+=1:counts.updated+=1):decision.action==="noop"?counts.ok+=1:(pushGroup(groups,decision.label,rel),counts.skipped+=1),decision.refreshRecord?nextRecords[rel]=latestHash:previous!==void 0&&(nextRecords[rel]=previous)}let skipped=SKIP_ORDER.filter(label=>groups.has(label)).map(label=>({label,rels:groups.get(label).slice().sort()}));return{files,writes,backups,skipped,counts,nextRecords}}function pushGroup(groups,label,rel){let existing=groups.get(label);existing?existing.push(rel):groups.set(label,[rel])}function scanDest(params){let{repoDir,projectRoot,rels,sources}=params,records=params.records??null,drifted=[],labels=new Map,unsafe=[];for(let rel of rels){let srcRel=sources?.get(rel)??rel,dest=safeJoin(projectRoot,rel),src=safeJoin(repoDir,srcRel),destLink=symlinkComponent(projectRoot,rel);if(destLink===UNREADABLE)continue;if(destLink!==null){unsafe.push({rel,link:destLink});continue}if(!lstatSync4(dest,{throwIfNoEntry:!1})?.isFile()||symlinkComponent(repoDir,srcRel)!==null||!lstatSync4(src,{throwIfNoEntry:!1})?.isFile())continue;let label=driftLabel(decideFileAction({diskHash:sha256File(dest),latestHash:sha256File(src),recordedHash:records!==null&&Object.hasOwn(records,rel)?records[rel]??null:null,recordsAvailable:records!==null,force:!0}));label!==null&&(drifted.push(rel),labels.set(rel,label))}return{drifted:drifted.sort(),labels,unsafe:unsafe.sort((a,b)=>a.rel<b.rel?-1:a.rel>b.rel?1:0)}}function manifestSources(repoDir,manifest){let sources=new Map;for(let[dest,from]of manifest)sources.set(dest,toPosix(relative(repoDir,from)));return sources}function driftLabel(decision){if(!decision.backup)return null;let{label}=decision;return label==="modified"||label==="unrecorded"?label:"unverifiable"}var UNREADABLE=Symbol("unreadable");function symlinkComponent(base,rel){try{return findSymlinkComponent(base,rel)}catch{return UNREADABLE}}import{copyFileSync,existsSync as existsSync6,lstatSync as lstatSync5,mkdirSync as mkdirSync2}from"node:fs";import{dirname as dirname3}from"node:path";var BACKUP_DIR=".pharn-backup",MAX_COLLISION_SUFFIX=100;function backupTimestamp(date){let pad=n=>String(n).padStart(2,"0");return`${date.getFullYear()}${pad(date.getMonth()+1)}${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`}function createBackup(projectRoot,rels,date=new Date){let root=safeJoin(projectRoot,BACKUP_DIR);if(lstatSync5(root,{throwIfNoEntry:!1})?.isSymbolicLink())throw new ManifestValidationError(`${BACKUP_DIR} is a symlink; refusing to write backups through it.`);let dirRel=uniqueBackupDir(projectRoot,backupTimestamp(date)),dir=safeJoin(projectRoot,dirRel);mkdirSync2(dir,{recursive:!0});for(let rel of rels){let from=safeJoin(projectRoot,rel),link=findSymlinkComponent(projectRoot,rel);if(link!==null)throw new ManifestValidationError(`Cannot back up ${rel}: ${link} is a symlink.`);if(!lstatSync5(from,{throwIfNoEntry:!1}))throw new ManifestValidationError(`Cannot back up ${rel}: it disappeared before the backup ran.`);let to=safeJoin(dir,rel);mkdirSync2(dirname3(to),{recursive:!0}),copyFileSync(from,to)}return dirRel}function uniqueBackupDir(projectRoot,timestamp){for(let n=1;n<=MAX_COLLISION_SUFFIX;n+=1){let rel=n===1?`${BACKUP_DIR}/${timestamp}`:`${BACKUP_DIR}/${timestamp}-${n}`;if(!existsSync6(safeJoin(projectRoot,rel)))return rel}throw new ManifestValidationError(`Cannot create a backup directory: ${BACKUP_DIR}/${timestamp} and ${MAX_COLLISION_SUFFIX-1} suffixed variants all exist.`)}var MAX_LISTED2=10;function recordedSkillsVersion(cwd){let read=readBoundedFile(safeJoin(cwd,PHARN_CONFIG_FILE));if(read.kind!=="ok")return null;try{let parsed=JSON.parse(read.bytes.toString("utf8"));if(typeof parsed!="object"||parsed===null)return null;let version=parsed.skillsVersion;return typeof version=="string"&&VERSION_RE.test(version)?version:null}catch{return null}}var MARKERS={modified:"(edited)",unrecorded:"(no pharn record)",unverifiable:"(differs from upstream)"},LABEL_ORDER=["modified","unrecorded","unverifiable"];function labelLine(label,n){let one=n===1;switch(label){case"modified":return`${n} of them changed since pharn wrote ${one?"it":"them"} (your edits).`;case"unrecorded":return`${n} of them ${one?"differs":"differ"} from upstream, and pharn has no record of ${one?"it":"them"}.`;case"unverifiable":return`${n} of them ${one?"differs":"differ"} from upstream \u2014 pharn has no record to tell your edits from upstream changes.`}}async function confirmWriteTargets(repoDir,cwd,selection,context={}){let layout=detectLayout(repoDir),manifest=context.manifest??collectExpectedInstallPaths({repoDir,capabilities:selection.selected,layout}),conflicts=conflictingWriteTargets({repoDir,projectRoot:cwd,capabilities:selection.selected,layout,expected:manifest});if(conflicts.length===0)return"proceed";let{labels}=scanDest({repoDir,projectRoot:cwd,rels:conflicts.filter(p=>manifest.has(p)),sources:manifestSources(repoDir,manifest),records:context.records??null}),rank=p=>{let label=labels.get(p);return label===void 0?LABEL_ORDER.length:LABEL_ORDER.indexOf(label)},ordered=[...conflicts].sort((a,b)=>rank(a)-rank(b)),shown=ordered.slice(0,MAX_LISTED2),more=ordered.length-shown.length,list=shown.map(p=>{let label=labels.get(p);return label===void 0?` \u2022 ${p}`:` \u2022 ${p} ${MARKERS[label]}`}).join(`
9
+ Then re-run: npx @pharn-dev/pharn init`)}function fail(message){logError(message),process.exit(1)}import{confirm as confirm2,isCancel as isCancel2,log as log3}from"@clack/prompts";import{existsSync as existsSync5,lstatSync as lstatSync3,readdirSync as readdirSync4}from"node:fs";import{join as join3,resolve as resolve4}from"node:path";import{lstatSync as lstatSync2}from"node:fs";function findSymlinkComponent(base,rel){let current="";for(let segment of toPosix(rel).split("/"))if(segment&&(current=current?`${current}/${segment}`:segment,lstatSync2(safeJoin(base,current),{throwIfNoEntry:!1})?.isSymbolicLink()))return current;return null}function findTypeCollision(base,rel){let segments=toPosix(rel).split("/").filter(Boolean),current="";for(let[i,segment]of segments.entries()){current=current?`${current}/${segment}`:segment;let stat=lstatSync2(safeJoin(base,current),{throwIfNoEntry:!1});if(stat===void 0)return null;if(i===segments.length-1?!stat.isFile():!stat.isDirectory())return current}return null}var PHARN_CONFIG_FILE="pharn.config.json";function*walkFiles(dir,prefix=""){for(let entry of readdirSync4(dir,{withFileTypes:!0})){if(entry.isSymbolicLink())continue;let rel=prefix?`${prefix}/${entry.name}`:entry.name;entry.isDirectory()?yield*walkFiles(resolve4(dir,entry.name),rel):yield rel}}function collectExpectedInstallPaths(params){let{repoDir,capabilities,layout}=params,paths=layoutPaths(layout),expected=new Map,add=(rel,repoPath)=>{expected.set(toPosix(rel),repoPath)},addDir=(relDir,keep2,validate)=>{let from=safeJoin(repoDir,relDir);if(findSymlinkComponent(repoDir,relDir)===null&&lstatSync3(from,{throwIfNoEntry:!1})?.isDirectory())for(let rel of walkFiles(from))keep2&&!keep2(rel)||(validate?.(rel),add(join3(relDir,rel),resolve4(from,rel)))},copyNameFloor=dir=>rel=>{let label=`${dir}/${rel}`;assertSafeString(rel,label,COPY_FILENAME_RE),assertNoDotDot(rel,label)};for(let cap of capabilities){let subtree=capabilitySubtree(paths,cap.role);addDir(`${subtree}/${cap.name}`)}addDir(CLAUDE_COMMANDS_DIR,rel=>!rel.includes("/")&&rel.endsWith(".md")&&rel.startsWith(PRODUCT_COMMAND_PREFIX)&&!rel.startsWith(DEV_COMMAND_PREFIX),copyNameFloor(CLAUDE_COMMANDS_DIR)),addDir(CLAUDE_HOOKS_DIR,rel=>!rel.includes("/")&&rel.endsWith(".cjs")&&!rel.endsWith(".test.cjs"),copyNameFloor(CLAUDE_HOOKS_DIR));for(let doc of paths.docs){let from=safeJoin(repoDir,doc);findSymlinkComponent(repoDir,doc)===null&&lstatSync3(from,{throwIfNoEntry:!1})?.isFile()&&add(doc,from)}if(findSymlinkComponent(repoDir,paths.license.from)===null){let licenseFrom=safeJoin(repoDir,paths.license.from);lstatSync3(licenseFrom,{throwIfNoEntry:!1})?.isFile()&&add(paths.license.to,licenseFrom)}let featuresRel=resolveFeaturesReadme(repoDir,layout);if(findSymlinkComponent(repoDir,featuresRel)===null){let featuresFrom=safeJoin(repoDir,featuresRel);lstatSync3(featuresFrom,{throwIfNoEntry:!1})?.isFile()&&add(featuresRel,featuresFrom)}return addDir(paths.contracts),addDir(paths.core),addDir(paths.floor,rel=>!/\.test\.(mjs|cjs)$/.test(rel)&&rel!==FLOOR_TEST_FIXTURES_DIR&&!rel.startsWith(`${FLOOR_TEST_FIXTURES_DIR}/`)),expected}function conflictingWriteTargets(params){let{repoDir,projectRoot,capabilities,layout}=params,expected=params.expected??collectExpectedInstallPaths({repoDir,capabilities,layout}),candidates=new Set(expected.keys());candidates.add(PHARN_CONFIG_FILE);let conflicts=[];for(let rel of candidates)existsSync5(safeJoin(projectRoot,rel))&&conflicts.push(rel);return conflicts.sort()}function capabilityCloneFiles(repoDir,paths,capability){let relDir=`${capabilitySubtree(paths,capability.role)}/${capability.name}`;if(findSymlinkComponent(repoDir,relDir)!==null)return[];let from=safeJoin(repoDir,relDir);return lstatSync3(from,{throwIfNoEntry:!1})?.isDirectory()?[...walkFiles(from)].map(rel=>`${relDir}/${rel}`).sort():[]}import{lstatSync as lstatSync4}from"node:fs";import{relative}from"node:path";import{createHash}from"node:crypto";import{readFileSync as readFileSync3}from"node:fs";function sha256File(path){return createHash("sha256").update(readFileSync3(path)).digest("hex")}function decideFileAction(input){let{diskHash,latestHash,recordedHash,recordsAvailable,force}=input;return diskHash===null?{action:"write",label:"restored",backup:!1,refreshRecord:!0,forced:!1}:diskHash===latestHash?{action:"noop",label:"ok",backup:!1,refreshRecord:!0,forced:!1}:recordsAvailable?recordedHash===null?skipOrForce("unrecorded",force):diskHash===recordedHash?{action:"write",label:"updated",backup:!1,refreshRecord:!0,forced:!1}:skipOrForce("modified",force):skipOrForce("unverifiable",force)}function skipOrForce(label,force){return force?{action:"write",label,backup:!0,refreshRecord:!0,forced:!0}:{action:"skip",label,backup:!1,refreshRecord:!1,forced:!1}}var SKIP_ORDER=["modified","unrecorded","unverifiable","unreadable"];function planUpdate(params){let{latestHashes,diskStates,records,force}=params,files=[],writes=[],backups=[],nextRecords={},groups=new Map,counts={updated:0,restored:0,ok:0,forced:0,skipped:0};for(let[rel,latestHash]of latestHashes){let disk=diskStates.get(rel)??{kind:"absent"},previous=records?.[rel];if(disk.kind==="unreadable"){files.push({rel,action:"skip",label:"unreadable",backup:!1,forced:!1}),pushGroup(groups,"unreadable",rel),counts.skipped+=1,previous!==void 0&&(nextRecords[rel]=previous);continue}let decision=decideFileAction({diskHash:disk.kind==="file"?disk.hash:null,latestHash,recordedHash:previous??null,recordsAvailable:records!==null,force});files.push({rel,action:decision.action,label:decision.label,backup:decision.backup,forced:decision.forced}),decision.action==="write"?(writes.push(rel),decision.backup&&backups.push(rel),decision.forced?counts.forced+=1:decision.label==="restored"?counts.restored+=1:counts.updated+=1):decision.action==="noop"?counts.ok+=1:(pushGroup(groups,decision.label,rel),counts.skipped+=1),decision.refreshRecord?nextRecords[rel]=latestHash:previous!==void 0&&(nextRecords[rel]=previous)}let skipped=SKIP_ORDER.filter(label=>groups.has(label)).map(label=>({label,rels:groups.get(label).slice().sort()}));return{files,writes,backups,skipped,counts,nextRecords}}function pushGroup(groups,label,rel){let existing=groups.get(label);existing?existing.push(rel):groups.set(label,[rel])}function scanDest(params){let{repoDir,projectRoot,rels,sources}=params,records=params.records??null,drifted=[],labels=new Map,unsafe=[];for(let rel of rels){let srcRel=sources?.get(rel)??rel,dest=safeJoin(projectRoot,rel),src=safeJoin(repoDir,srcRel),destLink=symlinkComponent(projectRoot,rel);if(destLink===UNREADABLE)continue;if(destLink!==null){unsafe.push({rel,link:destLink});continue}if(!lstatSync4(dest,{throwIfNoEntry:!1})?.isFile()||symlinkComponent(repoDir,srcRel)!==null||!lstatSync4(src,{throwIfNoEntry:!1})?.isFile())continue;let label=driftLabel(decideFileAction({diskHash:sha256File(dest),latestHash:sha256File(src),recordedHash:records!==null&&Object.hasOwn(records,rel)?records[rel]??null:null,recordsAvailable:records!==null,force:!0}));label!==null&&(drifted.push(rel),labels.set(rel,label))}return{drifted:drifted.sort(),labels,unsafe:unsafe.sort((a,b)=>a.rel<b.rel?-1:a.rel>b.rel?1:0)}}function manifestSources(repoDir,manifest){let sources=new Map;for(let[dest,from]of manifest)sources.set(dest,toPosix(relative(repoDir,from)));return sources}function driftLabel(decision){if(!decision.backup)return null;let{label}=decision;return label==="modified"||label==="unrecorded"?label:"unverifiable"}var UNREADABLE=Symbol("unreadable");function symlinkComponent(base,rel){try{return findSymlinkComponent(base,rel)}catch{return UNREADABLE}}import{copyFileSync,existsSync as existsSync6,lstatSync as lstatSync5,mkdirSync as mkdirSync2}from"node:fs";import{dirname as dirname3}from"node:path";var BACKUP_DIR=".pharn-backup",MAX_COLLISION_SUFFIX=100;function backupTimestamp(date){let pad=n=>String(n).padStart(2,"0");return`${date.getFullYear()}${pad(date.getMonth()+1)}${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`}function createBackup(projectRoot,rels,date=new Date){let root=safeJoin(projectRoot,BACKUP_DIR);if(lstatSync5(root,{throwIfNoEntry:!1})?.isSymbolicLink())throw new ManifestValidationError(`${BACKUP_DIR} is a symlink; refusing to write backups through it.`);let dirRel=uniqueBackupDir(projectRoot,backupTimestamp(date)),dir=safeJoin(projectRoot,dirRel);mkdirSync2(dir,{recursive:!0});for(let rel of rels){let from=safeJoin(projectRoot,rel),link=findSymlinkComponent(projectRoot,rel);if(link!==null)throw new ManifestValidationError(`Cannot back up ${rel}: ${link} is a symlink.`);if(!lstatSync5(from,{throwIfNoEntry:!1}))throw new ManifestValidationError(`Cannot back up ${rel}: it disappeared before the backup ran.`);let to=safeJoin(dir,rel);mkdirSync2(dirname3(to),{recursive:!0}),copyFileSync(from,to)}return dirRel}function uniqueBackupDir(projectRoot,timestamp){for(let n=1;n<=MAX_COLLISION_SUFFIX;n+=1){let rel=n===1?`${BACKUP_DIR}/${timestamp}`:`${BACKUP_DIR}/${timestamp}-${n}`;if(!existsSync6(safeJoin(projectRoot,rel)))return rel}throw new ManifestValidationError(`Cannot create a backup directory: ${BACKUP_DIR}/${timestamp} and ${MAX_COLLISION_SUFFIX-1} suffixed variants all exist.`)}var MAX_LISTED2=10;function recordedSkillsVersion(cwd){let read=readBoundedFile(safeJoin(cwd,PHARN_CONFIG_FILE));if(read.kind!=="ok")return null;try{let parsed=JSON.parse(read.bytes.toString("utf8"));if(typeof parsed!="object"||parsed===null)return null;let version=parsed.skillsVersion;return typeof version=="string"&&VERSION_RE.test(version)?version:null}catch{return null}}var MARKERS={modified:"(edited)",unrecorded:"(no pharn record)",unverifiable:"(differs from upstream)"},LABEL_ORDER=["modified","unrecorded","unverifiable"];function labelLine(label,n){let one=n===1;switch(label){case"modified":return`${n} of them changed since pharn wrote ${one?"it":"them"} (your edits).`;case"unrecorded":return`${n} of them ${one?"differs":"differ"} from upstream, and pharn has no record of ${one?"it":"them"}.`;case"unverifiable":return`${n} of them ${one?"differs":"differ"} from upstream \u2014 pharn has no record to tell your edits from upstream changes.`}}async function confirmWriteTargets(repoDir,cwd,selection,context={}){let layout=detectLayout(repoDir),manifest=context.manifest??collectExpectedInstallPaths({repoDir,capabilities:selection.selected,layout}),conflicts=conflictingWriteTargets({repoDir,projectRoot:cwd,capabilities:selection.selected,layout,expected:manifest});if(conflicts.length===0)return"proceed";let{labels}=scanDest({repoDir,projectRoot:cwd,rels:conflicts.filter(p=>manifest.has(p)),sources:manifestSources(repoDir,manifest),records:context.records??null}),rank=p=>{let label=labels.get(p);return label===void 0?LABEL_ORDER.length:LABEL_ORDER.indexOf(label)},ordered=[...conflicts].sort((a,b)=>rank(a)-rank(b)),shown=ordered.slice(0,MAX_LISTED2),more=ordered.length-shown.length,list=shown.map(p=>{let label=labels.get(p);return label===void 0?` \u2022 ${p}`:` \u2022 ${p} ${MARKERS[label]}`}).join(`
10
10
  `),tail=more>0?`
11
11
  \u2026and ${more} more`:"",counts=LABEL_ORDER.map(label=>[label,[...labels.values()].filter(l=>l===label).length]).filter(([,n])=>n>0),editsLine=labels.size>0?`
12
12
  ${counts.map(([label,n])=>labelLine(label,n)).join(`
13
13
  `)}
14
14
  ${labels.size===1?"It":`All ${labels.size}`} will be copied to ${BACKUP_DIR}/ before being overwritten.`:"",version=conflicts.includes(PHARN_CONFIG_FILE)?recordedSkillsVersion(cwd):null,intro7=version===null?"PHARN installs into your existing project.":`PHARN installs into your existing project (currently at skills v${version}).`;log3.warn(`${intro7} These paths already exist and may be overwritten:
15
15
  ${list}${tail}${editsLine}`);let result=await confirm2({message:"Continue and overwrite?",initialValue:!1});return isCancel2(result)?"cancel":result===!0?"proceed":"decline"}import{isCancel as isCancel3,note,select}from"@clack/prompts";function describeMatched(matched){return matched==="universal"?"universal":matched==="manual"?"added by hand":matched.join(", ")}async function runArchetypeSummary(archetypes,selection){let lines=["\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"," PHARN Install Summary (archetype)","\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500","",row("Detected archetypes",archetypes.join(", ")),"",` CAPABILITIES SELECTED (${selection.selected.length})`,...selection.selected.length>0?selection.selected.map(c=>row(`${c.name} (${c.role})`,describeMatched(c.matched))):[" (none)"]];selection.skipped.length>0&&lines.push("",` CAPABILITIES SKIPPED (${selection.skipped.length})`,...selection.skipped.map(c=>row(`${c.name} (${c.role})`,c.reason))),lines.push(""," Installs PHARN product commands + write-gating hooks into .claude/"," (an existing .claude/settings.json is preserved, not overwritten).","","\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"),note(lines.join(`
16
- `));let action=await select({message:"Ready to install?",initialValue:"install",options:[{value:"install",label:"Yes, install"},{value:"cancel",label:"Cancel"}]});return isCancel3(action)?"cancel":action==="install"?"install":"cancel"}import{log as log4,outro,spinner}from"@clack/prompts";import pc2 from"picocolors";import{cpSync,existsSync as existsSync7,lstatSync as lstatSync7,mkdirSync as mkdirSync3,readdirSync as readdirSync5}from"node:fs";import{relative as relative2}from"node:path";import{lstatSync as lstatSync6}from"node:fs";import{chmod,lstat,unlink,writeFile,rename}from"node:fs/promises";function tmpPathFor(target){return`${target}.${process.pid}.tmp`}async function writeJsonAtomic(target,value){let tmp=tmpPathFor(target);try{await writeFile(tmp,`${JSON.stringify(value,null,2)}
17
- `,"utf8");let prev=await lstat(target).catch(()=>null);prev?.isFile()&&await chmod(tmp,prev.mode&511).catch(()=>{}),await rename(tmp,target)}catch(err){throw await unlink(tmp).catch(()=>{}),err}}import{resolve as resolve5}from"node:path";var RECORDS_FILE="pharn.records.json",RECORDS_SCHEMA_VERSION=1,SHA256_RE=/^[0-9a-f]{64}$/;function isInvalidRecordKey(key2){let normalized=toPosix(key2);return normalized===""||normalized.startsWith("/")||DRIVE_ABSOLUTE_RE.test(normalized)?!0:normalized.split("/").some(segment=>segment===".."||segment===".")}var DRIVE_ABSOLUTE_RE=/^[A-Za-z]:\//;function recordsPath(cwd){return resolve5(cwd,RECORDS_FILE)}function readRecords(cwd){let read=readBoundedFile(recordsPath(cwd));if(read.kind==="absent")return{kind:"absent"};if(read.kind==="unusable")return{kind:"invalid",message:`${RECORDS_FILE} ${read.reason}`};let raw;try{raw=JSON.parse(read.bytes.toString("utf8"))}catch{return{kind:"invalid",message:`${RECORDS_FILE} is not valid JSON`}}if(!isPlainObject(raw))return{kind:"invalid",message:`${RECORDS_FILE} is not a JSON object`};if(raw.schemaVersion!==RECORDS_SCHEMA_VERSION)return{kind:"invalid",message:`${RECORDS_FILE} has unknown schemaVersion ${JSON.stringify(raw.schemaVersion)} (expected ${RECORDS_SCHEMA_VERSION})`};if(typeof raw.skillsVersion!="string")return{kind:"invalid",message:`${RECORDS_FILE} has an invalid skillsVersion stamp`};if(raw.commit!==null&&typeof raw.commit!="string")return{kind:"invalid",message:`${RECORDS_FILE} has an invalid commit stamp`};if(!isPlainObject(raw.files))return{kind:"invalid",message:`${RECORDS_FILE} has no \`files\` object`};let files={};for(let[key2,value]of Object.entries(raw.files)){if(isInvalidRecordKey(key2))return{kind:"invalid",message:`${RECORDS_FILE} has an invalid file path key ${JSON.stringify(key2)}`};if(typeof value!="string"||!SHA256_RE.test(value))return{kind:"invalid",message:`${RECORDS_FILE} has an invalid hash for ${JSON.stringify(key2)}`};files[key2]=value}return{kind:"ok",store:{schemaVersion:RECORDS_SCHEMA_VERSION,skillsVersion:raw.skillsVersion,commit:raw.commit,files}}}function recordsBaseline(read,config){if(read.kind==="absent")return{records:null,note:null};if(read.kind==="invalid")return{records:null,note:read.message};let{store}=read,mismatches=stampMismatches(store,config);if(mismatches.length>0){let side=pick=>mismatches.map(m=>`${m.field} ${pick(m)}`).join(", ");return{records:null,note:`${RECORDS_FILE} was written for a different install state (${side(m=>m.store)}) than pharn.config.json (${side(m=>m.config)}); ignoring it, so every file that differs from upstream is \`unverifiable\` instead of a clean upgrade`}}return{records:store.files,note:null}}function stampMismatches(store,config){let out=[];return store.skillsVersion!==config.skillsVersion&&out.push({field:"skillsVersion",store:JSON.stringify(store.skillsVersion),config:JSON.stringify(config.skillsVersion)}),store.commit!==config.commit&&out.push({field:"commit",store:JSON.stringify(store.commit),config:JSON.stringify(config.commit)}),out}async function writeRecords(cwd,params){let store={schemaVersion:RECORDS_SCHEMA_VERSION,skillsVersion:params.skillsVersion,commit:params.commit,files:sortRecords(params.files)};await writeJsonAtomic(recordsPath(cwd),store)}function sortRecords(files){let out={};for(let key2 of Object.keys(files).sort())out[key2]=files[key2];return out}function buildRecords(projectRoot,rels){let files={};for(let rel of rels){let dest=safeJoin(projectRoot,rel),stat;try{stat=lstatSync6(dest,{throwIfNoEntry:!1})}catch{continue}stat?.isFile()&&(files[rel]=sha256File(dest))}return files}function mergeRecords(prev,next){return{...prev,...next}}function recordsUnderCapabilities(records,paths,capabilities){let prefixes=capabilities.map(cap=>`${capabilitySubtree(paths,cap.role)}/${cap.name}/`);if(prefixes.length===0)return{};let out={};for(let[rel,hash]of Object.entries(records))prefixes.some(prefix=>rel.startsWith(prefix))&&(out[rel]=hash);return out}var isTestFile=p=>/\.test\.(mjs|cjs)$/.test(p),inFloorFixtures=(floorFrom,src)=>{let rel=toPosix(relative2(floorFrom,src));return rel===FLOOR_TEST_FIXTURES_DIR||rel.startsWith(`${FLOOR_TEST_FIXTURES_DIR}/`)},isSymlink=p=>lstatSync7(p).isSymbolicLink(),noSymlinks=src=>!isSymlink(src);function installCapabilityDirs(repoDir,projectRoot,capabilities,paths=layoutPaths(detectLayout(repoDir))){let planned=capabilities.map(cap=>{assertSafeString(cap.name,`capability "${cap.name}"`,CAPABILITY_NAME_RE),assertNoDotDot(cap.name,`capability "${cap.name}"`);let subtree=capabilitySubtree(paths,cap.role),from=safeJoin(repoDir,`${subtree}/${cap.name}`);if(!existsSync7(from))throw new ManifestValidationError(`Capability "${cap.name}" (${cap.role}) is missing at ${subtree}/${cap.name} in the fetched repo.`);if(isSymlink(from))throw new ManifestValidationError(`Capability "${cap.name}" (${cap.role}) is a symlink at ${subtree}/${cap.name}; refusing to copy from the untrusted repo.`);return{name:cap.name,role:cap.role,subtree,from}}),installed2=[];for(let cap of planned){let to=safeJoin(projectRoot,`${cap.subtree}/${cap.name}`);cpSync(cap.from,to,{recursive:!0,force:!0,filter:noSymlinks}),installed2.push({name:cap.name,role:cap.role})}return installed2}function prepareInstall(repoDir,projectRoot,capabilities,manifest){let paths=layoutPaths(detectLayout(repoDir)),featuresRel=resolveFeaturesReadme(repoDir,paths.layout),expected=manifest??collectExpectedInstallPaths({repoDir,capabilities,layout:paths.layout});return assertDestinationsInProject(projectRoot,expected),assertDestinationTypes(projectRoot,expected,featuresRel),{paths,featuresRel,manifest:expected}}function installCapabilities(repoDir,projectRoot,selection,manifest){let{paths,featuresRel}=prepareInstall(repoDir,projectRoot,selection.selected,manifest),capabilities=installCapabilityDirs(repoDir,projectRoot,selection.selected,paths);copyFilteredDir(repoDir,projectRoot,CLAUDE_COMMANDS_DIR,fileName=>!fileName.endsWith(".md")||fileName.startsWith(DEV_COMMAND_PREFIX)?!1:fileName.startsWith(PRODUCT_COMMAND_PREFIX)),copyFilteredDir(repoDir,projectRoot,CLAUDE_HOOKS_DIR,fileName=>fileName.endsWith(".cjs")?!isTestFile(fileName):!1);let settingsFrom=safeJoin(repoDir,CLAUDE_SETTINGS_FILE),settingsTo=safeJoin(projectRoot,CLAUDE_SETTINGS_FILE),settingsPreserved=existsSync7(settingsTo);!settingsPreserved&&existsSync7(settingsFrom)&&!isSymlink(settingsFrom)&&(mkdirSync3(safeJoin(projectRoot,".claude"),{recursive:!0}),cpSync(settingsFrom,settingsTo,{force:!0}));let written=[];for(let doc of paths.docs){let from=safeJoin(repoDir,doc);existsSync7(from)&&!isSymlink(from)&&(cpSync(from,safeJoin(projectRoot,doc),{force:!0}),written.push(doc))}let licenseFrom=safeJoin(repoDir,paths.license.from);existsSync7(licenseFrom)&&!isSymlink(licenseFrom)&&cpSync(licenseFrom,safeJoin(projectRoot,paths.license.to),{force:!0});let featuresFrom=safeJoin(repoDir,featuresRel);findSymlinkComponent(repoDir,featuresRel)===null&&existsSync7(featuresFrom)&&!isSymlink(featuresFrom)&&destAcceptsWrite(projectRoot,featuresRel)&&cpSync(featuresFrom,safeJoin(projectRoot,featuresRel),{force:!0});let contractsFrom=safeJoin(repoDir,paths.contracts);existsSync7(contractsFrom)&&!isSymlink(contractsFrom)&&cpSync(contractsFrom,safeJoin(projectRoot,paths.contracts),{recursive:!0,force:!0,filter:noSymlinks});let coreFrom=safeJoin(repoDir,paths.core);existsSync7(coreFrom)&&!isSymlink(coreFrom)&&cpSync(coreFrom,safeJoin(projectRoot,paths.core),{recursive:!0,force:!0,filter:noSymlinks});let floorFrom=safeJoin(repoDir,paths.floor);return existsSync7(floorFrom)&&!isSymlink(floorFrom)&&cpSync(floorFrom,safeJoin(projectRoot,paths.floor),{recursive:!0,force:!0,filter:src=>!isTestFile(src)&&!inFloorFixtures(floorFrom,src)&&noSymlinks(src)}),{capabilities,settingsPreserved,layout:paths.layout,docs:written}}function assertDestinationsInProject(projectRoot,manifest){let dirs=new Set,files=new Set;for(let rel of manifest.keys()){let hit=symlinkedComponent(projectRoot,rel);hit!==null&&(hit===rel?files:dirs).add(hit)}let settingsHit=symlinkedComponent(projectRoot,CLAUDE_SETTINGS_FILE);if(settingsHit===CLAUDE_SETTINGS_FILE?existsSync7(safeJoin(projectRoot,CLAUDE_SETTINGS_FILE))||files.add(settingsHit):settingsHit!==null&&dirs.add(settingsHit),dirs.size===0&&files.size===0)return;let sentences=[];if(dirs.size>0){let one=dirs.size===1;sentences.push(`${shownList(dirs)} ${one?"is a symbolic link":"are symbolic links"} inside the project, so writing through ${one?"it":"them"} would put files OUTSIDE the project. Replace ${one?"it":"each"} with a real directory (or remove ${one?"it":"them"}).`)}if(files.size>0){let one=files.size===1;sentences.push(`${shownList(files)} ${one?"is a symbolic link":"are symbolic links"} where pharn writes a file, so the install would replace ${one?"it":"them"} with pharn's copy and the link would be lost. Replace ${one?"it":"each"} with a regular file (or remove ${one?"it":"them"}).`),files.has(CLAUDE_SETTINGS_FILE)&&sentences.push(`A link at ${CLAUDE_SETTINGS_FILE} is fine while it points at an existing file \u2014 pharn never writes over an existing one \u2014 but this one points at nothing.`)}throw new ManifestValidationError(`Refusing to install: ${sentences.join(" ")} Nothing was written; re-run \`pharn init\` once that is done.`)}function symlinkedComponent(projectRoot,rel){try{return findSymlinkComponent(projectRoot,rel)}catch{return null}}var MAX_LINKED_SHOWN=5;function shownList(items){let shown=[...items].sort();return shown.length>MAX_LINKED_SHOWN?`${shown.slice(0,MAX_LINKED_SHOWN).join(", ")} and ${shown.length-MAX_LINKED_SHOWN} more`:shown.join(", ")}function assertDestinationTypes(projectRoot,manifest,featuresRel){let colliding=new Set;for(let rel of manifest.keys()){if(rel===featuresRel)continue;let hit=findTypeCollision(projectRoot,rel);hit!==null&&colliding.add(hit)}for(let rel of[PHARN_CONFIG_FILE,RECORDS_FILE])lstatSync7(safeJoin(projectRoot,rel),{throwIfNoEntry:!1})?.isDirectory()&&colliding.add(rel);if(colliding.size===0)return;let one=colliding.size===1;throw new ManifestValidationError(`Refusing to install: ${shownList(colliding)} ${one?"is in the way":"are in the way"} \u2014 pharn needs a file where you have a directory, or a directory where you have a file. Nothing was written. Move or rename ${one?"it":"them"} and re-run \`pharn init\`.`)}function destAcceptsWrite(projectRoot,rel){try{return findSymlinkComponent(projectRoot,rel)===null&&findTypeCollision(projectRoot,rel)===null}catch{return!1}}function copyFilteredDir(repoDir,projectRoot,relDir,keep){let fromDir=safeJoin(repoDir,relDir);if(!existsSync7(fromDir))return;let entries=readdirSync5(fromDir,{withFileTypes:!0}),ensured=!1;for(let entry of entries)!entry.isFile()||entry.isSymbolicLink()||keep(entry.name)&&(assertSafeString(entry.name,`${relDir}/${entry.name}`,COPY_FILENAME_RE),assertNoDotDot(entry.name,`${relDir}/${entry.name}`),ensured||(mkdirSync3(safeJoin(projectRoot,relDir),{recursive:!0}),ensured=!0),cpSync(safeJoin(fromDir,entry.name),safeJoin(projectRoot,`${relDir}/${entry.name}`),{force:!0}))}var ModelRoutingError=class extends Error{constructor(message){super(message),this.name="ModelRoutingError"}},MODEL_IDS=["opus-4-8","sonnet-5","fable-5","haiku-4-5"],EFFORT_LEVELS=["low","high","max"],PIPELINE_STAGES=["plan","grill","build","regress","verify","review","ship"],DEFAULT_MODEL_ROUTING={default:{model:"sonnet-5",effort:"high"},stages:{plan:{model:"opus-4-8",effort:"max"},review:{model:"opus-4-8",effort:"high"}}},ROUTING_KEYS=["default","stages"],STAGE_MODEL_KEYS=["model","effort"];function assertNoUnknownKeys(obj,known,label){for(let key2 of Object.keys(obj))if(!known.includes(key2))throw new ModelRoutingError(`${label} has unknown key ${JSON.stringify(key2)} (expected one of ${known.join(", ")})`)}function assertStageModel(value,label){if(!isPlainObject(value))throw new ModelRoutingError(`${label} must be an object`);assertNoUnknownKeys(value,STAGE_MODEL_KEYS,label);let{model,effort}=value;if(typeof model!="string"||!MODEL_IDS.includes(model))throw new ModelRoutingError(`${label} has invalid model ${JSON.stringify(model)} (expected one of ${MODEL_IDS.join(", ")})`);if(typeof effort!="string"||!EFFORT_LEVELS.includes(effort))throw new ModelRoutingError(`${label} has invalid effort ${JSON.stringify(effort)} (expected one of ${EFFORT_LEVELS.join(", ")})`);return{model,effort}}function validateModelRouting(input){if(!isPlainObject(input))throw new ModelRoutingError("models must be an object");assertNoUnknownKeys(input,ROUTING_KEYS,"models");let routingDefault=assertStageModel(input.default,"models.default"),stages={};if(input.stages!==void 0){if(!isPlainObject(input.stages))throw new ModelRoutingError("models.stages must be an object");for(let[stage,value]of Object.entries(input.stages)){if(!PIPELINE_STAGES.includes(stage))throw new ModelRoutingError(`models.stages has unknown stage ${JSON.stringify(stage)} (expected one of ${PIPELINE_STAGES.join(", ")})`);stages[stage]=assertStageModel(value,`models.stages.${stage}`)}}return{default:routingDefault,stages}}function formatModelRoutingLines(routing){let entries=[["default",routing.default]];for(let stage of PIPELINE_STAGES){let target=routing.stages?.[stage];target&&entries.push([stage,target])}let width=Math.max(...entries.map(([label])=>label.length))+3;return entries.map(([label,{model,effort}])=>`${label.padEnd(width)}${model} \xB7 ${effort}`)}var SeamConfigError=class extends Error{constructor(message){super(message),this.name="SeamConfigError"}},RESOLUTION_STEPS=["official-skill","pinned-docs","fetch","model","ask"],SEAM_CONFIDENCE_LEVELS=["low","medium","high"],TERMINAL_STEP="ask",DEFAULT_SEAM_CONFIG={resolutionOrder:["official-skill","pinned-docs","model","fetch","ask"],modelConfidenceThreshold:"high",haltOnUnknown:!0},SEAM_KEYS=["resolutionOrder","modelConfidenceThreshold","haltOnUnknown"];function assertNoUnknownKeys2(obj,known,label){for(let key2 of Object.keys(obj))if(!known.includes(key2))throw new SeamConfigError(`${label} has unknown key ${JSON.stringify(key2)} (expected one of ${known.join(", ")})`)}function validateSeamConfig(input){if(!isPlainObject(input))throw new SeamConfigError("seam must be an object");assertNoUnknownKeys2(input,SEAM_KEYS,"seam");let{resolutionOrder}=input;if(!Array.isArray(resolutionOrder))throw new SeamConfigError(`seam.resolutionOrder must be an array (got ${JSON.stringify(resolutionOrder)})`);for(let i=0;i<resolutionOrder.length;i++){let step=resolutionOrder[i];if(!RESOLUTION_STEPS.includes(step))throw new SeamConfigError(`seam.resolutionOrder[${i}] has invalid step ${JSON.stringify(step)} (expected one of ${RESOLUTION_STEPS.join(", ")})`)}let seen=new Set;for(let step of resolutionOrder){if(seen.has(step))throw new SeamConfigError(`seam.resolutionOrder has duplicate step ${JSON.stringify(step)} (each step may appear at most once)`);seen.add(step)}if(!resolutionOrder.includes(TERMINAL_STEP))throw new SeamConfigError(`seam.resolutionOrder must contain "${TERMINAL_STEP}" \u2014 the terminal fallback cannot be configured away (fail-closed)`);let config={resolutionOrder};if(input.modelConfidenceThreshold!==void 0){let threshold=input.modelConfidenceThreshold;if(typeof threshold!="string"||!SEAM_CONFIDENCE_LEVELS.includes(threshold))throw new SeamConfigError(`seam.modelConfidenceThreshold ${JSON.stringify(threshold)} is invalid (expected one of ${SEAM_CONFIDENCE_LEVELS.join(", ")})`);config.modelConfidenceThreshold=threshold}if(input.modelConfidenceThreshold!==void 0&&!resolutionOrder.includes("model"))throw new SeamConfigError('seam.modelConfidenceThreshold is set but resolutionOrder has no "model" step for it to gate (add "model" or remove the threshold)');if(input.haltOnUnknown!==void 0){if(typeof input.haltOnUnknown!="boolean")throw new SeamConfigError(`seam.haltOnUnknown must be a boolean (got ${JSON.stringify(input.haltOnUnknown)})`);config.haltOnUnknown=input.haltOnUnknown}return config}import{createHash as createHash2}from"node:crypto";import{resolve as resolve6}from"node:path";var CONFIG_FILENAME="pharn.config.json",CAPABILITY_SOURCES=["auto","manual"],CapabilitySourceError=class extends Error{constructor(message){super(message),this.name="CapabilitySourceError"}},CapabilityEntryError=class extends Error{constructor(message){super(message),this.name="CapabilityEntryError"}};function validateCapabilityEntries(raw){Array.isArray(raw)&&raw.forEach((entry,i)=>{let at=`${CONFIG_FILENAME}: capabilities[${i}]`;if(!isPlainObject(entry))throw new CapabilityEntryError(`${at} must be an object with "name" and "role" (found ${JSON.stringify(entry)}). Fix it by hand.`);try{assertSafeString(entry.name,`${at}.name`,CAPABILITY_NAME_RE)}catch(err){throw new CapabilityEntryError(`${errorMessage(err)}. A capability name is one lowercase, hyphen-separated segment (e.g. "a11y", "n-plus-one"). Fix it by hand.`)}if(!ROLE_VALUES.some(r=>r===entry.role))throw new CapabilityEntryError(`${at}.role must be ${ROLE_VALUES.map(r=>`"${r}"`).join(" or ")} (found ${JSON.stringify(entry.role)}). Fix it by hand.`)})}function validateCapabilitySources(raw){Array.isArray(raw)&&raw.forEach((entry,i)=>{if(!isPlainObject(entry)||!("source"in entry))return;let source=entry.source;if(typeof source!="string"||!CAPABILITY_SOURCES.includes(source))throw new CapabilitySourceError(`${CONFIG_FILENAME}: capabilities[${i}].source must be ${CAPABILITY_SOURCES.map(s=>`"${s}"`).join(" or ")} (found ${JSON.stringify(source)}). Fix it by hand, or delete the field \u2014 an absent \`source\` is valid and the next \`pharn update\` will set it.`)})}var ConfigParseError=class extends Error{constructor(message){super(message),this.name="ConfigParseError"}};function parseLocation(err){let at=/\(line (\d{1,9}) column (\d{1,9})\)$/.exec(errorMessage(err));return at?` (line ${at[1]}, column ${at[2]})`:""}var CONTROL_CHARS_RE2=/[\x00-\x1f\x7f-\x9f]/g;function displayPath(path){return path.replace(CONTROL_CHARS_RE2,c=>`\\x${c.charCodeAt(0).toString(16).padStart(2,"0")}`)}function configPath(cwd){return resolve6(cwd,CONFIG_FILENAME)}var CLI_OWNED_KEYS=new Set(Object.keys({pharnVersion:!0,skillsVersion:!0,pendingSkillsVersion:!0,frozenCapabilities:!0,repo:!0,commit:!0,constitution:!0,isMultiTenant:!0,modules:!0,installedAt:!0,models:!0,seam:!0,stackAnswers:!0,installedSkills:!0,archetypes:!0,capabilities:!0,layout:!0}));function userOwnedConfigEntries(raw){return Object.fromEntries(Object.entries(raw).filter(([key2])=>!CLI_OWNED_KEYS.has(key2)))}function readPharnConfig(cwd){let path=configPath(cwd),read=readBoundedFile(path);if(read.kind!=="ok")return null;let text=read.bytes.toString("utf8"),raw;try{raw=JSON.parse(text)}catch(err){throw new ConfigParseError(`${displayPath(path)} is not valid JSON${parseLocation(err)}. The file exists \u2014 fix its syntax by hand. Do NOT run \`pharn init\` to clear it: init OVERWRITES this config, discarding your recorded capabilities, any manual \`pharn add\` provenance, and hand-edited models/seam blocks. If it is beyond repair, move it aside first (\`mv ${CONFIG_FILENAME} ${CONFIG_FILENAME}.bak\`), then run \`pharn init\`.`)}if(!isPlainObject(raw)||typeof raw.skillsVersion!="string"||!Array.isArray(raw.modules))return null;let models=raw.models!==void 0?validateModelRouting(raw.models):void 0,seam=raw.seam!==void 0?validateSeamConfig(raw.seam):void 0;validateCapabilityEntries(raw.capabilities),validateCapabilitySources(raw.capabilities);let config={...raw,...models!==void 0?{models}:{},...seam!==void 0?{seam}:{}};return config.layout!=="pharn"&&config.layout!=="flat"&&delete config.layout,config.pendingSkillsVersion!==void 0&&(typeof config.pendingSkillsVersion!="string"||!VERSION_RE.test(config.pendingSkillsVersion))&&delete config.pendingSkillsVersion,config.frozenCapabilities!==void 0&&!isFrozenKeyList(config.frozenCapabilities)&&delete config.frozenCapabilities,config}function isFrozenKeyList(value){return Array.isArray(value)&&value.every(key2=>{if(typeof key2!="string")return!1;let sep2=key2.indexOf(":");if(sep2===-1)return!1;let role=key2.slice(0,sep2),name=key2.slice(sep2+1);return ROLE_VALUES.some(r=>r===role)&&CAPABILITY_NAME_RE.test(name)})}function loadConfigOrExit(cwd){try{let config=readPharnConfig(cwd);if(config)return config}catch(err){throw isConfigValidationError(err)&&(logError(err.message),process.exit(1)),err}logError("No pharn.config.json found. Run `pharn init` first."),process.exit(1)}function assertConfigUnchanged(cwd,snapshot,command){let current;try{current=readPharnConfig(cwd)}catch{current=null}if(!(current!==null&&JSON.stringify(current)===JSON.stringify(snapshot)))throw configChangedError(command)}function configChangedError(command){return new ProjectChangedError(`${CONFIG_FILENAME} changed while \`pharn ${command}\` was running \u2014 another pharn process (or an edit) wrote it after this command read it. Nothing was written. Re-run \`pharn ${command}\` to act on the current state.`)}function configFingerprint(cwd){let read=readBoundedFile(configPath(cwd));return read.kind==="absent"?"absent":read.kind==="unusable"?`unreadable:${read.code}`:`sha256:${createHash2("sha256").update(read.bytes).digest("hex")}`}function assertConfigFingerprintUnchanged(cwd,fingerprint,command){if(configFingerprint(cwd)!==fingerprint)throw configChangedError(command)}function isConfigValidationError(err){return err instanceof ConfigParseError||err instanceof ModelRoutingError||err instanceof SeamConfigError||err instanceof CapabilityEntryError||err instanceof CapabilitySourceError}async function writePharnConfig(cwd,config){await writeJsonAtomic(configPath(cwd),config)}function isArchetypeConfig(config){return Array.isArray(config.capabilities)}var LEGACY_CONFIG_MESSAGE="This project uses the legacy module layout (pre-archetype), which is no longer supported. Re-run `pharn init` to reinstall with the archetype/capability model.";function loadArchetypeConfigOrExit(cwd){let config=loadConfigOrExit(cwd);return isArchetypeConfig(config)||(logError(LEGACY_CONFIG_MESSAGE),process.exit(1)),config}function installManifest(repoDir,selection){return collectExpectedInstallPaths({repoDir,capabilities:selection.selected,layout:detectLayout(repoDir)})}function preflightInstall(repoDir,cwd,selection,manifest){prepareInstall(repoDir,cwd,selection.selected,manifest)}var NO_CARRY={manualKeys:new Set,kept:[],previousStamp:null};async function runInstallArchetype(repoDir,cwd,archetypes,selection,commit,carry=NO_CARRY,manifest){let startedAt=Date.now(),prepared=prepareInstall(repoDir,cwd,selection.selected,manifest),baseline=reinstallBaseline(cwd,carry.previousStamp),scan=scanDest({repoDir,projectRoot:cwd,rels:[...prepared.manifest.keys()],sources:manifestSources(repoDir,prepared.manifest),records:baseline});if(scan.drifted.length>0&&scan.unsafe.length===0){let backupDir=createBackup(cwd,scan.drifted);log4.info(`Backed up ${scan.drifted.length} file(s) to ${backupDir} before overwriting.`)}let s=spinner();s.start("Installing capabilities");let capabilities,settingsPreserved,skillsVersion,layout,docsWritten;try{let result=installCapabilities(repoDir,cwd,selection,prepared.manifest);capabilities=result.capabilities,settingsPreserved=result.settingsPreserved,layout=result.layout,docsWritten=result.docs,skillsVersion=readSkillsVersion(repoDir)}catch(err){throw s.stop("Failed to install capabilities"),err}s.stop("Capabilities installed"),settingsPreserved&&log4.warn("Existing .claude/settings.json preserved \u2014 compare it against pharn-oss to wire the PHARN hooks if needed.");let docsMissing=layoutPaths(layout).docs.filter(doc=>!docsWritten.includes(doc));docsMissing.length&&log4.warn(`The fetched repo shipped no ${docsMissing.join(", ")} \u2014 not installed. Installed commands and checkers that cite ${docsMissing.length===1?"it":"them"} by path will not resolve.`);let frozenCapabilities=[...new Set(carry.kept.map(c=>`${c.role}:${c.name}`))].sort(),config={pharnVersion:PHARN_VERSION,skillsVersion,repo:REPO_URL.replace(/^github\.com\//,""),commit,modules:[],installedAt:new Date().toISOString(),models:DEFAULT_MODEL_ROUTING,seam:DEFAULT_SEAM_CONFIG,archetypes,capabilities:[...capabilities.map(c=>({...c,source:carry.manualKeys.has(`${c.role}:${c.name}`)?"manual":"auto"})),...carry.kept.map(c=>({...c}))],layout,...frozenCapabilities.length>0?{frozenCapabilities}:{}};await writeRecords(cwd,{skillsVersion,commit,files:{...keptRecords(baseline,carry,layout),...buildRecords(cwd,prepared.manifest.keys())}}),await writePharnConfig(cwd,{...config,...readCarriedEntries(cwd)});let elapsed=((Date.now()-startedAt)/1e3).toFixed(1),check=pc2.green("\u2714"),grillers=capabilities.filter(c=>c.role==="griller").length,lenses=capabilities.filter(c=>c.role==="lens").length,modelLines=config.models?formatModelRoutingLines(config.models):[],docsLine=docsWritten.length?`${check} ${docsWritten.length} trusted doc${docsWritten.length===1?"":"s"} written \u2192 ${pc2.dim(docsWritten.join(", "))}`:`${pc2.yellow("!")} no trusted docs written ${pc2.dim("(the fetched repo shipped none at their expected paths)")}`;outro([`${check} ${capabilities.length} capabilit${capabilities.length===1?"y":"ies"} installed \u2192 ${pc2.dim(`(${grillers} griller${grillers===1?"":"s"}, ${lenses} lens${lenses===1?"":"es"})`)}`,`${check} PHARN commands + hooks written \u2192 ${pc2.dim(".claude/")}`,docsLine,`${check} pharn.config.json written ${pc2.dim(`(skills v${skillsVersion}, archetypes: ${archetypes.join(", ")})`)}`,`${pc2.dim(`Done in ${elapsed}s`)}`,"",pc2.bold("Models per stage"),...modelLines.map(line=>` ${line}`),` ${pc2.dim("Recorded in pharn.config.json \u2192 models.stages \u2014 no installed stage reads it yet")}`,"",pc2.bold("Next steps"),` ${pc2.cyan("1.")} ${pc2.bold("claude")} ${pc2.dim("open Claude Code")}`,` ${pc2.cyan("2.")} ${pc2.bold(FIRST_FEATURE_COMMAND)} ${pc2.dim("capture your first feature's intent")}`].join(`
16
+ `));let action=await select({message:"Ready to install?",initialValue:"install",options:[{value:"install",label:"Yes, install"},{value:"cancel",label:"Cancel"}]});return isCancel3(action)?"cancel":action==="install"?"install":"cancel"}import{log as log4,outro,spinner}from"@clack/prompts";import pc2 from"picocolors";import{cpSync,existsSync as existsSync7,lstatSync as lstatSync7,mkdirSync as mkdirSync3,readdirSync as readdirSync5}from"node:fs";import{relative as relative2}from"node:path";import{createHash as createHash2}from"node:crypto";import{lstatSync as lstatSync6}from"node:fs";import{chmod,lstat,unlink,writeFile,rename}from"node:fs/promises";function tmpPathFor(target){return`${target}.${process.pid}.tmp`}async function writeJsonAtomic(target,value){let tmp=tmpPathFor(target);try{await writeFile(tmp,`${JSON.stringify(value,null,2)}
17
+ `,"utf8");let prev=await lstat(target).catch(()=>null);prev?.isFile()&&await chmod(tmp,prev.mode&511).catch(()=>{}),await rename(tmp,target)}catch(err){throw await unlink(tmp).catch(()=>{}),err}}import{resolve as resolve5}from"node:path";var RECORDS_FILE="pharn.records.json",MODELS_RECORD_KEY="pharn.config.json#/models";function modelsRecordHash(block){return createHash2("sha256").update(JSON.stringify(block)).digest("hex")}var RECORDS_SCHEMA_VERSION=1,SHA256_RE=/^[0-9a-f]{64}$/;function isInvalidRecordKey(key2){let normalized=toPosix(key2);return normalized===""||normalized.startsWith("/")||DRIVE_ABSOLUTE_RE.test(normalized)?!0:normalized.split("/").some(segment=>segment===".."||segment===".")}var DRIVE_ABSOLUTE_RE=/^[A-Za-z]:\//;function recordsPath(cwd){return resolve5(cwd,RECORDS_FILE)}function readRecords(cwd){let read=readBoundedFile(recordsPath(cwd));if(read.kind==="absent")return{kind:"absent"};if(read.kind==="unusable")return{kind:"invalid",message:`${RECORDS_FILE} ${read.reason}`};let raw;try{raw=JSON.parse(read.bytes.toString("utf8"))}catch{return{kind:"invalid",message:`${RECORDS_FILE} is not valid JSON`}}if(!isPlainObject(raw))return{kind:"invalid",message:`${RECORDS_FILE} is not a JSON object`};if(raw.schemaVersion!==RECORDS_SCHEMA_VERSION)return{kind:"invalid",message:`${RECORDS_FILE} has unknown schemaVersion ${JSON.stringify(raw.schemaVersion)} (expected ${RECORDS_SCHEMA_VERSION})`};if(typeof raw.skillsVersion!="string")return{kind:"invalid",message:`${RECORDS_FILE} has an invalid skillsVersion stamp`};if(raw.commit!==null&&typeof raw.commit!="string")return{kind:"invalid",message:`${RECORDS_FILE} has an invalid commit stamp`};if(!isPlainObject(raw.files))return{kind:"invalid",message:`${RECORDS_FILE} has no \`files\` object`};let files={};for(let[key2,value]of Object.entries(raw.files)){if(isInvalidRecordKey(key2))return{kind:"invalid",message:`${RECORDS_FILE} has an invalid file path key ${JSON.stringify(key2)}`};if(typeof value!="string"||!SHA256_RE.test(value))return{kind:"invalid",message:`${RECORDS_FILE} has an invalid hash for ${JSON.stringify(key2)}`};files[key2]=value}return{kind:"ok",store:{schemaVersion:RECORDS_SCHEMA_VERSION,skillsVersion:raw.skillsVersion,commit:raw.commit,files}}}function recordsBaseline(read,config){if(read.kind==="absent")return{records:null,note:null};if(read.kind==="invalid")return{records:null,note:read.message};let{store}=read,mismatches=stampMismatches(store,config);if(mismatches.length>0){let side=pick=>mismatches.map(m=>`${m.field} ${pick(m)}`).join(", ");return{records:null,note:`${RECORDS_FILE} was written for a different install state (${side(m=>m.store)}) than pharn.config.json (${side(m=>m.config)}); ignoring it, so every file that differs from upstream is \`unverifiable\` instead of a clean upgrade`}}return{records:store.files,note:null}}function stampMismatches(store,config){let out=[];return store.skillsVersion!==config.skillsVersion&&out.push({field:"skillsVersion",store:JSON.stringify(store.skillsVersion),config:JSON.stringify(config.skillsVersion)}),store.commit!==config.commit&&out.push({field:"commit",store:JSON.stringify(store.commit),config:JSON.stringify(config.commit)}),out}async function writeRecords(cwd,params){let store={schemaVersion:RECORDS_SCHEMA_VERSION,skillsVersion:params.skillsVersion,commit:params.commit,files:sortRecords(params.files)};await writeJsonAtomic(recordsPath(cwd),store)}function sortRecords(files){let out={};for(let key2 of Object.keys(files).sort())out[key2]=files[key2];return out}function buildRecords(projectRoot,rels){let files={};for(let rel of rels){let dest=safeJoin(projectRoot,rel),stat;try{stat=lstatSync6(dest,{throwIfNoEntry:!1})}catch{continue}stat?.isFile()&&(files[rel]=sha256File(dest))}return files}function mergeRecords(prev,next){return{...prev,...next}}function recordsUnderCapabilities(records,paths,capabilities){let prefixes=capabilities.map(cap=>`${capabilitySubtree(paths,cap.role)}/${cap.name}/`);if(prefixes.length===0)return{};let out={};for(let[rel,hash]of Object.entries(records))prefixes.some(prefix=>rel.startsWith(prefix))&&(out[rel]=hash);return out}var isTestFile=p=>/\.test\.(mjs|cjs)$/.test(p),inFloorFixtures=(floorFrom,src)=>{let rel=toPosix(relative2(floorFrom,src));return rel===FLOOR_TEST_FIXTURES_DIR||rel.startsWith(`${FLOOR_TEST_FIXTURES_DIR}/`)},isSymlink=p=>lstatSync7(p).isSymbolicLink(),noSymlinks=src=>!isSymlink(src);function installCapabilityDirs(repoDir,projectRoot,capabilities,paths=layoutPaths(detectLayout(repoDir))){let planned=capabilities.map(cap=>{assertSafeString(cap.name,`capability "${cap.name}"`,CAPABILITY_NAME_RE),assertNoDotDot(cap.name,`capability "${cap.name}"`);let subtree=capabilitySubtree(paths,cap.role),from=safeJoin(repoDir,`${subtree}/${cap.name}`);if(!existsSync7(from))throw new ManifestValidationError(`Capability "${cap.name}" (${cap.role}) is missing at ${subtree}/${cap.name} in the fetched repo.`);if(isSymlink(from))throw new ManifestValidationError(`Capability "${cap.name}" (${cap.role}) is a symlink at ${subtree}/${cap.name}; refusing to copy from the untrusted repo.`);return{name:cap.name,role:cap.role,subtree,from}}),installed2=[];for(let cap of planned){let to=safeJoin(projectRoot,`${cap.subtree}/${cap.name}`);cpSync(cap.from,to,{recursive:!0,force:!0,filter:noSymlinks}),installed2.push({name:cap.name,role:cap.role})}return installed2}function prepareInstall(repoDir,projectRoot,capabilities,manifest){let paths=layoutPaths(detectLayout(repoDir)),featuresRel=resolveFeaturesReadme(repoDir,paths.layout),expected=manifest??collectExpectedInstallPaths({repoDir,capabilities,layout:paths.layout});return assertDestinationsInProject(projectRoot,expected),assertDestinationTypes(projectRoot,expected,featuresRel),{paths,featuresRel,manifest:expected}}function installCapabilities(repoDir,projectRoot,selection,manifest){let{paths,featuresRel}=prepareInstall(repoDir,projectRoot,selection.selected,manifest),capabilities=installCapabilityDirs(repoDir,projectRoot,selection.selected,paths);copyFilteredDir(repoDir,projectRoot,CLAUDE_COMMANDS_DIR,fileName=>!fileName.endsWith(".md")||fileName.startsWith(DEV_COMMAND_PREFIX)?!1:fileName.startsWith(PRODUCT_COMMAND_PREFIX)),copyFilteredDir(repoDir,projectRoot,CLAUDE_HOOKS_DIR,fileName=>fileName.endsWith(".cjs")?!isTestFile(fileName):!1);let settingsFrom=safeJoin(repoDir,CLAUDE_SETTINGS_FILE),settingsTo=safeJoin(projectRoot,CLAUDE_SETTINGS_FILE),settingsPreserved=existsSync7(settingsTo);!settingsPreserved&&existsSync7(settingsFrom)&&!isSymlink(settingsFrom)&&(mkdirSync3(safeJoin(projectRoot,".claude"),{recursive:!0}),cpSync(settingsFrom,settingsTo,{force:!0}));let written=[];for(let doc of paths.docs){let from=safeJoin(repoDir,doc);existsSync7(from)&&!isSymlink(from)&&(cpSync(from,safeJoin(projectRoot,doc),{force:!0}),written.push(doc))}let licenseFrom=safeJoin(repoDir,paths.license.from);existsSync7(licenseFrom)&&!isSymlink(licenseFrom)&&cpSync(licenseFrom,safeJoin(projectRoot,paths.license.to),{force:!0});let featuresFrom=safeJoin(repoDir,featuresRel);findSymlinkComponent(repoDir,featuresRel)===null&&existsSync7(featuresFrom)&&!isSymlink(featuresFrom)&&destAcceptsWrite(projectRoot,featuresRel)&&cpSync(featuresFrom,safeJoin(projectRoot,featuresRel),{force:!0});let contractsFrom=safeJoin(repoDir,paths.contracts);existsSync7(contractsFrom)&&!isSymlink(contractsFrom)&&cpSync(contractsFrom,safeJoin(projectRoot,paths.contracts),{recursive:!0,force:!0,filter:noSymlinks});let coreFrom=safeJoin(repoDir,paths.core);existsSync7(coreFrom)&&!isSymlink(coreFrom)&&cpSync(coreFrom,safeJoin(projectRoot,paths.core),{recursive:!0,force:!0,filter:noSymlinks});let floorFrom=safeJoin(repoDir,paths.floor);return existsSync7(floorFrom)&&!isSymlink(floorFrom)&&cpSync(floorFrom,safeJoin(projectRoot,paths.floor),{recursive:!0,force:!0,filter:src=>!isTestFile(src)&&!inFloorFixtures(floorFrom,src)&&noSymlinks(src)}),{capabilities,settingsPreserved,layout:paths.layout,docs:written}}function assertDestinationsInProject(projectRoot,manifest){let dirs=new Set,files=new Set;for(let rel of manifest.keys()){let hit=symlinkedComponent(projectRoot,rel);hit!==null&&(hit===rel?files:dirs).add(hit)}let settingsHit=symlinkedComponent(projectRoot,CLAUDE_SETTINGS_FILE);if(settingsHit===CLAUDE_SETTINGS_FILE?existsSync7(safeJoin(projectRoot,CLAUDE_SETTINGS_FILE))||files.add(settingsHit):settingsHit!==null&&dirs.add(settingsHit),dirs.size===0&&files.size===0)return;let sentences=[];if(dirs.size>0){let one=dirs.size===1;sentences.push(`${shownList(dirs)} ${one?"is a symbolic link":"are symbolic links"} inside the project, so writing through ${one?"it":"them"} would put files OUTSIDE the project. Replace ${one?"it":"each"} with a real directory (or remove ${one?"it":"them"}).`)}if(files.size>0){let one=files.size===1;sentences.push(`${shownList(files)} ${one?"is a symbolic link":"are symbolic links"} where pharn writes a file, so the install would replace ${one?"it":"them"} with pharn's copy and the link would be lost. Replace ${one?"it":"each"} with a regular file (or remove ${one?"it":"them"}).`),files.has(CLAUDE_SETTINGS_FILE)&&sentences.push(`A link at ${CLAUDE_SETTINGS_FILE} is fine while it points at an existing file \u2014 pharn never writes over an existing one \u2014 but this one points at nothing.`)}throw new ManifestValidationError(`Refusing to install: ${sentences.join(" ")} Nothing was written; re-run \`pharn init\` once that is done.`)}function symlinkedComponent(projectRoot,rel){try{return findSymlinkComponent(projectRoot,rel)}catch{return null}}var MAX_LINKED_SHOWN=5;function shownList(items){let shown=[...items].sort();return shown.length>MAX_LINKED_SHOWN?`${shown.slice(0,MAX_LINKED_SHOWN).join(", ")} and ${shown.length-MAX_LINKED_SHOWN} more`:shown.join(", ")}function assertDestinationTypes(projectRoot,manifest,featuresRel){let colliding=new Set;for(let rel of manifest.keys()){if(rel===featuresRel)continue;let hit=findTypeCollision(projectRoot,rel);hit!==null&&colliding.add(hit)}for(let rel of[PHARN_CONFIG_FILE,RECORDS_FILE])lstatSync7(safeJoin(projectRoot,rel),{throwIfNoEntry:!1})?.isDirectory()&&colliding.add(rel);if(colliding.size===0)return;let one=colliding.size===1;throw new ManifestValidationError(`Refusing to install: ${shownList(colliding)} ${one?"is in the way":"are in the way"} \u2014 pharn needs a file where you have a directory, or a directory where you have a file. Nothing was written. Move or rename ${one?"it":"them"} and re-run \`pharn init\`.`)}function destAcceptsWrite(projectRoot,rel){try{return findSymlinkComponent(projectRoot,rel)===null&&findTypeCollision(projectRoot,rel)===null}catch{return!1}}function copyFilteredDir(repoDir,projectRoot,relDir,keep2){let fromDir=safeJoin(repoDir,relDir);if(!existsSync7(fromDir))return;let entries=readdirSync5(fromDir,{withFileTypes:!0}),ensured=!1;for(let entry of entries)!entry.isFile()||entry.isSymbolicLink()||keep2(entry.name)&&(assertSafeString(entry.name,`${relDir}/${entry.name}`,COPY_FILENAME_RE),assertNoDotDot(entry.name,`${relDir}/${entry.name}`),ensured||(mkdirSync3(safeJoin(projectRoot,relDir),{recursive:!0}),ensured=!0),cpSync(safeJoin(fromDir,entry.name),safeJoin(projectRoot,`${relDir}/${entry.name}`),{force:!0}))}var PRODUCT_STAGES=["spec","plan","grill","build","regress","verify","ship","loop","review","memory-promote","ac-test"],MODEL_ALIASES=["sonnet","opus","haiku","fable","inherit"],MODEL_ID_RE=/^claude-[a-z0-9][a-z0-9-]*$/,EFFORT_LEVELS=["low","medium","high","xhigh","max"];function isValidModel(model){return typeof model=="string"&&(MODEL_ALIASES.includes(model)||MODEL_ID_RE.test(model))}function isValidEffort(effort){return typeof effort=="string"&&EFFORT_LEVELS.includes(effort)}function checkModelsBlock(models){if(models==null)return{kind:"no-stages"};if(!isPlainObject(models))return invalid("shape","`models` is present but is not an object");let stages=models.stages;if(stages==null)return{kind:"no-stages"};if(!isPlainObject(stages))return invalid("shape","`models.stages` is present but is not an object");let reds=[];Object.hasOwn(stages,"default")||reds.push({kind:"default",detail:"missing required `default` stage entry (the resolution fallback)"});for(let[name,entry]of Object.entries(stages)){let stage=`stage ${JSON.stringify(name)}`;if(name!=="default"&&!PRODUCT_STAGES.includes(name)){reds.push({kind:"stage",detail:`${stage} is not a product stage \u2014 expected one of {${PRODUCT_STAGES.join(", ")}} or "default"`});continue}if(!isPlainObject(entry)){reds.push({kind:"entry",detail:`${stage} is not an object with {model, effort}`});continue}Object.hasOwn(entry,"model")?isValidModel(entry.model)||reds.push({kind:"model",detail:`${stage} model ${JSON.stringify(entry.model)} is not an alias {${MODEL_ALIASES.join(", ")}} nor a claude-* id`}):reds.push({kind:"model",detail:`${stage} missing \`model\``}),Object.hasOwn(entry,"effort")?isValidEffort(entry.effort)||reds.push({kind:"effort",detail:`${stage} effort ${JSON.stringify(entry.effort)} not in {${EFFORT_LEVELS.join(", ")}}`}):reds.push({kind:"effort",detail:`${stage} missing \`effort\``})}return reds.length>0?{kind:"invalid",reds}:{kind:"valid",stages}}function invalid(kind,detail){return{kind:"invalid",reds:[{kind,detail}]}}function resolveStageModel(stages,stage){let entry=Object.hasOwn(stages,stage)?stages[stage]:stages.default,{model,effort}=entry;return{model,effort}}function modelsCheckerCommand(floorDir,mode){return`node ${floorDir}/check-model-config.mjs ${mode}`}function resolvedStageLines(stages){let rows=[["default",stages.default.model,stages.default.effort,!1]];for(let stage of PRODUCT_STAGES){let{model,effort}=resolveStageModel(stages,stage);rows.push([stage,model,effort,!Object.hasOwn(stages,stage)])}let width=Math.max(...rows.map(([label])=>label.length))+3;return rows.map(([label,model,effort,via])=>`${label.padEnd(width)}${model} \xB7 ${effort}${via?" (default)":""}`)}function modelsLabelLines(floorDir){return["Claude Code applies each /pharn-* command's own model:/effort:","frontmatter, not this block. The block is the source of truth","that frontmatter is held to; to check the two agree:",modelsCheckerCommand(floorDir,"agreement")]}var UPSTREAM_CONFIG_FILE="pharn.config.json",MAX_UPSTREAM_CONFIG_BYTES=1024*1024;function readUpstreamModels(repoDir){let read=readBoundedFile(safeJoin(repoDir,UPSTREAM_CONFIG_FILE),MAX_UPSTREAM_CONFIG_BYTES);if(read.kind==="absent")return{kind:"absent"};if(read.kind==="unusable")return{kind:"invalid",reasons:[`pharn-oss's ${UPSTREAM_CONFIG_FILE} ${read.reason}`]};let config;try{config=JSON.parse(read.bytes.toString("utf8"))}catch{return{kind:"invalid",reasons:[`pharn-oss's ${UPSTREAM_CONFIG_FILE} is not valid JSON`]}}if(!isPlainObject(config))return{kind:"invalid",reasons:[`pharn-oss's ${UPSTREAM_CONFIG_FILE} is valid JSON but is not an object`]};let block=config.models;if(block==null)return{kind:"absent"};let check=checkModelsBlock(block);if(check.kind==="invalid")return{kind:"invalid",reasons:check.reds.map(red=>red.detail)};try{JSON.stringify(block)}catch{return{kind:"invalid",reasons:["pharn-oss's models block is nested too deeply to copy"]}}return{kind:"ok",block}}var SeamConfigError=class extends Error{constructor(message){super(message),this.name="SeamConfigError"}},RESOLUTION_STEPS=["official-skill","pinned-docs","fetch","model","ask"],SEAM_CONFIDENCE_LEVELS=["low","medium","high"],TERMINAL_STEP="ask",DEFAULT_SEAM_CONFIG={resolutionOrder:["official-skill","pinned-docs","model","fetch","ask"],modelConfidenceThreshold:"high",haltOnUnknown:!0},SEAM_KEYS=["resolutionOrder","modelConfidenceThreshold","haltOnUnknown"];function assertNoUnknownKeys(obj,known,label){for(let key2 of Object.keys(obj))if(!known.includes(key2))throw new SeamConfigError(`${label} has unknown key ${JSON.stringify(key2)} (expected one of ${known.join(", ")})`)}function validateSeamConfig(input){if(!isPlainObject(input))throw new SeamConfigError("seam must be an object");assertNoUnknownKeys(input,SEAM_KEYS,"seam");let{resolutionOrder}=input;if(!Array.isArray(resolutionOrder))throw new SeamConfigError(`seam.resolutionOrder must be an array (got ${JSON.stringify(resolutionOrder)})`);for(let i=0;i<resolutionOrder.length;i++){let step=resolutionOrder[i];if(!RESOLUTION_STEPS.includes(step))throw new SeamConfigError(`seam.resolutionOrder[${i}] has invalid step ${JSON.stringify(step)} (expected one of ${RESOLUTION_STEPS.join(", ")})`)}let seen=new Set;for(let step of resolutionOrder){if(seen.has(step))throw new SeamConfigError(`seam.resolutionOrder has duplicate step ${JSON.stringify(step)} (each step may appear at most once)`);seen.add(step)}if(!resolutionOrder.includes(TERMINAL_STEP))throw new SeamConfigError(`seam.resolutionOrder must contain "${TERMINAL_STEP}" \u2014 the terminal fallback cannot be configured away (fail-closed)`);let config={resolutionOrder};if(input.modelConfidenceThreshold!==void 0){let threshold=input.modelConfidenceThreshold;if(typeof threshold!="string"||!SEAM_CONFIDENCE_LEVELS.includes(threshold))throw new SeamConfigError(`seam.modelConfidenceThreshold ${JSON.stringify(threshold)} is invalid (expected one of ${SEAM_CONFIDENCE_LEVELS.join(", ")})`);config.modelConfidenceThreshold=threshold}if(input.modelConfidenceThreshold!==void 0&&!resolutionOrder.includes("model"))throw new SeamConfigError('seam.modelConfidenceThreshold is set but resolutionOrder has no "model" step for it to gate (add "model" or remove the threshold)');if(input.haltOnUnknown!==void 0){if(typeof input.haltOnUnknown!="boolean")throw new SeamConfigError(`seam.haltOnUnknown must be a boolean (got ${JSON.stringify(input.haltOnUnknown)})`);config.haltOnUnknown=input.haltOnUnknown}return config}import{createHash as createHash3}from"node:crypto";import{resolve as resolve6}from"node:path";var CONFIG_FILENAME="pharn.config.json",CAPABILITY_SOURCES=["auto","manual"],CapabilitySourceError=class extends Error{constructor(message){super(message),this.name="CapabilitySourceError"}},CapabilityEntryError=class extends Error{constructor(message){super(message),this.name="CapabilityEntryError"}};function validateCapabilityEntries(raw){Array.isArray(raw)&&raw.forEach((entry,i)=>{let at=`${CONFIG_FILENAME}: capabilities[${i}]`;if(!isPlainObject(entry))throw new CapabilityEntryError(`${at} must be an object with "name" and "role" (found ${JSON.stringify(entry)}). Fix it by hand.`);try{assertSafeString(entry.name,`${at}.name`,CAPABILITY_NAME_RE)}catch(err){throw new CapabilityEntryError(`${errorMessage(err)}. A capability name is one lowercase, hyphen-separated segment (e.g. "a11y", "n-plus-one"). Fix it by hand.`)}if(!ROLE_VALUES.some(r=>r===entry.role))throw new CapabilityEntryError(`${at}.role must be ${ROLE_VALUES.map(r=>`"${r}"`).join(" or ")} (found ${JSON.stringify(entry.role)}). Fix it by hand.`)})}function validateCapabilitySources(raw){Array.isArray(raw)&&raw.forEach((entry,i)=>{if(!isPlainObject(entry)||!("source"in entry))return;let source=entry.source;if(typeof source!="string"||!CAPABILITY_SOURCES.includes(source))throw new CapabilitySourceError(`${CONFIG_FILENAME}: capabilities[${i}].source must be ${CAPABILITY_SOURCES.map(s=>`"${s}"`).join(" or ")} (found ${JSON.stringify(source)}). Fix it by hand, or delete the field \u2014 an absent \`source\` is valid and the next \`pharn update\` will set it.`)})}var ConfigParseError=class extends Error{constructor(message){super(message),this.name="ConfigParseError"}};function parseLocation(err){let at=/\(line (\d{1,9}) column (\d{1,9})\)$/.exec(errorMessage(err));return at?` (line ${at[1]}, column ${at[2]})`:""}var CONTROL_CHARS_RE2=/[\x00-\x1f\x7f-\x9f]/g;function displayPath(path){return path.replace(CONTROL_CHARS_RE2,c=>`\\x${c.charCodeAt(0).toString(16).padStart(2,"0")}`)}function configPath(cwd){return resolve6(cwd,CONFIG_FILENAME)}var CLI_OWNED_KEYS=new Set(Object.keys({pharnVersion:!0,skillsVersion:!0,pendingSkillsVersion:!0,frozenCapabilities:!0,repo:!0,commit:!0,constitution:!0,isMultiTenant:!0,modules:!0,installedAt:!0,models:!0,seam:!0,stackAnswers:!0,installedSkills:!0,archetypes:!0,capabilities:!0,layout:!0}));function userOwnedConfigEntries(raw){return Object.fromEntries(Object.entries(raw).filter(([key2])=>!CLI_OWNED_KEYS.has(key2)))}function readPharnConfig(cwd){let path=configPath(cwd),read=readBoundedFile(path);if(read.kind!=="ok")return null;let text=read.bytes.toString("utf8"),raw;try{raw=JSON.parse(text)}catch(err){throw new ConfigParseError(`${displayPath(path)} is not valid JSON${parseLocation(err)}. The file exists \u2014 fix its syntax by hand. Do NOT run \`pharn init\` to clear it: init OVERWRITES this config, discarding your recorded capabilities, any manual \`pharn add\` provenance, and hand-edited models/seam blocks. If it is beyond repair, move it aside first (\`mv ${CONFIG_FILENAME} ${CONFIG_FILENAME}.bak\`), then run \`pharn init\`.`)}if(!isPlainObject(raw)||typeof raw.skillsVersion!="string"||!Array.isArray(raw.modules))return null;let seam=raw.seam!==void 0?validateSeamConfig(raw.seam):void 0;validateCapabilityEntries(raw.capabilities),validateCapabilitySources(raw.capabilities);let config={...raw,...seam!==void 0?{seam}:{}};return config.layout!=="pharn"&&config.layout!=="flat"&&delete config.layout,config.pendingSkillsVersion!==void 0&&(typeof config.pendingSkillsVersion!="string"||!VERSION_RE.test(config.pendingSkillsVersion))&&delete config.pendingSkillsVersion,config.frozenCapabilities!==void 0&&!isFrozenKeyList(config.frozenCapabilities)&&delete config.frozenCapabilities,config}function isFrozenKeyList(value){return Array.isArray(value)&&value.every(key2=>{if(typeof key2!="string")return!1;let sep2=key2.indexOf(":");if(sep2===-1)return!1;let role=key2.slice(0,sep2),name=key2.slice(sep2+1);return ROLE_VALUES.some(r=>r===role)&&CAPABILITY_NAME_RE.test(name)})}function loadConfigOrExit(cwd){try{let config=readPharnConfig(cwd);if(config)return config}catch(err){throw isConfigValidationError(err)&&(logError(err.message),process.exit(1)),err}logError("No pharn.config.json found. Run `pharn init` first."),process.exit(1)}function assertConfigUnchanged(cwd,snapshot,command){let current;try{current=readPharnConfig(cwd)}catch{current=null}if(!(current!==null&&JSON.stringify(current)===JSON.stringify(snapshot)))throw configChangedError(command)}function configChangedError(command){return new ProjectChangedError(`${CONFIG_FILENAME} changed while \`pharn ${command}\` was running \u2014 another pharn process (or an edit) wrote it after this command read it. Nothing was written. Re-run \`pharn ${command}\` to act on the current state.`)}function configFingerprint(cwd){let read=readBoundedFile(configPath(cwd));return read.kind==="absent"?"absent":read.kind==="unusable"?`unreadable:${read.code}`:`sha256:${createHash3("sha256").update(read.bytes).digest("hex")}`}function assertConfigFingerprintUnchanged(cwd,fingerprint,command){if(configFingerprint(cwd)!==fingerprint)throw configChangedError(command)}function isConfigValidationError(err){return err instanceof ConfigParseError||err instanceof SeamConfigError||err instanceof CapabilityEntryError||err instanceof CapabilitySourceError}async function writePharnConfig(cwd,config){await writeJsonAtomic(configPath(cwd),config)}function isArchetypeConfig(config){return Array.isArray(config.capabilities)}var LEGACY_CONFIG_MESSAGE="This project uses the legacy module layout (pre-archetype), which is no longer supported. Re-run `pharn init` to reinstall with the archetype/capability model.";function loadArchetypeConfigOrExit(cwd){let config=loadConfigOrExit(cwd);return isArchetypeConfig(config)||(logError(LEGACY_CONFIG_MESSAGE),process.exit(1)),config}function installManifest(repoDir,selection){return collectExpectedInstallPaths({repoDir,capabilities:selection.selected,layout:detectLayout(repoDir)})}function preflightInstall(repoDir,cwd,selection,manifest){prepareInstall(repoDir,cwd,selection.selected,manifest)}var NO_CARRY={manualKeys:new Set,kept:[],previousStamp:null};async function runInstallArchetype(repoDir,cwd,archetypes,selection,commit,carry=NO_CARRY,manifest){let startedAt=Date.now(),prepared=prepareInstall(repoDir,cwd,selection.selected,manifest),baseline=reinstallBaseline(cwd,carry.previousStamp),scan=scanDest({repoDir,projectRoot:cwd,rels:[...prepared.manifest.keys()],sources:manifestSources(repoDir,prepared.manifest),records:baseline});if(scan.drifted.length>0&&scan.unsafe.length===0){let backupDir=createBackup(cwd,scan.drifted);log4.info(`Backed up ${scan.drifted.length} file(s) to ${backupDir} before overwriting.`)}let s=spinner();s.start("Installing capabilities");let capabilities,settingsPreserved,skillsVersion,layout,docsWritten;try{let result=installCapabilities(repoDir,cwd,selection,prepared.manifest);capabilities=result.capabilities,settingsPreserved=result.settingsPreserved,layout=result.layout,docsWritten=result.docs,skillsVersion=readSkillsVersion(repoDir)}catch(err){throw s.stop("Failed to install capabilities"),err}s.stop("Capabilities installed"),settingsPreserved&&log4.warn("Existing .claude/settings.json preserved \u2014 compare it against pharn-oss to wire the PHARN hooks if needed.");let upstreamModels=readUpstreamModels(repoDir);upstreamModels.kind==="invalid"&&log4.warn(["pharn-oss's models block was not written; this pharn rejects it:",...upstreamModels.reasons.map(reason=>` ${terminalSafe(reason,{max:300})}`),"If pharn-oss changed its models format, upgrade pharn","(npm i -g @pharn-dev/pharn@latest) and run `pharn update`."].join(`
18
+ `));let models=upstreamModels.kind==="ok"?upstreamModels.block:void 0,docsMissing=layoutPaths(layout).docs.filter(doc=>!docsWritten.includes(doc));docsMissing.length&&log4.warn(`The fetched repo shipped no ${docsMissing.join(", ")} \u2014 not installed. Installed commands and checkers that cite ${docsMissing.length===1?"it":"them"} by path will not resolve.`);let frozenCapabilities=[...new Set(carry.kept.map(c=>`${c.role}:${c.name}`))].sort(),config={pharnVersion:PHARN_VERSION,skillsVersion,repo:REPO_URL.replace(/^github\.com\//,""),commit,modules:[],installedAt:new Date().toISOString(),models,seam:DEFAULT_SEAM_CONFIG,archetypes,capabilities:[...capabilities.map(c=>({...c,source:carry.manualKeys.has(`${c.role}:${c.name}`)?"manual":"auto"})),...carry.kept.map(c=>({...c}))],layout,...frozenCapabilities.length>0?{frozenCapabilities}:{}};await writeRecords(cwd,{skillsVersion,commit,files:{...keptRecords(baseline,carry,layout),...buildRecords(cwd,prepared.manifest.keys()),...models!==void 0?{[MODELS_RECORD_KEY]:modelsRecordHash(models)}:{}}}),await writePharnConfig(cwd,{...config,...readCarriedEntries(cwd)});let elapsed=((Date.now()-startedAt)/1e3).toFixed(1),check=pc2.green("\u2714"),grillers=capabilities.filter(c=>c.role==="griller").length,lenses=capabilities.filter(c=>c.role==="lens").length,modelsCheck=checkModelsBlock(models),modelsSection=modelsCheck.kind==="valid"?["",pc2.bold("Declared model per stage")+` ${pc2.dim("(pharn.config.json \u2192 models, from pharn-oss)")}`,...resolvedStageLines(modelsCheck.stages).map(line=>` ${line}`),...modelsLabelLines(layoutPaths(layout).floor).map(line=>` ${pc2.dim(line)}`)]:[],docsLine=docsWritten.length?`${check} ${docsWritten.length} trusted doc${docsWritten.length===1?"":"s"} written \u2192 ${pc2.dim(docsWritten.join(", "))}`:`${pc2.yellow("!")} no trusted docs written ${pc2.dim("(the fetched repo shipped none at their expected paths)")}`;outro([`${check} ${capabilities.length} capabilit${capabilities.length===1?"y":"ies"} installed \u2192 ${pc2.dim(`(${grillers} griller${grillers===1?"":"s"}, ${lenses} lens${lenses===1?"":"es"})`)}`,`${check} PHARN commands + hooks written \u2192 ${pc2.dim(".claude/")}`,docsLine,`${check} pharn.config.json written ${pc2.dim(`(skills v${skillsVersion}, archetypes: ${archetypes.join(", ")})`)}`,`${pc2.dim(`Done in ${elapsed}s`)}`,...modelsSection,"",pc2.bold("Next steps"),` ${pc2.cyan("1.")} ${pc2.bold("claude")} ${pc2.dim("open Claude Code")}`,` ${pc2.cyan("2.")} ${pc2.bold(FIRST_FEATURE_COMMAND)} ${pc2.dim("capture your first feature's intent")}`].join(`
18
19
  `))}function reinstallBaseline(cwd,previousStamp){return previousStamp===null?null:recordsBaseline(readRecords(cwd),previousStamp).records}function keptRecords(baseline,carry,layout){return carry.kept.length===0||baseline===null?{}:recordsUnderCapabilities(baseline,layoutPaths(layout),carry.kept)}function readCarriedEntries(cwd){let read=readBoundedFile(configPath(cwd));if(read.kind!=="ok")return{};try{let raw=JSON.parse(read.bytes.toString("utf8"));return isPlainObject(raw)?userOwnedConfigEntries(raw):{}}catch{return{}}}async function runInit(){showBanner(),intro("init wizard"),runGitPrereq(),interactiveAllowed({stdinIsTTY:process.stdin.isTTY,stdoutIsTTY:process.stdout.isTTY})||(logError("pharn init is interactive \u2014 run it in an interactive terminal. There is deliberately no --yes for init: it confirms before overwriting existing files, and auto-confirming that in a pipeline is what the prompt exists to prevent."),process.exit(1)),await runInitArchetype()}async function runInitArchetype(){let cwd=process.cwd(),{archetypes}=detectArchetypesFromProject(cwd);note2(archetypes.join(", "),"Detected archetypes");let proxyNotice=detectProxyNotice(process.env);proxyNotice&&log5.warn(proxyNoticeMessage(proxyNotice));let s=spinner2();s.start(`Fetching PHARN from ${REPO_URL}`);let repo;try{repo=await fetchRepo()}catch(err){s.stop("Failed to fetch PHARN"),reportFatal(`Could not reach ${REPO_URL}: ${errorMessage(err)}`,{err}),process.exit(1)}s.stop(`PHARN fetched from ${REPO_URL}`);let outcome="cancelled",failure=null,refusal2=null;try{let gate=minCliGate(repo.dir,PHARN_VERSION);if(gate.warning&&log5.warn(gate.warning),gate.refusal)refusal2=gate.refusal;else{let index=parseCapabilityIndex(repo.dir),unknownWarning=unknownCapabilitiesWarning(index.unknown);unknownWarning&&log5.warn(unknownWarning);let resolved=resolveCapabilities(archetypes,index),previous=readPreviousConfig(cwd),carry=carryOver(previous.config,index,resolved);carry.extra.length&&log5.info(`Keeping ${carry.extra.length} capabilit${carry.extra.length===1?"y":"ies"} you added by hand: ${carry.extra.map(capabilityKey).join(", ")}.`),carry.kept.length&&log5.info(`Keeping ${carry.kept.length} capabilit${carry.kept.length===1?"y":"ies"} as installed \u2014 upstream still ships ${carry.kept.length===1?"it":"them"} but this pharn cannot read ${carry.kept.length===1?"it":"them"} (see above): ${carry.kept.map(capabilityKey).join(", ")}. Files and config entries are left as they are; \`pharn update\` re-checks them.`),carry.gone.length&&log5.warn(`Not keeping ${carry.gone.length} capabilit${carry.gone.length===1?"y":"ies"} you added by hand that upstream no longer ships: ${carry.gone.map(capabilityKey).join(", ")}. ${carry.gone.length===1?"It is":"They are"} dropped from pharn.config.json; the files stay on disk.`);let extraKeys=new Set(carry.extra.map(capabilityKey)),selection=carry.extra.length?{selected:[...resolved.selected,...carry.extra.map(c=>({name:c.name,role:c.role,matched:"manual"}))],skipped:resolved.skipped.filter(s2=>!extraKeys.has(capabilityKey(s2)))}:resolved,manifest=await runArchetypeSummary(archetypes,selection)==="install"?installManifest(repo.dir,selection):null;manifest!==null&&preflightInstall(repo.dir,cwd,selection,manifest);let overwrite=manifest!==null?await confirmWriteTargets(repo.dir,cwd,selection,{manifest,records:reinstallBaseline(cwd,carry.previousStamp)}):"cancel";if(manifest!==null&&overwrite==="proceed"){let commit=repo.sha;await withProjectLock(cwd,"init",()=>(assertConfigFingerprintUnchanged(cwd,previous.fingerprint,"init"),runInstallArchetype(repo.dir,cwd,archetypes,selection,commit,carry,manifest))),outcome="installed"}}}catch(err){err instanceof ProjectLockedError?refusal2=err.message:failure={err}}finally{repo.cleanup()}refusal2&&(reportFatal(refusal2),process.exit(1)),failure&&(reportFatal(errorMessage(failure.err),failure),process.exit(1)),outcome==="cancelled"&&cancelAndExit()}function capabilityKey(cap){return`${cap.role}:${cap.name}`}function readPreviousConfig(cwd){let fingerprint=configFingerprint(cwd),config;try{config=readPharnConfig(cwd)}catch{config=null}return{fingerprint,config}}function carryOver(previous,index,resolved){let inIndex=new Set(index.capabilities.map(capabilityKey)),frozen=new Set(index.unknown.map(capabilityKey)),selected=new Set(resolved.selected.map(capabilityKey)),manualKeys=new Set,kept=[],extra=[],gone=[],seen=new Set;for(let cap of previous?.capabilities??[]){let k=capabilityKey(cap);if(!seen.has(k)){if(seen.add(k),frozen.has(k)){kept.push({...cap});continue}if(cap.source==="manual"){if(!inIndex.has(k)){gone.push(cap);continue}manualKeys.add(k),selected.has(k)||extra.push(cap)}}}return{manualKeys,kept,extra,gone,previousStamp:previous?{skillsVersion:previous.skillsVersion,commit:previous.commit}:null}}import{groupMultiselect,intro as intro2,isCancel as isCancel4,log as log6,outro as outro2,spinner as spinner3}from"@clack/prompts";import pc3 from"picocolors";function parseCapabilityArg(arg){if(!arg.includes(":"))return{name:arg};let idx=arg.indexOf(":"),role=arg.slice(0,idx),name=arg.slice(idx+1);return role!=="griller"&&role!=="lens"?{name,error:`Unknown role "${role}". Use griller:<name> or lens:<name>.`}:{name,role}}async function runAdd(capabilityArg){intro2("pharn add");let cwd=process.cwd(),config=loadArchetypeConfigOrExit(cwd);await runArchetypeAdd(config,cwd,capabilityArg)}function versionGate(repoDir,config){let fetched=readSkillsVersion(repoDir);if(fetched===config.skillsVersion||fetched===config.pendingSkillsVersion)return null;let base=`Skills version mismatch: pharn.config.json records v${config.skillsVersion}, but the fetched ${REPO_URL} is at v${fetched}. \`pharn add\` installs only at the version your project is already on`;return config.pendingSkillsVersion===void 0?`${base} \u2014 run \`pharn update\` first, then re-run \`pharn add\`.`:`${base} (v${config.pendingSkillsVersion} apart from files you edited). Run \`pharn update\` first; if it keeps skipping your edited files, either revert those edits or run \`pharn update --force\` (it backs them up to .pharn-backup/ first), then re-run \`pharn add\`.`}function addStamp(config,version,sha){return version===config.skillsVersion?{skillsVersion:version,commit:sha}:{skillsVersion:config.skillsVersion,commit:config.commit}}function layoutGate(repoDir,config){let clone=detectLayout(repoDir),recorded=configLayout(config);return clone===recorded?null:`Install layout mismatch: pharn.config.json records the \`${recorded}\` layout, but the fetched ${REPO_URL} uses the \`${clone}\` layout. \`pharn add\` installs only at the layout your project is already recorded at \u2014 adding here would put files where \`pharn remove\` and \`pharn status\` will never look for them. Run \`pharn update --force\` first, then re-run \`pharn add\`.`}async function runArchetypeAdd(config,cwd,arg){if(arg===void 0){await runAddPicker(config,cwd);return}let parsed=parseCapabilityArg(arg);parsed.error&&(logError(parsed.error),process.exit(1));let result;try{result=await withProjectLock(cwd,"add",async()=>{assertConfigUnchanged(cwd,config,"add");let proxyNotice=detectProxyNotice(process.env);proxyNotice&&log6.warn(proxyNoticeMessage(proxyNotice));let s=spinner3();s.start(`Fetching capabilities from ${REPO_URL}`);let repo;try{repo=await fetchRepo(),s.stop(`Capabilities fetched from ${REPO_URL}`)}catch(err){throw s.stop("Failed to fetch capabilities"),err}try{let gate=minCliGate(repo.dir,PHARN_VERSION);gate.warning&&log6.warn(gate.warning);let refusal2=gate.refusal??versionGate(repo.dir,config)??layoutGate(repo.dir,config);return refusal2?{kind:"error",message:refusal2}:await resolveArchetypeAdd(repo.dir,repo.sha,config,cwd,parsed,arg)}finally{repo.cleanup()}})}catch(err){result=err instanceof ProjectLockedError?{kind:"error",message:err.message}:{kind:"error",message:errorMessage(err),cause:{err}}}if(result.kind==="error"&&(reportFatal(result.message,result.cause),process.exit(1)),result.kind==="noop"){outro2(`${result.name} is already installed.`);return}outro2(`${pc3.green("\u2714")} Added ${result.name} ${pc3.dim(`(skills v${result.version})`)}`)}async function runAddPicker(config,cwd){interactiveAllowed({stdinIsTTY:process.stdin.isTTY,stdoutIsTTY:process.stdout.isTTY})||(logError("Specify a capability (e.g. `pharn add a11y` or `pharn add lens:n-plus-one`), or run `pharn add` in an interactive terminal to pick from a list."),process.exit(1));let outcome;try{outcome=await withProjectLock(cwd,"add",async()=>{assertConfigUnchanged(cwd,config,"add");let proxyNotice=detectProxyNotice(process.env);proxyNotice&&log6.warn(proxyNoticeMessage(proxyNotice));let s=spinner3();s.start(`Fetching capabilities from ${REPO_URL}`);let repo;try{repo=await fetchRepo(),s.stop(`Capabilities fetched from ${REPO_URL}`)}catch(err){throw s.stop("Failed to fetch capabilities"),err}try{let gate=minCliGate(repo.dir,PHARN_VERSION);gate.warning&&log6.warn(gate.warning);let refusal2=gate.refusal??versionGate(repo.dir,config)??layoutGate(repo.dir,config);return refusal2?{kind:"error",message:refusal2}:await resolveAddPicker(repo.dir,repo.sha,config,cwd)}finally{repo.cleanup()}})}catch(err){outcome=err instanceof ProjectLockedError?{kind:"error",message:err.message}:{kind:"error",message:errorMessage(err),cause:{err}}}if(outcome.kind==="error"&&(reportFatal(outcome.message,outcome.cause),process.exit(1)),outcome.kind==="all-installed"){outro2("All available capabilities are already installed.");return}if(outcome.kind==="cancelled"&&cancelAndExit(),outcome.kind==="none"){outro2("Nothing selected. No capabilities were added.");return}outro2(`${pc3.green("\u2714")} Added ${outcome.added.length} ${plural(outcome.added.length)}.`)}async function resolveAddPicker(repoDir,sha,config,cwd){let index=parseCapabilityIndex(repoDir),unknownWarning=unknownCapabilitiesWarning(index.unknown);unknownWarning&&log6.warn(unknownWarning);let installed2=config.capabilities??[],{groups,availableCount}=buildAddSelection(index,installed2);if(availableCount===0)return{kind:"all-installed"};installed2.length>0&&log6.info(`Installed (${installed2.length}): ${installed2.map(c=>`${c.name} (${c.role})`).join(", ")}`);let picked=await groupMultiselect({message:"Select capabilities to add",options:groups,required:!1,selectableGroups:!1});if(isCancel4(picked))return{kind:"cancelled"};let values=picked;if(values.length===0)return{kind:"none"};let added=[],cfg=config;for(let value of values){let parsed=parseCapabilityArg(value),result=await resolveArchetypeAdd(repoDir,sha,cfg,cwd,parsed,value,index);result.kind==="added"?(log6.info(`${pc3.green("\u2714")} Added ${result.name}`),added.push(result.name),cfg={...cfg,...result.stamp,capabilities:[...cfg.capabilities??[],{name:parsed.name,role:parsed.role,source:"manual"}]}):result.kind==="noop"?log6.info(`${result.name} is already installed.`):logError(`\u26A0 ${result.message}`)}return{kind:"installed",added}}function plural(n){return n===1?"capability":"capabilities"}async function resolveArchetypeAdd(repoDir,sha,config,cwd,parsed,arg,parsedIndex){let index=parsedIndex;if(index===void 0){index=parseCapabilityIndex(repoDir);let unknownWarning=unknownCapabilitiesWarning(index.unknown);unknownWarning&&log6.warn(unknownWarning)}let matches=index.capabilities.filter(c=>c.name===parsed.name&&(parsed.role===void 0||c.role===parsed.role));if(matches.length===0){let valid=index.capabilities.map(c=>`${c.role}:${c.name}`).sort();return{kind:"error",message:`Unknown capability "${arg}". Valid capabilities:
19
20
  ${valid.join(`
20
- `)}`}}if(matches.length>1)return{kind:"error",message:`"${parsed.name}" is ambiguous \u2014 use ${matches.map(m=>`${m.role}:${m.name}`).join(" or ")}.`};let cap=matches[0],existing=config.capabilities??[];if(existing.some(c=>c.name===cap.name&&c.role===cap.role))return{kind:"noop",name:cap.name};let paths=layoutPaths(detectLayout(repoDir)),cloneRels=capabilityCloneFiles(repoDir,paths,cap),scan=scanDest({repoDir,projectRoot:cwd,rels:cloneRels});if(scan.unsafe.length>0){let first=scan.unsafe[0],more=scan.unsafe.length>1?` (and ${scan.unsafe.length-1} more under the same capability)`:"";return{kind:"error",message:`Refusing to add ${cap.name}: \`${first.link}\` in your project is a symlink, and \`${first.rel}\` sits under it${more}. \`pharn add\` copies the whole capability directory, which would write THROUGH that link and replace files it points at \u2014 possibly outside your project \u2014 and those cannot be backed up. Replace the symlink with a real directory (or move it aside), then re-run \`pharn add ${cap.name}\`.`}}if(scan.drifted.length>0){let backupDir=createBackup(cwd,scan.drifted);log6.info(`Backed up ${scan.drifted.length} file(s) to ${backupDir} before overwriting.`)}installCapabilityDirs(repoDir,cwd,[{name:cap.name,role:cap.role}]);let version=readSkillsVersion(repoDir),stamp=addStamp(config,version,sha),capabilities=[...existing,{name:cap.name,role:cap.role,source:"manual"}];return await mergeCapabilityRecords(cwd,config,cloneRels,stamp.skillsVersion,stamp.commit),await writePharnConfig(cwd,{...config,...stamp,capabilities,installedAt:new Date().toISOString()}),{kind:"added",name:cap.name,version,stamp}}async function mergeCapabilityRecords(cwd,config,cloneRels,skillsVersion,commit){let{records}=recordsBaseline(readRecords(cwd),{skillsVersion:config.skillsVersion,commit:config.commit});if(records===null)return;let added=buildRecords(cwd,cloneRels);await writeRecords(cwd,{skillsVersion,commit,files:mergeRecords(records,added)})}import{existsSync as existsSync8,rmSync as rmSync3}from"node:fs";import{confirm as confirm3,groupMultiselect as groupMultiselect2,intro as intro3,isCancel as isCancel5,log as log7,outro as outro3}from"@clack/prompts";import pc4 from"picocolors";async function runRemove(arg){intro3("pharn remove");let cwd=process.cwd(),config=loadArchetypeConfigOrExit(cwd);try{await removeCapability(cwd,config,arg)}catch(err){throw err instanceof ProjectLockedError&&(logError(err.message),process.exit(1)),err}}async function removeCapability(cwd,config,arg){let installed2=config.capabilities??[];if(arg===void 0){await runRemovePicker(cwd,config,installed2);return}await removeNamed(cwd,config,installed2,arg)}function deleteCapabilityDir(cwd,paths,target){let dir=safeChildJoin(safeJoin(cwd,capabilitySubtree(paths,target.role)),target.name),existed=existsSync8(dir);return existed&&rmSync3(dir,{recursive:!0,force:!0}),existed}function refuseSymlinkedTargets(cwd,paths,targets){for(let target of targets){let linked;try{linked=findSymlinkComponent(cwd,capabilityRelDir(paths,target))}catch{linked=null}linked!==null&&(logError(`Refusing to remove ${target.name} (${target.role}): ${linked} is a symbolic link, so deleting through it would delete files OUTSIDE the project. Nothing was removed. Delete the directory yourself if that is what you want.`),process.exit(1))}}function capabilityRelDir(paths,capability){return`${capabilitySubtree(paths,capability.role)}/${capability.name}`}async function pruneCapabilityRecords(cwd,config,paths,targets){let{records}=recordsBaseline(readRecords(cwd),{skillsVersion:config.skillsVersion,commit:config.commit});if(records===null)return;let prefixes=targets.map(t=>`${capabilityRelDir(paths,t)}/`),kept={},dropped=0;for(let[key2,hash]of Object.entries(records))prefixes.some(prefix=>key2.startsWith(prefix))?dropped++:kept[key2]=hash;dropped!==0&&await writeRecords(cwd,{skillsVersion:config.skillsVersion,commit:config.commit,files:kept})}function warnIfAutoSelected(targets){let auto=targets.filter(c=>c.source==="auto");if(auto.length===0)return;let names=auto.map(c=>`${c.name} (${c.role})`).join(", ");log7.warn(`${names} ${auto.length===1?"is":"are"} selected automatically for your archetypes, so the next \`pharn update\` will reinstall ${auto.length===1?"it":"them"} (and will name ${auto.length===1?"it":"them"} in its report).`)}async function removeNamed(cwd,config,installed2,address){let parsed=parseCapabilityArg(address);parsed.error&&(logError(parsed.error),process.exit(1));let matches=installed2.filter(c=>c.name===parsed.name&&(parsed.role===void 0||c.role===parsed.role));if(matches.length===0){let valid=installed2.map(c=>`${c.role}:${c.name}`).sort(),hint=valid.length?` Installed: ${valid.join(", ")}.`:" No capabilities are installed.";log7.warn(`"${address}" is not an installed capability.${hint}`),outro3("Nothing was removed.");return}matches.length>1&&(logError(`"${parsed.name}" is ambiguous \u2014 use ${matches.map(m=>`${m.role}:${m.name}`).join(" or ")}.`),process.exit(1));let target=matches[0],paths=layoutPaths(configLayout(config));refuseSymlinkedTargets(cwd,paths,[target]);let note6=await withProjectLock(cwd,"remove",async()=>{assertConfigUnchanged(cwd,config,"remove");let existed=deleteCapabilityDir(cwd,paths,target);return await pruneCapabilityRecords(cwd,config,paths,[target]),await writePharnConfig(cwd,{...config,capabilities:installed2.filter(c=>!(c.name===target.name&&c.role===target.role)),installedAt:new Date().toISOString()}),existed?"":pc4.dim(" (its files were already gone)")});warnIfAutoSelected([target]),outro3(`${pc4.green("\u2714")} Removed ${target.name} (${target.role})${note6}`)}async function runRemovePicker(cwd,config,installed2){if(installed2.length===0){outro3("No capabilities are installed.");return}interactiveAllowed({stdinIsTTY:process.stdin.isTTY,stdoutIsTTY:process.stdout.isTTY})||(logError("Specify a capability to remove (e.g. `pharn remove a11y`), or run `pharn remove` in an interactive terminal to pick from a list."),process.exit(1));let{groups}=buildRemoveSelection(installed2),picked=await groupMultiselect2({message:"Select capabilities to remove",options:groups,required:!1,selectableGroups:!1});isCancel5(picked)&&cancelAndExit();let values=picked;if(values.length===0){outro3("Nothing selected. No capabilities were removed.");return}let targets=[];for(let value of values){let parsed=parseCapabilityArg(value),target=installed2.find(c=>c.name===parsed.name&&c.role===parsed.role);target&&targets.push(target)}let paths=layoutPaths(configLayout(config));refuseSymlinkedTargets(cwd,paths,targets);let ok=await confirm3({message:`Remove ${targets.length} ${plural2(targets.length)}: ${targets.map(t=>`${t.name} (${t.role})`).join(", ")}?`,initialValue:!1});(isCancel5(ok)||ok!==!0)&&cancelAndExit(),await withProjectLock(cwd,"remove",async()=>{assertConfigUnchanged(cwd,config,"remove");for(let target of targets)deleteCapabilityDir(cwd,paths,target);await pruneCapabilityRecords(cwd,config,paths,targets);let removed=new Set(targets.map(t=>`${t.role}:${t.name}`));await writePharnConfig(cwd,{...config,capabilities:installed2.filter(c=>!removed.has(`${c.role}:${c.name}`)),installedAt:new Date().toISOString()})}),warnIfAutoSelected(targets),outro3(`${pc4.green("\u2714")} Removed ${targets.length} ${plural2(targets.length)}.`)}function plural2(n){return n===1?"capability":"capabilities"}import{closeSync as closeSync6,constants as constants3,fstatSync as fstatSync4,openSync as openSync6,readSync as readSync4}from"node:fs";import{dirname as dirname4}from"node:path";var LOCAL_SETTINGS_FILE=".claude/settings.local.json",MAX_SETTINGS_BYTES=256*1024,PROJECT_OPEN=constants3.O_RDONLY|(constants3.O_NONBLOCK??0),UPSTREAM_OPEN=PROJECT_OPEN|(constants3.O_NOFOLLOW??0);function readSettings(base,rel,followLeaf){let linked;try{linked=findSymlinkComponent(base,followLeaf?dirname4(rel):rel)}catch{return{kind:"unreadable",reason:"a path component is not a directory"}}if(linked!==null)return{kind:"unreadable",reason:`${linked} is a symbolic link`};let fd;try{fd=openSync6(safeJoin(base,rel),followLeaf?PROJECT_OPEN:UPSTREAM_OPEN)}catch(err){let code=err.code;return code==="ENOENT"?{kind:"absent"}:code==="ENOTDIR"?{kind:"unreadable",reason:"a path component is not a directory"}:{kind:"unreadable",reason:"it cannot be opened"}}try{let st=fstatSync4(fd);if(!st.isFile())return{kind:"unreadable",reason:"it is not a regular file"};if(st.size>MAX_SETTINGS_BYTES)return{kind:"unreadable",reason:"it is larger than 256 KB"};let buf=Buffer.alloc(st.size),off=0;for(;off<buf.length;){let n=readSync4(fd,buf,off,buf.length-off,off);if(n===0)break;off+=n}try{return{kind:"ok",value:JSON.parse(buf.subarray(0,off).toString("utf8"))}}catch{return{kind:"unreadable",reason:"it is not valid JSON"}}}catch{return{kind:"unreadable",reason:"it cannot be read"}}finally{closeSync6(fd)}}function hookEntries(settings){if(!isPlainObject(settings)||!isPlainObject(settings.hooks))return null;let out=[];for(let[event,groups]of Object.entries(settings.hooks))if(Array.isArray(groups))for(let group of groups){if(!isPlainObject(group)||!Array.isArray(group.hooks))continue;let matcher=typeof group.matcher=="string"?group.matcher:"";for(let hook of group.hooks){if(!isPlainObject(hook)||typeof hook.command!="string")continue;let args=Array.isArray(hook.args)?hook.args.filter(a=>typeof a=="string"):[];out.push({event,matcher,command:hook.command,args})}}return out}function entryKey(e){return JSON.stringify([e.event,e.matcher,e.command,e.args])}function wiredKeys(result){return new Set(result.kind==="ok"?(hookEntries(result.value)??[]).map(entryKey):[])}function diffHookWiring(repoDir,projectRoot){let upstream=readSettings(repoDir,CLAUDE_SETTINGS_FILE,!1),upstreamEntries=upstream.kind==="ok"?hookEntries(upstream.value):null;if(upstreamEntries===null||upstreamEntries.length===0)return{status:"no-upstream",missing:[]};let shared=readSettings(projectRoot,CLAUDE_SETTINGS_FILE,!0),local=readSettings(projectRoot,LOCAL_SETTINGS_FILE,!0);for(let[file,result]of[[CLAUDE_SETTINGS_FILE,shared],[LOCAL_SETTINGS_FILE,local]])if(result.kind==="unreadable")return{status:"unreadable",missing:upstreamEntries,file,reason:result.reason};if(shared.kind==="absent"&&local.kind==="absent")return{status:"project-absent",missing:upstreamEntries};let inShared=wiredKeys(shared),inLocal=wiredKeys(local),missing=upstreamEntries.filter(e=>!inShared.has(entryKey(e))&&!inLocal.has(entryKey(e))),localOnly=upstreamEntries.filter(e=>!inShared.has(entryKey(e))&&inLocal.has(entryKey(e)));return missing.length?{status:"missing",missing}:localOnly.length?{status:"local-only",missing,localOnly}:{status:"match",missing}}function hookWiringFails(diff){return diff.status==="missing"||diff.status==="unreadable"||diff.status==="project-absent"}var ESCAPED=new RegExp(`[\\x00-\\x1f\\x7f-\\x9f\\p{Cf}${String.fromCodePoint(8232,8233)}]`,"gu"),MAX_LINE=300;function escapeUnsafe(text){return text.replace(ESCAPED,ch=>[...Array(ch.length).keys()].map(i=>`\\u${ch.charCodeAt(i).toString(16).padStart(4,"0")}`).join(""))}function displayHook(e){let hook=e.args.length>0?{type:"command",command:e.command,args:e.args}:{type:"command",command:e.command},where=e.matcher?`${e.event} [${e.matcher}]`:e.event,line=`${escapeUnsafe(where)}: ${escapeUnsafe(JSON.stringify(hook))}`;return line.length>MAX_LINE?`${line.slice(0,MAX_LINE)}\u2026`:line}var FILES=`${CLAUDE_SETTINGS_FILE} or ${LOCAL_SETTINGS_FILE}`;function hookWiringLines(diff){if(diff.status==="match"||diff.status==="no-upstream")return null;if(diff.status==="local-only"){let only=diff.localOnly??[];return[` Every hook upstream wires is wired \u2014 ${only.length} of them only in ${LOCAL_SETTINGS_FILE}.`," They run for you, not for teammates or CI:",...only.map(e=>` ${displayHook(e)}`)]}return[diff.status==="project-absent"?` Neither ${FILES} exists. Upstream wires:`:diff.status==="unreadable"?` ${diff.file??CLAUDE_SETTINGS_FILE} could not be read (${diff.reason??"unknown"}). Upstream wires:`:` Upstream wires ${diff.missing.length} hook(s) that neither ${FILES} does:`,...diff.missing.map(e=>` ${displayHook(e)}`),"",` pharn never writes ${CLAUDE_SETTINGS_FILE} after init \u2014 merge these by hand:`," each line is one hook, as JSON, under its event [matcher]. Matching is"," textual: an equivalent hook you wrote differently is listed too."]}import{confirm as confirm4,intro as intro4,isCancel as isCancel6,log as log8,note as note3,outro as outro4,spinner as spinner4}from"@clack/prompts";import pc5 from"picocolors";function key(cap){return`${cap.role}:${cap.name}`}function mergeCapabilities(selection,previous,frozen){let resolved=new Set(selection.selected.map(key)),inIndex=new Set([...selection.selected.map(key),...selection.skipped.map(key)]),previousSource=new Map,seenPrevious=new Set;for(let cap of previous){let k=key(cap);seenPrevious.has(k)||(seenPrevious.add(k),previousSource.set(k,cap.source))}let capabilities=[],changes=[];for(let sel of selection.selected){let k=key(sel);if(!seenPrevious.has(k)){let cap={name:sel.name,role:sel.role,source:"auto"};capabilities.push(cap),changes.push({cap,reason:"added"});continue}let source=previousSource.get(k)==="manual"?"manual":"auto";capabilities.push({name:sel.name,role:sel.role,source})}for(let cap of previous){let k=key(cap);if(resolved.has(k))continue;if(frozen.has(k)){let kept={...cap};capabilities.push(kept),changes.push({cap:kept,reason:"kept-frozen"});continue}if(cap.source==="auto"){changes.push({cap,reason:"dropped-unselected"});continue}let inferred={...cap,source:"manual"};if(!inIndex.has(k)){changes.push({cap:inferred,reason:"dropped-gone"});continue}capabilities.push(inferred),cap.source===void 0&&changes.push({cap:inferred,reason:"kept-manual"})}return{capabilities,changes}}import{copyFileSync as copyFileSync2,lstatSync as lstatSync8,mkdirSync as mkdirSync4}from"node:fs";import{dirname as dirname5}from"node:path";var ApplyError=class extends Error{constructor(message,written){super(message);this.written=written;this.name="ApplyError"}written};function readDiskState(projectRoot,rel){let dest=safeJoin(projectRoot,rel),stat;try{let link=findSymlinkComponent(projectRoot,rel);if(link!==null)return{kind:"unreadable",reason:link===toPosix(rel)?"the path is a symlink":`${link} is a symlink`};stat=lstatSync8(dest,{throwIfNoEntry:!1})}catch{return{kind:"unreadable",reason:"the path could not be inspected"}}if(!stat)return{kind:"absent"};if(!stat.isFile())return{kind:"unreadable",reason:"the path is not a regular file"};try{return{kind:"file",hash:sha256File(dest)}}catch{return{kind:"unreadable",reason:"the file could not be read"}}}function applyWrites(params){let{projectRoot,expected,writes}=params,written=[];for(let rel of writes){let from=expected.get(rel);if(from===void 0)throw new ApplyError(`Internal: no source for planned write ${rel}.`,written);try{let to=safeJoin(projectRoot,rel),link=findSymlinkComponent(projectRoot,rel);if(link!==null)throw new ManifestValidationError(`${link} is a symlink; refusing to write through it.`);mkdirSync4(dirname5(to),{recursive:!0}),copyFileSync2(from,to),written.push(rel)}catch(err){throw new ApplyError(`Failed to write ${rel}: ${err instanceof Error?err.message:String(err)}`,written)}}return written}async function runUpdate(opts={}){intro4("pharn update");let cwd=process.cwd(),config=loadArchetypeConfigOrExit(cwd),yes=opts.yes??!1;!yes&&!interactiveAllowed({stdinIsTTY:process.stdin.isTTY,stdoutIsTTY:process.stdout.isTTY})&&(logError("pharn update needs to confirm before it writes. Run it in an interactive terminal, or pass --yes to confirm automatically (e.g. `pharn update --yes`)."),process.exit(1)),await runArchetypeUpdate(config,cwd,opts.force??!1,yes)}async function runArchetypeUpdate(config,cwd,force,yes){let proxyNotice=detectProxyNotice(process.env);proxyNotice&&log8.warn(proxyNoticeMessage(proxyNotice));let s=spinner4();s.start("Checking for updates");let latest;try{latest=await fetchRemoteSkillsVersion(),s.stop(`Latest skills v${latest}`)}catch(err){s.stop("Failed to check for updates"),reportFatal(errorMessage(err),{err}),process.exit(1)}let current=config.skillsVersion===latest,recheckFrozen=(config.frozenCapabilities??[]).length>0;if(current&&!force&&!recheckFrozen){outro4(`Already up to date (skills v${config.skillsVersion}).`);return}if(note3([current?row("Skills version",`v${config.skillsVersion} (re-applying)`):row("Skills version",`v${config.skillsVersion} \u2192 v${latest}`),"",row("Archetypes",(config.archetypes??[]).join(", ")||"(none)"),"",force?pc5.dim(` --force: files you changed will be OVERWRITTEN (each is copied to ${BACKUP_DIR}/ first).`):pc5.dim(" Files you have changed are kept, not overwritten \u2014 they are listed at the end."),pc5.dim(" Re-resolves your archetypes against the latest capabilities and re-copies them."),pc5.dim(" https://github.com/pharn-dev/pharn-oss/blob/main/CHANGELOG.md")].join(`
21
- `)),!yes){let ok=await confirm4({message:force?"Re-fetch capabilities and overwrite your changes?":"Re-fetch capabilities at the latest version?",initialValue:!0});(isCancel6(ok)||ok!==!0)&&cancelAndExit()}let outcome=null,backupRef={current:null},spinnerRef={current:null},refusalRef={current:null},failure=null;try{outcome=await withProjectLock(cwd,"update",async()=>{assertConfigUnchanged(cwd,config,"update");let s2=spinner4();spinnerRef.current=s2,s2.start(`Updating from ${REPO_URL}`);let repo=await fetchRepo();try{let gate=minCliGate(repo.dir,PHARN_VERSION);if(gate.warning&&log8.warn(gate.warning),gate.refusal)return s2.stop("Update refused"),refusalRef.current=gate.refusal,null;let applied=await applyUpdate(repo.dir,repo.sha,config,cwd,force,backup=>{backupRef.current=backup,s2.stop("Backup written"),printBackupNotice(backup,{aborted:!1});let s3=spinner4();spinnerRef.current=s3,s3.start("Writing files")});return spinnerRef.current.stop(applied.plan.writes.length?"Capabilities updated":"Nothing to write"),{...applied,hookWiring:diffHookWiring(repo.dir,cwd)}}finally{repo.cleanup()}})}catch(err){err instanceof ProjectLockedError?(spinnerRef.current?.stop("Update refused"),refusalRef.current=err.message):(spinnerRef.current?.stop("Update failed"),failure={err})}let refusal2=refusalRef.current;refusal2&&(reportFatal(refusal2),process.exit(1)),(failure||!outcome)&&(failure?reportFatal(errorMessage(failure.err),failure):reportFatal("Update failed."),backupRef.current&&printBackupNotice(backupRef.current,{aborted:!0}),process.exit(1)),reportOutcome(outcome,force)}async function applyUpdate(repoDir,sha,config,cwd,force,onBackup){let index=parseCapabilityIndex(repoDir),unknownWarning=unknownCapabilitiesWarning(index.unknown);unknownWarning&&log8.warn(unknownWarning);let frozen=new Set(index.unknown.map(u=>`${u.role}:${u.name}`)),selection=resolveCapabilities(config.archetypes??[],index),merged=mergeCapabilities(selection,config.capabilities??[],frozen),configCapabilities=merged.capabilities,manifestCapabilities=configCapabilities.filter(cap=>!frozen.has(`${cap.role}:${cap.name}`)),installedVersion=readSkillsVersion(repoDir),layout=detectLayout(repoDir),previousLayout=configLayout(config),expected=collectExpectedInstallPaths({repoDir,capabilities:manifestCapabilities,layout}),latestHashes=new Map;for(let[rel,source]of expected)latestHashes.set(rel,sha256File(source));let diskStates=new Map;for(let rel of expected.keys())diskStates.set(rel,readDiskState(cwd,rel));let{records,note:recordsNote}=recordsBaseline(readRecords(cwd),{skillsVersion:config.skillsVersion,commit:config.commit}),plan=planUpdate({latestHashes,diskStates,records,force}),backup=plan.backups.length>0?{dir:createBackup(cwd,plan.backups),count:plan.backups.length}:null;backup&&onBackup(backup);let versionWithheld=plan.counts.skipped>0,nextSkillsVersion=versionWithheld?config.skillsVersion:installedVersion,nextCommit=versionWithheld?config.commit:sha,pendingSkillsVersion=versionWithheld&&installedVersion!==nextSkillsVersion&&plan.skipped.every(g=>USER_EDIT_SKIPS.has(g.label))?installedVersion:void 0,written;try{written=applyWrites({projectRoot:cwd,expected,writes:plan.writes})}catch(err){throw err instanceof ApplyError&&err.written.length>0&&records!==null&&await writeRecords(cwd,{skillsVersion:config.skillsVersion,commit:config.commit,files:{...records,...buildRecords(cwd,err.written)}}),err}let frozenRecords=records===null?{}:recordsUnderCapabilities(records,layoutPaths(layout),configCapabilities.filter(cap=>frozen.has(`${cap.role}:${cap.name}`)));await writeRecords(cwd,{skillsVersion:nextSkillsVersion,commit:nextCommit,files:{...frozenRecords,...plan.nextRecords,...buildRecords(cwd,written)}});let previouslyFrozen=new Set(config.frozenCapabilities??[]),skippedRels=plan.skipped.flatMap(group=>group.rels),paths=layoutPaths(layout),hasSkippedFile=cap=>{let prefix=`${capabilitySubtree(paths,cap.role)}/${cap.name}/`;return skippedRels.some(rel=>rel.startsWith(prefix))},frozenCapabilities=[...new Set(configCapabilities.filter(cap=>{let key2=`${cap.role}:${cap.name}`;return frozen.has(key2)||previouslyFrozen.has(key2)&&hasSkippedFile(cap)}).map(cap=>`${cap.role}:${cap.name}`))].sort(),{pendingSkillsVersion:_previousPending,frozenCapabilities:_previousFrozen,...rest}=config;return await writePharnConfig(cwd,{...rest,skillsVersion:nextSkillsVersion,commit:nextCommit,...pendingSkillsVersion!==void 0?{pendingSkillsVersion}:{},...frozenCapabilities.length>0?{frozenCapabilities}:{},capabilities:configCapabilities,layout,installedAt:new Date().toISOString()}),{installedVersion,recordedVersion:nextSkillsVersion,capCount:configCapabilities.length,capabilityChanges:merged.changes,plan,backup,recordsNote,versionWithheld,abandonedLayout:previousLayout!==layout&&written.length>0?previousLayout:null,featuresReadmeRelocation:records!==null&&FEATURES_README in records&&resolveFeaturesReadme(repoDir,layout)!==FEATURES_README}}var USER_EDIT_SKIPS=new Set(["modified","unrecorded"]),FORCEABLE_SKIPS=new Set(["modified","unrecorded","unverifiable"]);function reportOutcome(outcome,force){let{plan,recordsNote,versionWithheld}=outcome,{counts}=plan;recordsNote&&log8.warn(`\u26A0 ${recordsNote}`),reportCapabilityChanges(outcome.capabilityChanges);let hookLines=outcome.hookWiring?hookWiringLines(outcome.hookWiring):null;hookLines&&note3(hookLines.join(`
21
+ `)}`}}if(matches.length>1)return{kind:"error",message:`"${parsed.name}" is ambiguous \u2014 use ${matches.map(m=>`${m.role}:${m.name}`).join(" or ")}.`};let cap=matches[0],existing=config.capabilities??[];if(existing.some(c=>c.name===cap.name&&c.role===cap.role))return{kind:"noop",name:cap.name};let paths=layoutPaths(detectLayout(repoDir)),cloneRels=capabilityCloneFiles(repoDir,paths,cap),scan=scanDest({repoDir,projectRoot:cwd,rels:cloneRels});if(scan.unsafe.length>0){let first=scan.unsafe[0],more=scan.unsafe.length>1?` (and ${scan.unsafe.length-1} more under the same capability)`:"";return{kind:"error",message:`Refusing to add ${cap.name}: \`${first.link}\` in your project is a symlink, and \`${first.rel}\` sits under it${more}. \`pharn add\` copies the whole capability directory, which would write THROUGH that link and replace files it points at \u2014 possibly outside your project \u2014 and those cannot be backed up. Replace the symlink with a real directory (or move it aside), then re-run \`pharn add ${cap.name}\`.`}}if(scan.drifted.length>0){let backupDir=createBackup(cwd,scan.drifted);log6.info(`Backed up ${scan.drifted.length} file(s) to ${backupDir} before overwriting.`)}installCapabilityDirs(repoDir,cwd,[{name:cap.name,role:cap.role}]);let version=readSkillsVersion(repoDir),stamp=addStamp(config,version,sha),capabilities=[...existing,{name:cap.name,role:cap.role,source:"manual"}];return await mergeCapabilityRecords(cwd,config,cloneRels,stamp.skillsVersion,stamp.commit),await writePharnConfig(cwd,{...config,...stamp,capabilities,installedAt:new Date().toISOString()}),{kind:"added",name:cap.name,version,stamp}}async function mergeCapabilityRecords(cwd,config,cloneRels,skillsVersion,commit){let{records}=recordsBaseline(readRecords(cwd),{skillsVersion:config.skillsVersion,commit:config.commit});if(records===null)return;let added=buildRecords(cwd,cloneRels);await writeRecords(cwd,{skillsVersion,commit,files:mergeRecords(records,added)})}import{existsSync as existsSync8,rmSync as rmSync3}from"node:fs";import{confirm as confirm3,groupMultiselect as groupMultiselect2,intro as intro3,isCancel as isCancel5,log as log7,outro as outro3}from"@clack/prompts";import pc4 from"picocolors";async function runRemove(arg){intro3("pharn remove");let cwd=process.cwd(),config=loadArchetypeConfigOrExit(cwd);try{await removeCapability(cwd,config,arg)}catch(err){throw err instanceof ProjectLockedError&&(logError(err.message),process.exit(1)),err}}async function removeCapability(cwd,config,arg){let installed2=config.capabilities??[];if(arg===void 0){await runRemovePicker(cwd,config,installed2);return}await removeNamed(cwd,config,installed2,arg)}function deleteCapabilityDir(cwd,paths,target){let dir=safeChildJoin(safeJoin(cwd,capabilitySubtree(paths,target.role)),target.name),existed=existsSync8(dir);return existed&&rmSync3(dir,{recursive:!0,force:!0}),existed}function refuseSymlinkedTargets(cwd,paths,targets){for(let target of targets){let linked;try{linked=findSymlinkComponent(cwd,capabilityRelDir(paths,target))}catch{linked=null}linked!==null&&(logError(`Refusing to remove ${target.name} (${target.role}): ${linked} is a symbolic link, so deleting through it would delete files OUTSIDE the project. Nothing was removed. Delete the directory yourself if that is what you want.`),process.exit(1))}}function capabilityRelDir(paths,capability){return`${capabilitySubtree(paths,capability.role)}/${capability.name}`}async function pruneCapabilityRecords(cwd,config,paths,targets){let{records}=recordsBaseline(readRecords(cwd),{skillsVersion:config.skillsVersion,commit:config.commit});if(records===null)return;let prefixes=targets.map(t=>`${capabilityRelDir(paths,t)}/`),kept={},dropped=0;for(let[key2,hash]of Object.entries(records))prefixes.some(prefix=>key2.startsWith(prefix))?dropped++:kept[key2]=hash;dropped!==0&&await writeRecords(cwd,{skillsVersion:config.skillsVersion,commit:config.commit,files:kept})}function warnIfAutoSelected(targets){let auto=targets.filter(c=>c.source==="auto");if(auto.length===0)return;let names=auto.map(c=>`${c.name} (${c.role})`).join(", ");log7.warn(`${names} ${auto.length===1?"is":"are"} selected automatically for your archetypes, so the next \`pharn update\` will reinstall ${auto.length===1?"it":"them"} (and will name ${auto.length===1?"it":"them"} in its report).`)}async function removeNamed(cwd,config,installed2,address){let parsed=parseCapabilityArg(address);parsed.error&&(logError(parsed.error),process.exit(1));let matches=installed2.filter(c=>c.name===parsed.name&&(parsed.role===void 0||c.role===parsed.role));if(matches.length===0){let valid=installed2.map(c=>`${c.role}:${c.name}`).sort(),hint=valid.length?` Installed: ${valid.join(", ")}.`:" No capabilities are installed.";log7.warn(`"${address}" is not an installed capability.${hint}`),outro3("Nothing was removed.");return}matches.length>1&&(logError(`"${parsed.name}" is ambiguous \u2014 use ${matches.map(m=>`${m.role}:${m.name}`).join(" or ")}.`),process.exit(1));let target=matches[0],paths=layoutPaths(configLayout(config));refuseSymlinkedTargets(cwd,paths,[target]);let note6=await withProjectLock(cwd,"remove",async()=>{assertConfigUnchanged(cwd,config,"remove");let existed=deleteCapabilityDir(cwd,paths,target);return await pruneCapabilityRecords(cwd,config,paths,[target]),await writePharnConfig(cwd,{...config,capabilities:installed2.filter(c=>!(c.name===target.name&&c.role===target.role)),installedAt:new Date().toISOString()}),existed?"":pc4.dim(" (its files were already gone)")});warnIfAutoSelected([target]),outro3(`${pc4.green("\u2714")} Removed ${target.name} (${target.role})${note6}`)}async function runRemovePicker(cwd,config,installed2){if(installed2.length===0){outro3("No capabilities are installed.");return}interactiveAllowed({stdinIsTTY:process.stdin.isTTY,stdoutIsTTY:process.stdout.isTTY})||(logError("Specify a capability to remove (e.g. `pharn remove a11y`), or run `pharn remove` in an interactive terminal to pick from a list."),process.exit(1));let{groups}=buildRemoveSelection(installed2),picked=await groupMultiselect2({message:"Select capabilities to remove",options:groups,required:!1,selectableGroups:!1});isCancel5(picked)&&cancelAndExit();let values=picked;if(values.length===0){outro3("Nothing selected. No capabilities were removed.");return}let targets=[];for(let value of values){let parsed=parseCapabilityArg(value),target=installed2.find(c=>c.name===parsed.name&&c.role===parsed.role);target&&targets.push(target)}let paths=layoutPaths(configLayout(config));refuseSymlinkedTargets(cwd,paths,targets);let ok=await confirm3({message:`Remove ${targets.length} ${plural2(targets.length)}: ${targets.map(t=>`${t.name} (${t.role})`).join(", ")}?`,initialValue:!1});(isCancel5(ok)||ok!==!0)&&cancelAndExit(),await withProjectLock(cwd,"remove",async()=>{assertConfigUnchanged(cwd,config,"remove");for(let target of targets)deleteCapabilityDir(cwd,paths,target);await pruneCapabilityRecords(cwd,config,paths,targets);let removed=new Set(targets.map(t=>`${t.role}:${t.name}`));await writePharnConfig(cwd,{...config,capabilities:installed2.filter(c=>!removed.has(`${c.role}:${c.name}`)),installedAt:new Date().toISOString()})}),warnIfAutoSelected(targets),outro3(`${pc4.green("\u2714")} Removed ${targets.length} ${plural2(targets.length)}.`)}function plural2(n){return n===1?"capability":"capabilities"}import{closeSync as closeSync6,constants as constants3,fstatSync as fstatSync4,openSync as openSync6,readSync as readSync4}from"node:fs";import{dirname as dirname4}from"node:path";var LOCAL_SETTINGS_FILE=".claude/settings.local.json",MAX_SETTINGS_BYTES=256*1024,PROJECT_OPEN=constants3.O_RDONLY|(constants3.O_NONBLOCK??0),UPSTREAM_OPEN=PROJECT_OPEN|(constants3.O_NOFOLLOW??0);function readSettings(base,rel,followLeaf){let linked;try{linked=findSymlinkComponent(base,followLeaf?dirname4(rel):rel)}catch{return{kind:"unreadable",reason:"a path component is not a directory"}}if(linked!==null)return{kind:"unreadable",reason:`${linked} is a symbolic link`};let fd;try{fd=openSync6(safeJoin(base,rel),followLeaf?PROJECT_OPEN:UPSTREAM_OPEN)}catch(err){let code=err.code;return code==="ENOENT"?{kind:"absent"}:code==="ENOTDIR"?{kind:"unreadable",reason:"a path component is not a directory"}:{kind:"unreadable",reason:"it cannot be opened"}}try{let st=fstatSync4(fd);if(!st.isFile())return{kind:"unreadable",reason:"it is not a regular file"};if(st.size>MAX_SETTINGS_BYTES)return{kind:"unreadable",reason:"it is larger than 256 KB"};let buf=Buffer.alloc(st.size),off=0;for(;off<buf.length;){let n=readSync4(fd,buf,off,buf.length-off,off);if(n===0)break;off+=n}try{return{kind:"ok",value:JSON.parse(buf.subarray(0,off).toString("utf8"))}}catch{return{kind:"unreadable",reason:"it is not valid JSON"}}}catch{return{kind:"unreadable",reason:"it cannot be read"}}finally{closeSync6(fd)}}function hookEntries(settings){if(!isPlainObject(settings)||!isPlainObject(settings.hooks))return null;let out=[];for(let[event,groups]of Object.entries(settings.hooks))if(Array.isArray(groups))for(let group of groups){if(!isPlainObject(group)||!Array.isArray(group.hooks))continue;let matcher=typeof group.matcher=="string"?group.matcher:"";for(let hook of group.hooks){if(!isPlainObject(hook)||typeof hook.command!="string")continue;let args=Array.isArray(hook.args)?hook.args.filter(a=>typeof a=="string"):[];out.push({event,matcher,command:hook.command,args})}}return out}function entryKey(e){return JSON.stringify([e.event,e.matcher,e.command,e.args])}function wiredKeys(result){return new Set(result.kind==="ok"?(hookEntries(result.value)??[]).map(entryKey):[])}function diffHookWiring(repoDir,projectRoot){let upstream=readSettings(repoDir,CLAUDE_SETTINGS_FILE,!1),upstreamEntries=upstream.kind==="ok"?hookEntries(upstream.value):null;if(upstreamEntries===null||upstreamEntries.length===0)return{status:"no-upstream",missing:[]};let shared=readSettings(projectRoot,CLAUDE_SETTINGS_FILE,!0),local=readSettings(projectRoot,LOCAL_SETTINGS_FILE,!0);for(let[file,result]of[[CLAUDE_SETTINGS_FILE,shared],[LOCAL_SETTINGS_FILE,local]])if(result.kind==="unreadable")return{status:"unreadable",missing:upstreamEntries,file,reason:result.reason};if(shared.kind==="absent"&&local.kind==="absent")return{status:"project-absent",missing:upstreamEntries};let inShared=wiredKeys(shared),inLocal=wiredKeys(local),missing=upstreamEntries.filter(e=>!inShared.has(entryKey(e))&&!inLocal.has(entryKey(e))),localOnly=upstreamEntries.filter(e=>!inShared.has(entryKey(e))&&inLocal.has(entryKey(e)));return missing.length?{status:"missing",missing}:localOnly.length?{status:"local-only",missing,localOnly}:{status:"match",missing}}function hookWiringFails(diff){return diff.status==="missing"||diff.status==="unreadable"||diff.status==="project-absent"}var ESCAPED=new RegExp(`[\\x00-\\x1f\\x7f-\\x9f\\p{Cf}${String.fromCodePoint(8232,8233)}]`,"gu"),MAX_LINE=300;function escapeUnsafe(text){return text.replace(ESCAPED,ch=>[...Array(ch.length).keys()].map(i=>`\\u${ch.charCodeAt(i).toString(16).padStart(4,"0")}`).join(""))}function displayHook(e){let hook=e.args.length>0?{type:"command",command:e.command,args:e.args}:{type:"command",command:e.command},where=e.matcher?`${e.event} [${e.matcher}]`:e.event,line=`${escapeUnsafe(where)}: ${escapeUnsafe(JSON.stringify(hook))}`;return line.length>MAX_LINE?`${line.slice(0,MAX_LINE)}\u2026`:line}var FILES=`${CLAUDE_SETTINGS_FILE} or ${LOCAL_SETTINGS_FILE}`;function hookWiringLines(diff){if(diff.status==="match"||diff.status==="no-upstream")return null;if(diff.status==="local-only"){let only=diff.localOnly??[];return[` Every hook upstream wires is wired \u2014 ${only.length} of them only in ${LOCAL_SETTINGS_FILE}.`," They run for you, not for teammates or CI:",...only.map(e=>` ${displayHook(e)}`)]}return[diff.status==="project-absent"?` Neither ${FILES} exists. Upstream wires:`:diff.status==="unreadable"?` ${diff.file??CLAUDE_SETTINGS_FILE} could not be read (${diff.reason??"unknown"}). Upstream wires:`:` Upstream wires ${diff.missing.length} hook(s) that neither ${FILES} does:`,...diff.missing.map(e=>` ${displayHook(e)}`),"",` pharn never writes ${CLAUDE_SETTINGS_FILE} after init \u2014 merge these by hand:`," each line is one hook, as JSON, under its event [matcher]. Matching is"," textual: an equivalent hook you wrote differently is listed too."]}import{confirm as confirm4,intro as intro4,isCancel as isCancel6,log as log8,note as note3,outro as outro4,spinner as spinner4}from"@clack/prompts";import pc5 from"picocolors";function key(cap){return`${cap.role}:${cap.name}`}function mergeCapabilities(selection,previous,frozen){let resolved=new Set(selection.selected.map(key)),inIndex=new Set([...selection.selected.map(key),...selection.skipped.map(key)]),previousSource=new Map,seenPrevious=new Set;for(let cap of previous){let k=key(cap);seenPrevious.has(k)||(seenPrevious.add(k),previousSource.set(k,cap.source))}let capabilities=[],changes=[];for(let sel of selection.selected){let k=key(sel);if(!seenPrevious.has(k)){let cap={name:sel.name,role:sel.role,source:"auto"};capabilities.push(cap),changes.push({cap,reason:"added"});continue}let source=previousSource.get(k)==="manual"?"manual":"auto";capabilities.push({name:sel.name,role:sel.role,source})}for(let cap of previous){let k=key(cap);if(resolved.has(k))continue;if(frozen.has(k)){let kept={...cap};capabilities.push(kept),changes.push({cap:kept,reason:"kept-frozen"});continue}if(cap.source==="auto"){changes.push({cap,reason:"dropped-unselected"});continue}let inferred={...cap,source:"manual"};if(!inIndex.has(k)){changes.push({cap:inferred,reason:"dropped-gone"});continue}capabilities.push(inferred),cap.source===void 0&&changes.push({cap:inferred,reason:"kept-manual"})}return{capabilities,changes}}import{copyFileSync as copyFileSync2,lstatSync as lstatSync8,mkdirSync as mkdirSync4}from"node:fs";import{dirname as dirname5}from"node:path";var ApplyError=class extends Error{constructor(message,written){super(message);this.written=written;this.name="ApplyError"}written};function readDiskState(projectRoot,rel){let dest=safeJoin(projectRoot,rel),stat;try{let link=findSymlinkComponent(projectRoot,rel);if(link!==null)return{kind:"unreadable",reason:link===toPosix(rel)?"the path is a symlink":`${link} is a symlink`};stat=lstatSync8(dest,{throwIfNoEntry:!1})}catch{return{kind:"unreadable",reason:"the path could not be inspected"}}if(!stat)return{kind:"absent"};if(!stat.isFile())return{kind:"unreadable",reason:"the path is not a regular file"};try{return{kind:"file",hash:sha256File(dest)}}catch{return{kind:"unreadable",reason:"the file could not be read"}}}function applyWrites(params){let{projectRoot,expected,writes}=params,written=[];for(let rel of writes){let from=expected.get(rel);if(from===void 0)throw new ApplyError(`Internal: no source for planned write ${rel}.`,written);try{let to=safeJoin(projectRoot,rel),link=findSymlinkComponent(projectRoot,rel);if(link!==null)throw new ManifestValidationError(`${link} is a symlink; refusing to write through it.`);mkdirSync4(dirname5(to),{recursive:!0}),copyFileSync2(from,to),written.push(rel)}catch(err){throw new ApplyError(`Failed to write ${rel}: ${err instanceof Error?err.message:String(err)}`,written)}}return written}var LEGACY_MODEL_ALIASES=new Map([["opus-4-8","opus"],["sonnet-5","sonnet"],["fable-5","fable"],["haiku-4-5","haiku"]]),LEGACY_DEFAULTS=[JSON.stringify({default:{model:"sonnet-5",effort:"high"},stages:{plan:{model:"opus-4-8",effort:"max"},review:{model:"opus-4-8",effort:"high"}}}),JSON.stringify({default:{model:"sonnet-5",effort:"high"},stages:{plan:{model:"opus-4-8",effort:"max"},review:{model:"fable-5",effort:"max"}}})];function isLegacyDefault(block){return block!==void 0&&LEGACY_DEFAULTS.includes(JSON.stringify(block))}function convertLegacyModels(block){if(!isPlainObject(block))return{block,changes:[],leftovers:[]};let changes=[],leftovers=[],next=block;if(Object.hasOwn(next,"default")){let{default:moved,...rest}=next,stages2=next.stages;stages2==null?(next={...rest,stages:{default:moved}},changes.push("moved `models.default` into `models.stages.default`")):isPlainObject(stages2)&&!Object.hasOwn(stages2,"default")?(next={...rest,stages:{default:moved,...stages2}},changes.push("moved `models.default` into `models.stages.default`")):leftovers.push(isPlainObject(stages2)?"`models.default` was left where it is: `models.stages.default` is already set, and pharn-oss reads only that one":"`models.default` was left where it is: `models.stages` is not an object, so it cannot move into it")}let stages=next.stages;if(isPlainObject(stages)){let mapped=!1,converted=Object.fromEntries(Object.entries(stages).map(([name,entry])=>{if(!isPlainObject(entry)||typeof entry.model!="string")return[name,entry];let alias=LEGACY_MODEL_ALIASES.get(entry.model);return alias===void 0?[name,entry]:(mapped=!0,changes.push(`stage ${JSON.stringify(name)} model ${JSON.stringify(entry.model)} \u2192 ${JSON.stringify(alias)}`),[name,{...entry,model:alias}])}));mapped&&(next={...next,stages:converted})}return{block:changes.length>0?next:block,changes,leftovers}}function needsModelsConversion(block){return convertLegacyModels(block).changes.length>0}function modelsMigrationPending(block,recorded){return needsModelsConversion(block)&&(recorded===null||modelsRecordHash(block)!==recorded)}function decideModelsUpdate(input){let{current,upstream,recorded,recordsAvailable,force}=input,none={label:null,replacedLegacyDefault:!1,backup:!1,conversion:null,problems:[],upstreamReasons:[]};if(upstream.kind!=="ok"){let kept=keep(current),pharns=current!==void 0&&(isLegacyDefault(current)||recorded!==null&&modelsRecordHash(current)===recorded);return{...none,...kept,outcome:upstream.kind==="absent"?"upstream-absent":"upstream-invalid",nextRecord:pharns?modelsRecordHash(kept.next):recorded,upstreamReasons:upstream.kind==="invalid"?upstream.reasons:[]}}let latestHash=modelsRecordHash(upstream.block),diskHash=current===void 0?null:modelsRecordHash(current),legacyDefault=isLegacyDefault(current),decision=decideFileAction({diskHash,latestHash,recordedHash:legacyDefault?diskHash:recorded,recordsAvailable:legacyDefault||recordsAvailable,force});return decision.action==="write"?{...none,outcome:decision.forced?"forced":decision.label==="restored"?"restored":"updated",label:decision.forced?decision.label:null,replacedLegacyDefault:legacyDefault,next:upstream.block,nextRecord:latestHash,backup:decision.backup}:decision.action==="noop"?{...none,outcome:"ok",next:current,nextRecord:latestHash}:{...none,...keep(current),outcome:"kept",label:decision.label,nextRecord:recorded}}function keep(current){if(current===void 0)return{next:void 0,conversion:null,problems:[]};let conversion=convertLegacyModels(current),check=checkModelsBlock(conversion.block);return{next:conversion.block,conversion:conversion.changes.length>0?conversion:null,problems:[...conversion.leftovers,...check.kind==="invalid"?check.reds.map(red=>red.detail):[]]}}async function runUpdate(opts={}){intro4("pharn update");let cwd=process.cwd(),config=loadArchetypeConfigOrExit(cwd),yes=opts.yes??!1;!yes&&!interactiveAllowed({stdinIsTTY:process.stdin.isTTY,stdoutIsTTY:process.stdout.isTTY})&&(logError("pharn update needs to confirm before it writes. Run it in an interactive terminal, or pass --yes to confirm automatically (e.g. `pharn update --yes`)."),process.exit(1)),await runArchetypeUpdate(config,cwd,opts.force??!1,yes)}async function runArchetypeUpdate(config,cwd,force,yes){let proxyNotice=detectProxyNotice(process.env);proxyNotice&&log8.warn(proxyNoticeMessage(proxyNotice));let s=spinner4();s.start("Checking for updates");let latest;try{latest=await fetchRemoteSkillsVersion(),s.stop(`Latest skills v${latest}`)}catch(err){s.stop("Failed to check for updates"),reportFatal(errorMessage(err),{err}),process.exit(1)}let current=config.skillsVersion===latest,recheckFrozen=(config.frozenCapabilities??[]).length>0,convertModels=modelsMigrationPending(config.models,recordsBaseline(readRecords(cwd),{skillsVersion:config.skillsVersion,commit:config.commit}).records?.[MODELS_RECORD_KEY]??null);if(current&&!force&&!recheckFrozen&&!convertModels){outro4(`Already up to date (skills v${config.skillsVersion}).`);return}if(note3([current?row("Skills version",`v${config.skillsVersion} (re-applying)`):row("Skills version",`v${config.skillsVersion} \u2192 v${latest}`),"",row("Archetypes",(config.archetypes??[]).join(", ")||"(none)"),"",force?pc5.dim(` --force: files you changed will be OVERWRITTEN (each is copied to ${BACKUP_DIR}/ first).`):pc5.dim(" Files you have changed are kept, not overwritten \u2014 they are listed at the end."),...convertModels?[pc5.dim(" Your models block is in the format pharn wrote before 0.7.0 \u2014 this run converts it.")]:[],pc5.dim(" Re-resolves your archetypes against the latest capabilities and re-copies them."),pc5.dim(" https://github.com/pharn-dev/pharn-oss/blob/main/CHANGELOG.md")].join(`
22
+ `)),!yes){let ok=await confirm4({message:force?"Re-fetch capabilities and overwrite your changes?":"Re-fetch capabilities at the latest version?",initialValue:!0});(isCancel6(ok)||ok!==!0)&&cancelAndExit()}let outcome=null,backupRef={current:null},spinnerRef={current:null},refusalRef={current:null},failure=null;try{outcome=await withProjectLock(cwd,"update",async()=>{assertConfigUnchanged(cwd,config,"update");let s2=spinner4();spinnerRef.current=s2,s2.start(`Updating from ${REPO_URL}`);let repo=await fetchRepo();try{let gate=minCliGate(repo.dir,PHARN_VERSION);if(gate.warning&&log8.warn(gate.warning),gate.refusal)return s2.stop("Update refused"),refusalRef.current=gate.refusal,null;let applied=await applyUpdate(repo.dir,repo.sha,config,cwd,force,backup=>{backupRef.current=backup,s2.stop("Backup written"),printBackupNotice(backup,{aborted:!1});let s3=spinner4();spinnerRef.current=s3,s3.start("Writing files")});return spinnerRef.current.stop(applied.plan.writes.length?"Capabilities updated":"Nothing to write"),{...applied,hookWiring:diffHookWiring(repo.dir,cwd)}}finally{repo.cleanup()}})}catch(err){err instanceof ProjectLockedError?(spinnerRef.current?.stop("Update refused"),refusalRef.current=err.message):(spinnerRef.current?.stop("Update failed"),failure={err})}let refusal2=refusalRef.current;refusal2&&(reportFatal(refusal2),process.exit(1)),(failure||!outcome)&&(failure?reportFatal(errorMessage(failure.err),failure):reportFatal("Update failed."),backupRef.current&&printBackupNotice(backupRef.current,{aborted:!0}),process.exit(1)),reportOutcome(outcome,force)}async function applyUpdate(repoDir,sha,config,cwd,force,onBackup){let index=parseCapabilityIndex(repoDir),unknownWarning=unknownCapabilitiesWarning(index.unknown);unknownWarning&&log8.warn(unknownWarning);let frozen=new Set(index.unknown.map(u=>`${u.role}:${u.name}`)),selection=resolveCapabilities(config.archetypes??[],index),merged=mergeCapabilities(selection,config.capabilities??[],frozen),configCapabilities=merged.capabilities,manifestCapabilities=configCapabilities.filter(cap=>!frozen.has(`${cap.role}:${cap.name}`)),installedVersion=readSkillsVersion(repoDir),layout=detectLayout(repoDir),previousLayout=configLayout(config),expected=collectExpectedInstallPaths({repoDir,capabilities:manifestCapabilities,layout}),latestHashes=new Map;for(let[rel,source]of expected)latestHashes.set(rel,sha256File(source));let diskStates=new Map;for(let rel of expected.keys())diskStates.set(rel,readDiskState(cwd,rel));let{records,note:recordsNote}=recordsBaseline(readRecords(cwd),{skillsVersion:config.skillsVersion,commit:config.commit}),plan=planUpdate({latestHashes,diskStates,records,force}),models=decideModelsUpdate({current:config.models,upstream:readUpstreamModels(repoDir),recorded:records?.[MODELS_RECORD_KEY]??null,recordsAvailable:records!==null,force}),backupRels=models.backup?[...plan.backups,CONFIG_FILENAME]:plan.backups,backup=backupRels.length>0?{dir:createBackup(cwd,backupRels),count:backupRels.length}:null;backup&&onBackup(backup);let versionWithheld=plan.counts.skipped>0,nextSkillsVersion=versionWithheld?config.skillsVersion:installedVersion,nextCommit=versionWithheld?config.commit:sha,pendingSkillsVersion=versionWithheld&&installedVersion!==nextSkillsVersion&&plan.skipped.every(g=>USER_EDIT_SKIPS.has(g.label))?installedVersion:void 0,written;try{written=applyWrites({projectRoot:cwd,expected,writes:plan.writes})}catch(err){throw err instanceof ApplyError&&err.written.length>0&&records!==null&&await writeRecords(cwd,{skillsVersion:config.skillsVersion,commit:config.commit,files:{...records,...buildRecords(cwd,err.written)}}),err}let frozenRecords=records===null?{}:recordsUnderCapabilities(records,layoutPaths(layout),configCapabilities.filter(cap=>frozen.has(`${cap.role}:${cap.name}`)));await writeRecords(cwd,{skillsVersion:nextSkillsVersion,commit:nextCommit,files:{...frozenRecords,...plan.nextRecords,...buildRecords(cwd,written),...models.nextRecord!==null?{[MODELS_RECORD_KEY]:models.nextRecord}:{}}});let previouslyFrozen=new Set(config.frozenCapabilities??[]),skippedRels=plan.skipped.flatMap(group=>group.rels),paths=layoutPaths(layout),hasSkippedFile=cap=>{let prefix=`${capabilitySubtree(paths,cap.role)}/${cap.name}/`;return skippedRels.some(rel=>rel.startsWith(prefix))},frozenCapabilities=[...new Set(configCapabilities.filter(cap=>{let key2=`${cap.role}:${cap.name}`;return frozen.has(key2)||previouslyFrozen.has(key2)&&hasSkippedFile(cap)}).map(cap=>`${cap.role}:${cap.name}`))].sort(),{pendingSkillsVersion:_previousPending,frozenCapabilities:_previousFrozen,...rest}=config;return await writePharnConfig(cwd,{...rest,skillsVersion:nextSkillsVersion,commit:nextCommit,...pendingSkillsVersion!==void 0?{pendingSkillsVersion}:{},...frozenCapabilities.length>0?{frozenCapabilities}:{},capabilities:configCapabilities,layout,installedAt:new Date().toISOString(),models:models.next}),{installedVersion,recordedVersion:nextSkillsVersion,capCount:configCapabilities.length,capabilityChanges:merged.changes,plan,backup,recordsNote,versionWithheld,abandonedLayout:previousLayout!==layout&&written.length>0?previousLayout:null,featuresReadmeRelocation:records!==null&&FEATURES_README in records&&resolveFeaturesReadme(repoDir,layout)!==FEATURES_README,models,modelsFloor:layoutPaths(layout).floor}}var USER_EDIT_SKIPS=new Set(["modified","unrecorded"]),FORCEABLE_SKIPS=new Set(["modified","unrecorded","unverifiable"]);function reportOutcome(outcome,force){let{plan,recordsNote,versionWithheld}=outcome,{counts}=plan;recordsNote&&log8.warn(`\u26A0 ${recordsNote}`),reportCapabilityChanges(outcome.capabilityChanges),reportModels(outcome.models,outcome.modelsFloor,outcome.backup);let hookLines=outcome.hookWiring?hookWiringLines(outcome.hookWiring):null;hookLines&&note3(hookLines.join(`
22
23
  `),"HOOKS");let forceable=plan.skipped.some(g=>FORCEABLE_SKIPS.has(g.label)),hasUnreadable=plan.skipped.some(g=>!FORCEABLE_SKIPS.has(g.label));if(plan.skipped.length>0){let lines=[];for(let group of plan.skipped){lines.push(` ${skipHeading(group.label)}`);for(let rel of group.rels)lines.push(` ${rel}`);lines.push("")}forceable&&lines.push(pc5.dim(` Re-run with --force to overwrite (skipped files are backed up to ${BACKUP_DIR}/ first).`)),hasUnreadable&&lines.push(pc5.dim(" --force cannot clear the UNREADABLE paths above."),pc5.dim(" Inspect each path by hand \u2014 a directory, a symlink, or an"),pc5.dim(" unreadable file sits where pharn expects a regular file.")),note3(lines.join(`
23
24
  `),"SKIPPED")}outcome.abandonedLayout==="flat"?log8.warn("Your install moved to the pharn/ layout. The old top-level copies are left behind and are no longer managed by pharn \u2014 delete them by hand."):outcome.abandonedLayout==="pharn"&&log8.warn("Your install moved to the flat layout. The old pharn/ tree (contracts, floor scripts, docs, and capabilities) is left behind and is no longer managed by pharn \u2014 delete it by hand."),outcome.featuresReadmeRelocation&&log8.warn("Your install now keeps the product-loop boundary contract at pharn/features/README.md. A copy at the project root (features/README.md) is left behind and is no longer managed by pharn \u2014 delete it by hand."),versionWithheld&&log8.warn(`${counts.skipped} file(s) were skipped, so your install is still recorded as skills v${outcome.recordedVersion} (upstream is v${outcome.installedVersion}) \u2014 resolve them${forceable?", or re-run with --force,":""} to finish the upgrade.`);let summary=`updated ${counts.updated} \xB7 restored ${counts.restored} \xB7 unchanged ${counts.ok}`+(force?` \xB7 forced ${counts.forced}`:"")+` \xB7 skipped ${counts.skipped}`;outro4(`${pc5.green("\u2714")} ${summary} ${pc5.dim(`(${outcome.capCount} capabilit${outcome.capCount===1?"y":"ies"}, skills v${outcome.recordedVersion})`)}`)}function printBackupNotice(backup,opts){let output=opts.aborted?process.stderr:process.stdout;opts.aborted&&log8.warn("The update stopped part-way \u2014 some originals may already have been overwritten.",{output}),log8.info(`Backed up ${backup.count} file(s) to ${backup.dir} before overwriting.`,{output}),opts.aborted||log8.info(`${BACKUP_DIR}/ is not gitignored \u2014 add it to .gitignore or delete it once you are happy.`,{output})}var CHANGE_ORDER=[{reason:"added",heading:"ADDED \u2014 newly selected for your archetypes"},{reason:"dropped-unselected",heading:"REMOVED \u2014 no longer selected for your archetypes"},{reason:"dropped-gone",heading:"REMOVED \u2014 no longer exists upstream (was a manual add)"},{reason:"kept-manual",heading:"KEPT \u2014 your manual add, not selected by your archetypes"},{reason:"kept-frozen",heading:"KEPT \u2014 pharn could not read these upstream this run, so they were left exactly as they are"}];function reportCapabilityChanges(changes){if(changes.length===0)return;let lines=[];for(let{reason,heading}of CHANGE_ORDER){let inGroup=changes.filter(c=>c.reason===reason);if(inGroup.length!==0){lines.push(` ${heading}`);for(let{cap}of inGroup)lines.push(` ${cap.role}:${cap.name}`);lines.push("")}}changes.some(c=>c.reason==="dropped-unselected"||c.reason==="dropped-gone")&&lines.push(pc5.dim(" Removed capabilities\u2019 files are left on disk \u2014 pharn update never deletes.")),note3(lines.join(`
24
- `),"CAPABILITIES")}function skipHeading(label){switch(label){case"modified":return"MODIFIED \u2014 you changed these since pharn wrote them";case"unrecorded":return"UNRECORDED \u2014 pharn has no record of writing these";case"unverifiable":return`UNVERIFIABLE \u2014 no usable ${RECORDS_FILE} to compare against`;default:return"UNREADABLE \u2014 not a regular readable file"}}import{intro as intro5,note as note4,outro as outro5}from"@clack/prompts";import pc6 from"picocolors";async function runList(opts={}){let json=opts.json??!1,cwd=process.cwd();json||intro5("pharn list");let config;try{config=readPharnConfig(cwd)}catch(e){throw isConfigValidationError(e)&&(emitError(e.message,json),process.exit(1)),e}config||(emitError("No pharn.config.json found. Run `pharn init` first.",json),process.exit(1)),isArchetypeConfig(config)||(emitError(LEGACY_CONFIG_MESSAGE,json),process.exit(1));let inventory=buildArchetypeInventory(config);if(json){console.log(JSON.stringify(inventory,null,2));return}renderArchetypeHuman(inventory)}function buildArchetypeInventory(config){return{mode:"archetype",skillsVersion:config.skillsVersion,archetypes:config.archetypes??[],capabilities:(config.capabilities??[]).map(c=>({name:c.name,role:c.role,...c.source!==void 0?{source:c.source}:{}}))}}function renderArchetypeHuman(inv){let lines=renderCapabilityLines(inv);note4(lines.join(`
25
- `),"INSTALLED (archetype)"),outro5(pc6.dim("Read-only \u2014 nothing changed."))}function emitError(message,json){json?console.error(message):logError(message)}import{intro as intro6,log as log9,note as note5,outro as outro6,spinner as spinner5}from"@clack/prompts";import pc7 from"picocolors";function diffInstalledCapabilities(params){let{repoDir,projectRoot,capabilities,layout}=params,expected=collectExpectedInstallPaths({repoDir,capabilities,layout});return compareExpected(expected,projectRoot)}function compareExpected(expected,baseDir){let modified=[],missing=[],unreadable=[],okCount=0;for(let[rel,repoPath]of expected){let state=readDiskState(baseDir,rel);state.kind==="absent"?missing.push(rel):state.kind==="unreadable"?unreadable.push({rel,reason:state.reason}):state.hash===sha256File(repoPath)?okCount+=1:modified.push(rel)}return modified.sort(),missing.sort(),unreadable.sort((a,b)=>+(a.rel>b.rel)-+(a.rel<b.rel)),{modified,missing,unreadable,okCount}}var REF=`${REPO}@${REPO_BRANCH}`;async function runStatus(opts={}){let strict=opts.strict??!1,drift=opts.drift??!0;intro6("pharn status");let cwd=process.cwd(),config=loadArchetypeConfigOrExit(cwd);await runArchetypeStatus(config,{strict,drift},cwd)}async function runArchetypeStatus(config,opts,cwd){let{strict,drift}=opts,proxyNotice=detectProxyNotice(process.env);if(proxyNotice&&log9.warn(proxyNoticeMessage(proxyNotice)),!drift){let s2=spinner5();s2.start("Checking for updates");let latest;try{latest=await fetchRemoteSkillsVersion(),s2.stop(`Latest skills v${latest}`)}catch(err){s2.stop("Failed to check for updates"),reportFatal(errorMessage(err),{err}),process.exit(1)}let outdated=printArchetypeVersion(config,latest);printModelRouting(config),strict&&outdated&&process.exit(1),outro6(pc7.dim("Read-only \u2014 nothing changed (drift check skipped)."));return}let s=spinner5();s.start(`Comparing against ${REF}`);let repo;try{repo=await fetchRepo(),s.stop(`Compared against ${REF}`)}catch(err){s.stop(`Failed to reach ${REPO}`),reportFatal(errorMessage(err),{err}),process.exit(1)}let exitCode=0;try{let outdated=printArchetypeVersion(config,readSkillsVersion(repo.dir));printModelRouting(config);let index=parseCapabilityIndex(repo.dir),unknownWarning=unknownCapabilitiesWarning(index.unknown);unknownWarning&&log9.warn(unknownWarning);let frozen=new Set(index.unknown.map(u=>`${u.role}:${u.name}`)),result=diffInstalledCapabilities({repoDir:repo.dir,projectRoot:cwd,capabilities:(config.capabilities??[]).filter(cap=>!frozen.has(`${cap.role}:${cap.name}`)),layout:configLayout(config)});printDriftSection(result);let hooks=diffHookWiring(repo.dir,cwd),hookLines=hookWiringLines(hooks);hookLines&&note5(hookLines.join(`
25
+ `),"CAPABILITIES")}function reportModels(models,floor,backup){let lines=modelsReportLines(models,backup);lines.length!==0&&(models.next!==void 0&&lines.push("",...modelsLabelLines(floor).map(line=>pc5.dim(line))),note3(lines.join(`
26
+ `),"MODELS"))}function modelsReportLines(models,backup){let quoted=text=>` ${terminalSafe(text,{max:300})}`,lines=[];switch(models.outcome){case"ok":return[];case"restored":lines.push("Your config had no models block \u2014 wrote pharn-oss's.");break;case"updated":lines.push(...models.replacedLegacyDefault?["Replaced the models block an earlier pharn wrote with pharn-oss's.","Its model ids, such as opus-4-8, are rejected by Claude Code","and by pharn-oss's checker."]:["Moved your models block to pharn-oss's current one: you had","not changed it since pharn wrote it."]);break;case"forced":lines.push("Replaced your models block with pharn-oss's (--force).",`Your previous ${CONFIG_FILENAME} is in ${backup?.dir??`${BACKUP_DIR}/`}.`);break;case"kept":lines.push(keptModelsHeading(models.label));break;case"upstream-invalid":lines.push("pharn-oss's models block was not applied; this pharn rejects it:",...models.upstreamReasons.map(quoted),"If pharn-oss changed its models format, upgrade pharn","(npm i -g @pharn-dev/pharn@latest) and run `pharn update` again.");break;case"upstream-absent":if(models.conversion===null&&models.problems.length===0)return[];lines.push("pharn-oss ships no models block, so yours is kept.");break}return models.conversion!==null&&lines.push("","Converted your models block from the format pharn wrote before 0.7.0,","keeping your values:",...models.conversion.changes.map(quoted)),models.problems.length>0&&lines.push("","pharn-oss's rules still reject your models block \u2014 left as is,","fix it by hand:",...models.problems.map(quoted)),models.outcome==="kept"&&lines.push("",pc5.dim("--force replaces it with pharn-oss's, after copying"),pc5.dim(`${CONFIG_FILENAME} to ${BACKUP_DIR}/.`)),lines}function keptModelsHeading(label){switch(label){case"modified":return"Kept your models block \u2014 you changed it since pharn wrote it.";case"unrecorded":return"Kept your models block \u2014 pharn has no record of writing it.";default:return`Kept your models block \u2014 no usable ${RECORDS_FILE} to check it.`}}function skipHeading(label){switch(label){case"modified":return"MODIFIED \u2014 you changed these since pharn wrote them";case"unrecorded":return"UNRECORDED \u2014 pharn has no record of writing these";case"unverifiable":return`UNVERIFIABLE \u2014 no usable ${RECORDS_FILE} to compare against`;default:return"UNREADABLE \u2014 not a regular readable file"}}import{intro as intro5,note as note4,outro as outro5}from"@clack/prompts";import pc6 from"picocolors";async function runList(opts={}){let json=opts.json??!1,cwd=process.cwd();json||intro5("pharn list");let config;try{config=readPharnConfig(cwd)}catch(e){throw isConfigValidationError(e)&&(emitError(e.message,json),process.exit(1)),e}config||(emitError("No pharn.config.json found. Run `pharn init` first.",json),process.exit(1)),isArchetypeConfig(config)||(emitError(LEGACY_CONFIG_MESSAGE,json),process.exit(1));let inventory=buildArchetypeInventory(config);if(json){console.log(JSON.stringify(inventory,null,2));return}renderArchetypeHuman(inventory)}function buildArchetypeInventory(config){return{mode:"archetype",skillsVersion:config.skillsVersion,archetypes:config.archetypes??[],capabilities:(config.capabilities??[]).map(c=>({name:c.name,role:c.role,...c.source!==void 0?{source:c.source}:{}}))}}function renderArchetypeHuman(inv){let lines=renderCapabilityLines(inv);note4(lines.join(`
27
+ `),"INSTALLED (archetype)"),outro5(pc6.dim("Read-only \u2014 nothing changed."))}function emitError(message,json){json?console.error(message):logError(message)}import{intro as intro6,log as log9,note as note5,outro as outro6,spinner as spinner5}from"@clack/prompts";import pc7 from"picocolors";function diffInstalledCapabilities(params){let{repoDir,projectRoot,capabilities,layout}=params,expected=collectExpectedInstallPaths({repoDir,capabilities,layout});return compareExpected(expected,projectRoot)}function compareExpected(expected,baseDir){let modified=[],missing=[],unreadable=[],okCount=0;for(let[rel,repoPath]of expected){let state=readDiskState(baseDir,rel);state.kind==="absent"?missing.push(rel):state.kind==="unreadable"?unreadable.push({rel,reason:state.reason}):state.hash===sha256File(repoPath)?okCount+=1:modified.push(rel)}return modified.sort(),missing.sort(),unreadable.sort((a,b)=>+(a.rel>b.rel)-+(a.rel<b.rel)),{modified,missing,unreadable,okCount}}var REF=`${REPO}@${REPO_BRANCH}`;async function runStatus(opts={}){let strict=opts.strict??!1,drift=opts.drift??!0;intro6("pharn status");let cwd=process.cwd(),config=loadArchetypeConfigOrExit(cwd);await runArchetypeStatus(config,{strict,drift},cwd)}async function runArchetypeStatus(config,opts,cwd){let{strict,drift}=opts,proxyNotice=detectProxyNotice(process.env);if(proxyNotice&&log9.warn(proxyNoticeMessage(proxyNotice)),!drift){let s2=spinner5();s2.start("Checking for updates");let latest;try{latest=await fetchRemoteSkillsVersion(),s2.stop(`Latest skills v${latest}`)}catch(err){s2.stop("Failed to check for updates"),reportFatal(errorMessage(err),{err}),process.exit(1)}let outdated=printArchetypeVersion(config,latest);printModels(config),strict&&outdated&&process.exit(1),outro6(pc7.dim("Read-only \u2014 nothing changed (drift check skipped)."));return}let s=spinner5();s.start(`Comparing against ${REF}`);let repo;try{repo=await fetchRepo(),s.stop(`Compared against ${REF}`)}catch(err){s.stop(`Failed to reach ${REPO}`),reportFatal(errorMessage(err),{err}),process.exit(1)}let exitCode=0;try{let outdated=printArchetypeVersion(config,readSkillsVersion(repo.dir));printModels(config);let index=parseCapabilityIndex(repo.dir),unknownWarning=unknownCapabilitiesWarning(index.unknown);unknownWarning&&log9.warn(unknownWarning);let frozen=new Set(index.unknown.map(u=>`${u.role}:${u.name}`)),result=diffInstalledCapabilities({repoDir:repo.dir,projectRoot:cwd,capabilities:(config.capabilities??[]).filter(cap=>!frozen.has(`${cap.role}:${cap.name}`)),layout:configLayout(config)});printDriftSection(result);let hooks=diffHookWiring(repo.dir,cwd),hookLines=hookWiringLines(hooks);hookLines&&note5(hookLines.join(`
26
28
  `),"HOOKS"),strict&&(outdated||result.modified.length||result.missing.length||result.unreadable.length||hookWiringFails(hooks))&&(exitCode=1)}catch(err){reportFatal(errorMessage(err),{err}),exitCode=1}finally{repo.cleanup()}exitCode&&process.exit(exitCode),outro6(pc7.dim("Read-only \u2014 nothing changed."))}function printArchetypeVersion(config,latest){let outdated=config.skillsVersion!==latest,skillsLine=outdated?`${row("Skills version",`v${config.skillsVersion} \u2192 v${latest}`)} ${pc7.dim("(update available, run `pharn update`)")}`:`${row("Skills version",`v${config.skillsVersion}`)} ${pc7.dim("(up to date)")}`;return note5([skillsLine,"",row("Archetypes",(config.archetypes??[]).join(", ")||"(none)"),row("Capabilities",String((config.capabilities??[]).length))].join(`
27
- `),"VERSION"),outdated}function printModelRouting(config){config.models!==void 0&&note5([...formatModelRoutingLines(config.models),"",pc7.dim("Recorded only \u2014 no installed stage reads this yet.")].join(`
28
- `),"MODELS")}function printDriftSection(result){if(result.modified.length===0&&result.missing.length===0&&result.unreadable.length===0){note5(`No drift \u2014 ${result.okCount} file(s) match ${REF}.`,"DRIFT");return}let lines=[];if(result.modified.length){lines.push(` DIFFERS FROM ${REF} (PHARN-owned)`);for(let p of result.modified)lines.push(` ${p}`);lines.push(pc7.dim(" `pharn update` keeps files you've edited and cleanly"),pc7.dim(" upgrades the rest; `--force` overwrites edits too"),pc7.dim(" (backed up to .pharn-backup/ first)."))}if(result.missing.length){lines.length&&lines.push(""),lines.push(" MISSING (expected but absent)");for(let p of result.missing)lines.push(` ${p}`);lines.push(pc7.dim(" For recorded capabilities: `pharn update` when a newer skills version is"),pc7.dim(" available, or `pharn update --force` at the current version (a plain"),pc7.dim(" `pharn update` exits early when already up to date)."),pc7.dim(" To add a capability not yet in pharn.config.json, use `pharn add`"),pc7.dim(" (additive-only \u2014 already-listed capabilities are a no-op)."))}if(result.unreadable.length){lines.length&&lines.push(""),lines.push(" UNREADABLE \u2014 not a regular readable file");for(let{rel,reason}of result.unreadable)lines.push(` ${rel} \u2014 ${reason}`);lines.push(pc7.dim(" pharn cannot compare these, and `pharn update` skips them too."),pc7.dim(" Inspect each path by hand \u2014 a directory, a symlink, or an"),pc7.dim(" unreadable file sits where pharn expects a regular file."))}lines.push("",pc7.dim(` ${result.okCount} file(s) match ${REF}.`)),note5(lines.join(`
29
+ `),"VERSION"),outdated}function printModels(config){if(config.models===void 0)return;let floor=layoutPaths(configLayout(config)).floor;note5(modelsNoteLines(config.models,floor).join(`
30
+ `),"MODELS")}function modelsNoteLines(block,floor){if(needsModelsConversion(block))return["In the format pharn wrote before 0.7.0: a top-level `default` and","model ids such as `opus-4-8`, which Claude Code and pharn-oss's","checker reject.",pc7.dim("`pharn update` converts it: a block pharn wrote and you never"),pc7.dim("changed becomes pharn-oss's; an edited one keeps your values.")];let check=checkModelsBlock(block);return check.kind==="invalid"?["pharn-oss's rules reject this block:",...check.reds.map(red=>` ${terminalSafe(red.detail,{max:300})}`),pc7.dim("Fix it by hand. The checker installed with PHARN says the same:"),pc7.dim(modelsCheckerCommand(floor,"validate"))]:check.kind==="no-stages"?["Declares no stages: pharn-oss's checker reads that as none declared."]:[...resolvedStageLines(check.stages),"",...modelsLabelLines(floor).map(line=>pc7.dim(line))]}function printDriftSection(result){if(result.modified.length===0&&result.missing.length===0&&result.unreadable.length===0){note5(`No drift \u2014 ${result.okCount} file(s) match ${REF}.`,"DRIFT");return}let lines=[];if(result.modified.length){lines.push(` DIFFERS FROM ${REF} (PHARN-owned)`);for(let p of result.modified)lines.push(` ${p}`);lines.push(pc7.dim(" `pharn update` keeps files you've edited and cleanly"),pc7.dim(" upgrades the rest; `--force` overwrites edits too"),pc7.dim(" (backed up to .pharn-backup/ first)."))}if(result.missing.length){lines.length&&lines.push(""),lines.push(" MISSING (expected but absent)");for(let p of result.missing)lines.push(` ${p}`);lines.push(pc7.dim(" For recorded capabilities: `pharn update` when a newer skills version is"),pc7.dim(" available, or `pharn update --force` at the current version (a plain"),pc7.dim(" `pharn update` exits early when already up to date)."),pc7.dim(" To add a capability not yet in pharn.config.json, use `pharn add`"),pc7.dim(" (additive-only \u2014 already-listed capabilities are a no-op)."))}if(result.unreadable.length){lines.length&&lines.push(""),lines.push(" UNREADABLE \u2014 not a regular readable file");for(let{rel,reason}of result.unreadable)lines.push(` ${rel} \u2014 ${reason}`);lines.push(pc7.dim(" pharn cannot compare these, and `pharn update` skips them too."),pc7.dim(" Inspect each path by hand \u2014 a directory, a symlink, or an"),pc7.dim(" unreadable file sits where pharn expects a regular file."))}lines.push("",pc7.dim(` ${result.okCount} file(s) match ${REF}.`)),note5(lines.join(`
29
31
  `),"DRIFT")}var USAGE=`Pharn - Installs PHARN (an audit-grade methodology for Claude Code) into your project. npx @pharn-dev/pharn init detects your project's archetype and installs the applicable PHARN capabilities; pharn add installs another capability later; pharn update bumps to the latest skills version.
30
32
 
31
33
  Usage:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pharn-dev/pharn",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Audit-grade AI development methodology for Claude Code — spec, plan, grill, build, regress, verify, ship.",
5
5
  "keywords": [
6
6
  "claude-code",