@pharn-dev/pharn 0.7.1 → 0.9.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 (3) hide show
  1. package/README.md +10 -9
  2. package/dist/index.js +19 -16
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -17,7 +17,7 @@ chat window.
17
17
  [![CI](https://github.com/pharn-dev/pharn-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/pharn-dev/pharn-cli/actions/workflows/ci.yml)
18
18
  [![CodeQL](https://github.com/pharn-dev/pharn-cli/actions/workflows/codeql.yml/badge.svg)](https://github.com/pharn-dev/pharn-cli/actions/workflows/codeql.yml)
19
19
  [![License: Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-green)](./LICENSE)
20
- [![Node](https://img.shields.io/badge/node-%3E%3D20.13.0-brightgreen)](./package.json)
20
+ [![Node](https://img.shields.io/badge/node-%3E%3D24.2.0-brightgreen)](./package.json)
21
21
 
22
22
  ```bash
23
23
  npx @pharn-dev/pharn@latest init
@@ -134,7 +134,7 @@ catalog.
134
134
  | Step | What happens |
135
135
  | ---- | ------------ |
136
136
  | Detect | Reads `package.json` dependency names and performs a bounded, symlink-safe file-tree scan to classify the project as `ssr`, `backend`, `spa`, `lib`, or a combination. |
137
- | Fetch | Resolves `pharn-dev/pharn-oss@main` to a commit SHA, downloads the GitHub codeload tarball, and extracts it into a temp directory. |
137
+ | Fetch | Resolves the tip of `pharn-dev/pharn-oss` `main` to its commit, downloads that commit's GitHub codeload tarball, and extracts it into a temp directory. `--ref latest` installs the newest **verified release** instead (a tag pharn-oss creates only after its post-merge CI passed), checking its `SKILLS_VERSION` matches the tag. |
138
138
  | Resolve | Selects capabilities whose `applies` value is `universal` or intersects your detected archetypes. Skipped capabilities are named with the reason. |
139
139
  | Install | Copies the selected capabilities plus fixed PHARN product surfaces into your repo, mirroring the upstream layout. |
140
140
  | Record | Writes `pharn.config.json` and `pharn.records.json` so future updates can tell PHARN-owned files from local edits. |
@@ -326,13 +326,14 @@ PHARN is intentionally scoped:
326
326
 
327
327
  - It targets **Claude Code today**. Codex and Cursor support are planned, not
328
328
  shipped.
329
- - It requires a git-initialized project. The **CLI** (`@pharn-dev/pharn`) needs
330
- Node **>= 20.13.0** (`engines.node`; CI runs on Node 24, and a smoke job
331
- starts the packed CLI on exactly 20.13.0). The **installed floor**
332
- (`pharn/floor/*.mjs`) needs **Node 24.2+** for reliable checker runs
333
- (`import.meta.main`; on older Node a guarded tool can exit 0 without running
334
- its checks). Use 24.2+ locally for `/pharn-build`, `/pharn-verify`, and the
335
- rest of the pipeline.
329
+ - It requires a git-initialized project and **Node >= 24.2.0**, for the CLI
330
+ (`@pharn-dev/pharn`, `engines.node`) and for the **installed floor**
331
+ (`pharn/floor/*.mjs`) alike. The floor's checkers gate on `import.meta.main`;
332
+ on an older Node a guarded tool used to exit 0 without running its checks, so
333
+ they now refuse to run there (one stderr line, exit 2). `pharn init` and
334
+ `pharn update` therefore refuse up front on an older Node, with nothing
335
+ written, and `pharn status` prints the floor beside the Node you are running.
336
+ CI runs on Node 24, and a smoke job starts the packed CLI on exactly 24.2.0.
336
337
  - **Archetype detection is JS/TS-shaped.** The signals are `package.json`
337
338
  dependency names plus `next.config.*`, `app/` route handlers, `.tsx`/`.jsx`,
338
339
  `migrations/` and `.sql`. A Python, Go or Rust repo produces no signal,
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import{realpathSync}from"node:fs";import{fileURLToPath}from"node:url";import minimist from"minimist";import{intro,log as log5,note as note2,spinner as spinner2}from"@clack/prompts";import pc from"picocolors";import{createRequire}from"node:module";var require2=createRequire(import.meta.url),PHARN_VERSION=require2("../package.json").version;var LOGO=["\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557","\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551","\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551","\u2588\u2588\u2554\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551","\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551","\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D"];function showBanner(){console.log();for(let line of LOGO)console.log(` ${pc.cyan(pc.bold(line))}`);console.log(),console.log(` ${pc.dim(`Audit-grade methodology for Claude Code \xB7 v${PHARN_VERSION}`)}`),console.log()}import{confirm,isCancel,log}from"@clack/prompts";function cancelAndExit(message="Cancelled. Nothing was changed."){log.info(message),process.exit(0)}import{log as log2}from"@clack/prompts";var UNSAFE_RE=/[\x00-\x1f\x7f-\x9f\p{Cf}]/gu,UNSAFE_KEEP_WS_RE=/[\x00-\x08\x0b-\x1f\x7f-\x9f\p{Cf}]/gu;function terminalSafe(value,options={}){let stripped=value.replace(options.keepNewlines?UNSAFE_KEEP_WS_RE:UNSAFE_RE,""),{max}=options;return max!==void 0&&stripped.length>max?`${stripped.slice(0,max)}\u2026`:stripped}function hasUnsafeChars(value){return/[\x00-\x1f\x7f-\x9f\p{Cf}]/u.test(value)}var PHARN_DEBUG_HINT="Re-run with PHARN_DEBUG=1 for full error output.";function errorMessage(err){if(err instanceof Error)return err.message;try{return String(err)}catch{return"an unstringifiable value was thrown"}}function logError(message){log2.error(terminalSafe(message,{keepNewlines:!0}),{output:process.stderr})}function reportFatal(message,cause){if(logError(`\u26A0 ${message}`),cause!==void 0){if(process.env.PHARN_DEBUG){console.error(cause.err);return}log2.info(PHARN_DEBUG_HINT,{output:process.stderr})}}var REPO="pharn-dev/pharn-oss",REPO_BRANCH="main",REPO_URL="github.com/pharn-dev/pharn-oss",FIRST_FEATURE_COMMAND="/pharn-spec",GRILLERS_DIR="pharn-pipeline/grillers",LENSES_DIR="pharn-review",SKILLS_VERSION_FILE="SKILLS_VERSION",MIN_CLI_FILE="MIN_CLI",UPSTREAM_LICENSE="LICENSE",FLAT_LICENSE_DEST="PHARN-LICENSE",PHARN_LICENSE_DEST="pharn/LICENSE",TRUSTED_DOCS=["CONSTITUTION.md","ARCHITECTURE.md","THREAT-MODEL.md","LIMITS.md"],CONTRACTS_DIR="pharn-contracts",CORE_DIR="pharn-core",FLOOR_DIR=".dev/floor",FLOOR_TEST_FIXTURES_DIR="test-fixtures",PHARN_GRILLERS_DIR="pharn/pharn-pipeline/grillers",PHARN_LENSES_DIR="pharn/pharn-review",PHARN_CONTRACTS_DIR="pharn/pharn-contracts",PHARN_CORE_DIR="pharn/pharn-core",PHARN_FLOOR_DIR="pharn/floor",PHARN_TRUSTED_DOCS=["pharn/CONSTITUTION.md","pharn/ARCHITECTURE.md","THREAT-MODEL.md","LIMITS.md"],FEATURES_README="features/README.md",PHARN_FEATURES_README="pharn/features/README.md",CLAUDE_COMMANDS_DIR=".claude/commands",CLAUDE_HOOKS_DIR=".claude/hooks",CLAUDE_SETTINGS_FILE=".claude/settings.json",PRODUCT_COMMAND_PREFIX="pharn-",DEV_COMMAND_PREFIX="pharn-dev-";import{closeSync,constants,fstatSync,lstatSync,openSync,readSync,readdirSync}from"node:fs";import{join,resolve}from"node:path";var SSR_FRAMEWORKS=new Set(["next","nuxt","@remix-run/react","@remix-run/node","@sveltejs/kit","astro","@angular/ssr"]),BACKEND_FRAMEWORKS=new Set(["express","fastify","@nestjs/core","koa","hono","@hapi/hapi","prisma","@prisma/client","drizzle-orm"]),CLIENT_UI=new Set(["react","vue","svelte","@angular/core","solid-js","preact"]),ARCHETYPE_ORDER=["ssr","backend","spa","lib"],hasAny=(names,allowlist)=>{for(let name of allowlist)if(names.has(name))return!0;return!1};function packageSignals(pkg){let names=new Set([...Object.keys(pkg.dependencies??{}),...Object.keys(pkg.devDependencies??{})]);return{ssr:hasAny(names,SSR_FRAMEWORKS),backend:hasAny(names,BACKEND_FRAMEWORKS),clientUi:hasAny(names,CLIENT_UI)}}var SQL_HOST_DIRS=new Set(["migrations","db","database","prisma","drizzle","sql"]),NON_UI_DIRS=new Set(["test","tests","__tests__","__mocks__","spec","e2e","emails","email"]),TEST_FIXTURE_RE=/\.(test|spec)\.(tsx|jsx)$/;function classifyEntry(name,isDir,segments){let lower=name.toLowerCase(),parent=segments[segments.length-1],underDbLocation=segments.some(s=>SQL_HOST_DIRS.has(s));if(isDir){let apiBackend=lower==="api"&&(segments.length===0||parent==="pages"||parent==="app"),migrationsBackend=lower==="migrations"&&(segments.length===0||underDbLocation);return{ssr:!1,backend:apiBackend||migrationsBackend,clientUi:!1}}let isRouteHandler=lower==="route.ts"||lower==="route.tsx"||lower==="route.js"||lower==="route.mjs";return{ssr:lower.startsWith("next.config."),backend:isRouteHandler&&segments.includes("app")||lower.endsWith(".sql")&&underDbLocation,clientUi:(lower.endsWith(".tsx")||lower.endsWith(".jsx"))&&!TEST_FIXTURE_RE.test(lower)&&!segments.some(s=>NON_UI_DIRS.has(s))}}function mergeSignals(a,b){return{ssr:a.ssr||b.ssr,backend:a.backend||b.backend,clientUi:a.clientUi||b.clientUi}}function archetypesFromSignals(sig){let hasSpa=sig.clientUi&&!sig.ssr,found=new Set;return sig.ssr&&found.add("ssr"),sig.backend&&found.add("backend"),hasSpa&&found.add("spa"),found.size===0&&found.add("lib"),ARCHETYPE_ORDER.filter(a=>found.has(a))}var SKIP_DIRS=new Set(["node_modules",".git","dist","build",".next","out","coverage",".turbo",".vercel",".nuxt",".svelte-kit",".astro",".cache",".parcel-cache","storybook-static","__pycache__",".yarn"]),ECOSYSTEM_DIRS=new Map([["target",{siblings:["Cargo.toml","pom.xml","build.sbt"]}],["vendor",{siblings:["go.mod","composer.json","Gemfile"]}],["venv",{inside:"pyvenv.cfg"}],[".venv",{inside:"pyvenv.cfg"}]]);function isEcosystemTree(dir,name,siblingFiles){let marker=ECOSYSTEM_DIRS.get(name.toLowerCase());return marker===void 0?!1:"siblings"in marker?marker.siblings.some(file=>siblingFiles.has(file)):lstatSync(join(dir,name,marker.inside),{throwIfNoEntry:!1})?.isFile()===!0}var MAX_DEPTH=24,MAX_ENTRIES=5e4;function scanFileTreeSignals(root){let acc={ssr:!1,backend:!1,clientUi:!1},budget=MAX_ENTRIES,allFound=()=>acc.ssr&&acc.backend&&acc.clientUi,readEntries=dir=>{try{return readdirSync(dir,{withFileTypes:!0})}catch{return[]}},walk=(dir,depth,segments)=>{if(depth>MAX_DEPTH||budget<=0||allFound())return;let entries=readEntries(dir).sort((a,b)=>a.name<b.name?-1:a.name>b.name?1:0),siblingFiles=new Set(entries.filter(e=>e.isFile()).map(e=>e.name));for(let entry of entries){if(budget<=0||allFound())return;let name=entry.name;if(entry.isSymbolicLink())continue;let isDir=entry.isDirectory();isDir&&SKIP_DIRS.has(name.toLowerCase())||isDir&&isEcosystemTree(dir,name,siblingFiles)||!isDir&&name.toLowerCase().startsWith(".env")||!isDir&&!entry.isFile()||(budget-=1,acc=mergeSignals(acc,classifyEntry(name,isDir,segments)),isDir&&walk(join(dir,name),depth+1,[...segments,name.toLowerCase()]))}};return walk(root,0,[]),acc}var MAX_PACKAGE_JSON_BYTES=4*1024*1024,PKG_OPEN_FLAGS=constants.O_RDONLY|(constants.O_NONBLOCK??0);function readPackageJsonText(pkgPath){let fd;try{fd=openSync(pkgPath,PKG_OPEN_FLAGS)}catch{return null}try{if(!fstatSync(fd).isFile())return null;let buf=Buffer.alloc(MAX_PACKAGE_JSON_BYTES+1),total=0;for(;total<buf.length;){let n=readSync(fd,buf,total,buf.length-total,null);if(n===0)break;total+=n}return total>MAX_PACKAGE_JSON_BYTES?null:buf.subarray(0,total).toString("utf8").replace(/^\uFEFF/,"")}catch{return null}finally{closeSync(fd)}}function readPackageSignals(cwd){let empty=packageSignals({}),text=readPackageJsonText(resolve(cwd,"package.json"));if(text===null)return{pkgSig:empty,packageJsonFound:!1};try{let parsed=JSON.parse(text);return typeof parsed!="object"||parsed===null||Array.isArray(parsed)?{pkgSig:empty,packageJsonFound:!1}:{pkgSig:packageSignals(parsed),packageJsonFound:!0}}catch{return{pkgSig:empty,packageJsonFound:!1}}}function detectArchetypesFromProject(cwd){let{pkgSig,packageJsonFound}=readPackageSignals(cwd),fileSig=scanFileTreeSignals(cwd),merged=mergeSignals(pkgSig,fileSig);return{archetypes:archetypesFromSignals(merged),packageJsonFound}}function row(label,value){return`${` ${label}`.padEnd(28)}${value}`}var ROLE_GROUPS=[{role:"griller",label:"grillers"},{role:"lens",label:"lenses"}],BULLET="\u2014";function renderCapabilityLines(view){let lines=[row("Skills version",`v${view.skillsVersion}`),row("Archetypes",view.archetypes.join(", ")||"(none)"),""," CAPABILITIES"];if(view.capabilities.length===0)return lines.push(" (none)"),lines;for(let{role,label}of ROLE_GROUPS){let inRole=view.capabilities.filter(c=>c.role===role);if(inRole.length!==0){lines.push(` ${label} (${inRole.length})`);for(let cap of inRole){let tag=cap.source==="manual"?" (manual)":"";lines.push(` ${BULLET} ${cap.name}${tag}`)}}}return lines}function groupByRole(caps){let groups={};for(let{role,label}of ROLE_GROUPS){let inRole=caps.filter(c=>c.role===role);inRole.length!==0&&(groups[label]=inRole.map(c=>({value:`${c.role}:${c.name}`,label:c.name})))}return groups}function buildAddSelection(index,installed2){let isInstalled=c=>installed2.some(i=>i.name===c.name&&i.role===c.role),available=index.capabilities.filter(c=>!isInstalled(c));return{groups:groupByRole(available),availableCount:available.length,installed:[...installed2]}}function buildRemoveSelection(installed2){return{groups:groupByRole(installed2)}}function interactiveAllowed(streams){return!!streams.stdinIsTTY&&!!streams.stdoutIsTTY}import{closeSync as closeSync2,constants as fsConstants,existsSync as existsSync2,fstatSync as fstatSync2,openSync as openSync2,readFileSync,readdirSync as readdirSync2}from"node:fs";import{existsSync}from"node:fs";import{dirname,resolve as resolve2,sep}from"node:path";var ManifestValidationError=class extends Error{constructor(message){super(message),this.name="ManifestValidationError"}},VERSION_RE=/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/,CAPABILITY_NAME_RE=/^[a-z0-9]+(-[a-z0-9]+)*$/,COPY_FILENAME_RE=/^[a-z0-9]+(-[a-z0-9]+)*\.(md|cjs|mjs|json)$/,COMMIT_RE=/^[0-9a-f]{40}$/,ROLE_VALUES=["griller","lens"],APPLIES_TOKEN_VALUES=["universal","ssr","backend","spa","lib"],CONTROL_CHARS_RE=/[\x00-\x1f\x7f-\x9f]/;function assertRole(value,label){if(typeof value!="string")throw new ManifestValidationError(`${label} role must be a string`);let match=ROLE_VALUES.find(r=>r===value);if(!match)throw new ManifestValidationError(`${label} has invalid role ${JSON.stringify(value)} (expected one of ${ROLE_VALUES.join(", ")})`);return match}function assertAppliesToken(value,label){let match=APPLIES_TOKEN_VALUES.find(t=>t===value);if(!match)throw new ManifestValidationError(`${label} has invalid applies value ${JSON.stringify(value)} (expected one of ${APPLIES_TOKEN_VALUES.join(", ")})`);return match}function assertSafeString(value,label,pattern){if(typeof value!="string")throw new ManifestValidationError(`${label} must be a string`);if(CONTROL_CHARS_RE.test(value))throw new ManifestValidationError(`${label} contains control characters`);if(!pattern.test(value))throw new ManifestValidationError(`${label} has invalid format: ${JSON.stringify(value)}`);return value}function assertNoDotDot(value,label){if(value.includes(".."))throw new ManifestValidationError(`${label} must not contain '..'`)}function isPlainObject(v){return typeof v=="object"&&v!==null&&!Array.isArray(v)}function safeJoin(base,rel){let target=resolve2(base,rel),root=resolve2(base);if(target!==root&&!target.startsWith(root+sep))throw new ManifestValidationError(`Refusing path escape: ${rel} resolves outside ${base}`);return target}function safeChildJoin(base,name){let root=resolve2(base),target=resolve2(root,name);if(target===root||dirname(target)!==root)throw new ManifestValidationError(`Refusing path: ${JSON.stringify(name)} is not a single directory entry directly under ${base}`);return target}function toPosix(rel){return rel.split(sep).join("/").replace(/\/+$/,"")}function detectLayout(rootDir){return existsSync(safeJoin(rootDir,PHARN_CONTRACTS_DIR))?"pharn":"flat"}function layoutPaths(layout){return layout==="pharn"?{layout,grillers:PHARN_GRILLERS_DIR,lenses:PHARN_LENSES_DIR,contracts:PHARN_CONTRACTS_DIR,core:PHARN_CORE_DIR,floor:PHARN_FLOOR_DIR,docs:PHARN_TRUSTED_DOCS,featuresReadme:PHARN_FEATURES_README,license:{from:UPSTREAM_LICENSE,to:PHARN_LICENSE_DEST}}:{layout,grillers:GRILLERS_DIR,lenses:LENSES_DIR,contracts:CONTRACTS_DIR,core:CORE_DIR,floor:FLOOR_DIR,docs:TRUSTED_DOCS,featuresReadme:FEATURES_README,license:{from:UPSTREAM_LICENSE,to:FLAT_LICENSE_DEST}}}function capabilitySubtree(paths,role){return role==="griller"?paths.grillers:paths.lenses}function configLayout(config){return config.layout==="pharn"?"pharn":"flat"}function resolveFeaturesReadme(repoDir,layout){if(layout==="pharn"){let preferred=safeJoin(repoDir,PHARN_FEATURES_README);if(!existsSync(preferred))return FEATURES_README}return layoutPaths(layout).featuresReadme}function parseCapabilityIndex(repoDir){let capabilities=[],unknown=[],paths=layoutPaths(detectLayout(repoDir)),subtrees=[{dir:paths.grillers,role:"griller"},{dir:paths.lenses,role:"lens"}];for(let subtree of subtrees){let subtreeDir=safeJoin(repoDir,subtree.dir);if(!existsSync2(subtreeDir))throw new ManifestValidationError(`Capability subtree "${subtree.dir}" is missing in the fetched repo.`);let names=readdirSync2(subtreeDir,{withFileTypes:!0}).filter(e=>e.isDirectory()&&!e.isSymbolicLink()).map(e=>e.name).sort((a,b)=>a<b?-1:a>b?1:0);for(let name of names)try{assertSafeString(name,`capability "${name}"`,CAPABILITY_NAME_RE),assertNoDotDot(name,`capability "${name}"`);let capFile=safeJoin(subtreeDir,`${name}/${name}.md`),frontmatter=extractFrontmatter(readCapabilityMarkdown(capFile,name,subtree.dir),name),role=assertRole(readField(frontmatter,"role",name),name);if(role!==subtree.role)throw new ManifestValidationError(`Capability "${name}" declares role "${role}" but lives under ${subtree.dir} (expected "${subtree.role}").`);let applies=parseApplies(readField(frontmatter,"applies",name),name);capabilities.push({name,role,applies})}catch(err){if(!(err instanceof ManifestValidationError))throw err;unknown.push({name,role:subtree.role,subtree:subtree.dir,reason:err.message})}}return{capabilities,unknown}}var OPEN_FLAGS=fsConstants.O_RDONLY|(fsConstants.O_NOFOLLOW??0)|(fsConstants.O_NONBLOCK??0);function readCapabilityMarkdown(capFile,name,subtreeDir){let fd;try{fd=openSync2(capFile,OPEN_FLAGS)}catch(err){let code=err.code;throw code==="ENOENT"?new ManifestValidationError(`Capability "${name}" in ${subtreeDir} is missing its markdown ${name}/${name}.md.`):code==="ELOOP"||code==="EISDIR"?new ManifestValidationError(`Capability "${name}" in ${subtreeDir}: ${name}/${name}.md is not a regular file.`):err}try{if(!fstatSync2(fd).isFile())throw new ManifestValidationError(`Capability "${name}" in ${subtreeDir}: ${name}/${name}.md is not a regular file.`);return readFileSync(fd,"utf8")}finally{closeSync2(fd)}}function extractFrontmatter(content,name){let end=content.startsWith("---")?content.indexOf(`
2
+ import{realpathSync}from"node:fs";import{fileURLToPath}from"node:url";import minimist from"minimist";import{intro,log as log5,note as note2,spinner as spinner2}from"@clack/prompts";import pc from"picocolors";import{createRequire}from"node:module";var require2=createRequire(import.meta.url),PHARN_VERSION=require2("../package.json").version;var LOGO=["\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557","\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551","\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551","\u2588\u2588\u2554\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551","\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551","\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D"];function showBanner(){console.log();for(let line of LOGO)console.log(` ${pc.cyan(pc.bold(line))}`);console.log(),console.log(` ${pc.dim(`Audit-grade methodology for Claude Code \xB7 v${PHARN_VERSION}`)}`),console.log()}import{confirm,isCancel,log}from"@clack/prompts";function cancelAndExit(message="Cancelled. Nothing was changed."){log.info(message),process.exit(0)}import{log as log2}from"@clack/prompts";var UNSAFE_RE=/[\x00-\x1f\x7f-\x9f\p{Cf}]/gu,UNSAFE_KEEP_WS_RE=/[\x00-\x08\x0b-\x1f\x7f-\x9f\p{Cf}]/gu;function terminalSafe(value,options={}){let stripped=value.replace(options.keepNewlines?UNSAFE_KEEP_WS_RE:UNSAFE_RE,""),{max}=options;return max!==void 0&&stripped.length>max?`${stripped.slice(0,max)}\u2026`:stripped}function hasUnsafeChars(value){return/[\x00-\x1f\x7f-\x9f\p{Cf}]/u.test(value)}var PHARN_DEBUG_HINT="Re-run with PHARN_DEBUG=1 for full error output.";function errorMessage(err){if(err instanceof Error)return err.message;try{return String(err)}catch{return"an unstringifiable value was thrown"}}function logError(message){log2.error(terminalSafe(message,{keepNewlines:!0}),{output:process.stderr})}function reportFatal(message,cause){if(logError(`\u26A0 ${message}`),cause!==void 0){if(process.env.PHARN_DEBUG){console.error(cause.err);return}log2.info(PHARN_DEBUG_HINT,{output:process.stderr})}}var REPO="pharn-dev/pharn-oss",REPO_BRANCH="main",REPO_URL="github.com/pharn-dev/pharn-oss",FIRST_FEATURE_COMMAND="/pharn-spec",GRILLERS_DIR="pharn-pipeline/grillers",LENSES_DIR="pharn-review",SKILLS_VERSION_FILE="SKILLS_VERSION",MIN_CLI_FILE="MIN_CLI",UPSTREAM_LICENSE="LICENSE",FLAT_LICENSE_DEST="PHARN-LICENSE",PHARN_LICENSE_DEST="pharn/LICENSE",TRUSTED_DOCS=["CONSTITUTION.md","ARCHITECTURE.md","THREAT-MODEL.md","LIMITS.md"],CONTRACTS_DIR="pharn-contracts",CORE_DIR="pharn-core",FLOOR_DIR=".dev/floor",FLOOR_TEST_FIXTURES_DIR="test-fixtures",PHARN_GRILLERS_DIR="pharn/pharn-pipeline/grillers",PHARN_LENSES_DIR="pharn/pharn-review",PHARN_CONTRACTS_DIR="pharn/pharn-contracts",PHARN_CORE_DIR="pharn/pharn-core",PHARN_FLOOR_DIR="pharn/floor",PHARN_TRUSTED_DOCS=["pharn/CONSTITUTION.md","pharn/ARCHITECTURE.md","THREAT-MODEL.md","LIMITS.md"],FEATURES_README="features/README.md",PHARN_FEATURES_README="pharn/features/README.md",CLAUDE_COMMANDS_DIR=".claude/commands",CLAUDE_HOOKS_DIR=".claude/hooks",CLAUDE_SETTINGS_FILE=".claude/settings.json",PRODUCT_COMMAND_PREFIX="pharn-",DEV_COMMAND_PREFIX="pharn-dev-";import{closeSync,constants,fstatSync,lstatSync,openSync,readSync,readdirSync}from"node:fs";import{join,resolve}from"node:path";var SSR_FRAMEWORKS=new Set(["next","nuxt","@remix-run/react","@remix-run/node","@sveltejs/kit","astro","@angular/ssr"]),BACKEND_FRAMEWORKS=new Set(["express","fastify","@nestjs/core","koa","hono","@hapi/hapi","prisma","@prisma/client","drizzle-orm"]),CLIENT_UI=new Set(["react","vue","svelte","@angular/core","solid-js","preact"]),ARCHETYPE_ORDER=["ssr","backend","spa","lib"],hasAny=(names,allowlist)=>{for(let name of allowlist)if(names.has(name))return!0;return!1};function packageSignals(pkg){let names=new Set([...Object.keys(pkg.dependencies??{}),...Object.keys(pkg.devDependencies??{})]);return{ssr:hasAny(names,SSR_FRAMEWORKS),backend:hasAny(names,BACKEND_FRAMEWORKS),clientUi:hasAny(names,CLIENT_UI)}}var SQL_HOST_DIRS=new Set(["migrations","db","database","prisma","drizzle","sql"]),NON_UI_DIRS=new Set(["test","tests","__tests__","__mocks__","spec","e2e","emails","email"]),TEST_FIXTURE_RE=/\.(test|spec)\.(tsx|jsx)$/;function classifyEntry(name,isDir,segments){let lower=name.toLowerCase(),parent=segments[segments.length-1],underDbLocation=segments.some(s=>SQL_HOST_DIRS.has(s));if(isDir){let apiBackend=lower==="api"&&(segments.length===0||parent==="pages"||parent==="app"),migrationsBackend=lower==="migrations"&&(segments.length===0||underDbLocation);return{ssr:!1,backend:apiBackend||migrationsBackend,clientUi:!1}}let isRouteHandler=lower==="route.ts"||lower==="route.tsx"||lower==="route.js"||lower==="route.mjs";return{ssr:lower.startsWith("next.config."),backend:isRouteHandler&&segments.includes("app")||lower.endsWith(".sql")&&underDbLocation,clientUi:(lower.endsWith(".tsx")||lower.endsWith(".jsx"))&&!TEST_FIXTURE_RE.test(lower)&&!segments.some(s=>NON_UI_DIRS.has(s))}}function mergeSignals(a,b){return{ssr:a.ssr||b.ssr,backend:a.backend||b.backend,clientUi:a.clientUi||b.clientUi}}function archetypesFromSignals(sig){let hasSpa=sig.clientUi&&!sig.ssr,found=new Set;return sig.ssr&&found.add("ssr"),sig.backend&&found.add("backend"),hasSpa&&found.add("spa"),found.size===0&&found.add("lib"),ARCHETYPE_ORDER.filter(a=>found.has(a))}var SKIP_DIRS=new Set(["node_modules",".git","dist","build",".next","out","coverage",".turbo",".vercel",".nuxt",".svelte-kit",".astro",".cache",".parcel-cache","storybook-static","__pycache__",".yarn"]),ECOSYSTEM_DIRS=new Map([["target",{siblings:["Cargo.toml","pom.xml","build.sbt"]}],["vendor",{siblings:["go.mod","composer.json","Gemfile"]}],["venv",{inside:"pyvenv.cfg"}],[".venv",{inside:"pyvenv.cfg"}]]);function isEcosystemTree(dir,name,siblingFiles){let marker=ECOSYSTEM_DIRS.get(name.toLowerCase());return marker===void 0?!1:"siblings"in marker?marker.siblings.some(file=>siblingFiles.has(file)):lstatSync(join(dir,name,marker.inside),{throwIfNoEntry:!1})?.isFile()===!0}var MAX_DEPTH=24,MAX_ENTRIES=5e4;function scanFileTreeSignals(root){let acc={ssr:!1,backend:!1,clientUi:!1},budget=MAX_ENTRIES,allFound=()=>acc.ssr&&acc.backend&&acc.clientUi,readEntries=dir=>{try{return readdirSync(dir,{withFileTypes:!0})}catch{return[]}},walk=(dir,depth,segments)=>{if(depth>MAX_DEPTH||budget<=0||allFound())return;let entries=readEntries(dir).sort((a,b)=>a.name<b.name?-1:a.name>b.name?1:0),siblingFiles=new Set(entries.filter(e=>e.isFile()).map(e=>e.name));for(let entry of entries){if(budget<=0||allFound())return;let name=entry.name;if(entry.isSymbolicLink())continue;let isDir=entry.isDirectory();isDir&&SKIP_DIRS.has(name.toLowerCase())||isDir&&isEcosystemTree(dir,name,siblingFiles)||!isDir&&name.toLowerCase().startsWith(".env")||!isDir&&!entry.isFile()||(budget-=1,acc=mergeSignals(acc,classifyEntry(name,isDir,segments)),isDir&&walk(join(dir,name),depth+1,[...segments,name.toLowerCase()]))}};return walk(root,0,[]),acc}var MAX_PACKAGE_JSON_BYTES=4*1024*1024,PKG_OPEN_FLAGS=constants.O_RDONLY|(constants.O_NONBLOCK??0);function readPackageJsonText(pkgPath){let fd;try{fd=openSync(pkgPath,PKG_OPEN_FLAGS)}catch{return null}try{if(!fstatSync(fd).isFile())return null;let buf=Buffer.alloc(MAX_PACKAGE_JSON_BYTES+1),total=0;for(;total<buf.length;){let n=readSync(fd,buf,total,buf.length-total,null);if(n===0)break;total+=n}return total>MAX_PACKAGE_JSON_BYTES?null:buf.subarray(0,total).toString("utf8").replace(/^\uFEFF/,"")}catch{return null}finally{closeSync(fd)}}function readPackageSignals(cwd){let empty=packageSignals({}),text=readPackageJsonText(resolve(cwd,"package.json"));if(text===null)return{pkgSig:empty,packageJsonFound:!1};try{let parsed=JSON.parse(text);return typeof parsed!="object"||parsed===null||Array.isArray(parsed)?{pkgSig:empty,packageJsonFound:!1}:{pkgSig:packageSignals(parsed),packageJsonFound:!0}}catch{return{pkgSig:empty,packageJsonFound:!1}}}function detectArchetypesFromProject(cwd){let{pkgSig,packageJsonFound}=readPackageSignals(cwd),fileSig=scanFileTreeSignals(cwd),merged=mergeSignals(pkgSig,fileSig);return{archetypes:archetypesFromSignals(merged),packageJsonFound}}function row(label,value){return`${` ${label}`.padEnd(28)}${value}`}var ROLE_GROUPS=[{role:"griller",label:"grillers"},{role:"lens",label:"lenses"}],BULLET="\u2014";function renderCapabilityLines(view){let lines=[row("Skills version",`v${view.skillsVersion}`),row("Archetypes",view.archetypes.join(", ")||"(none)"),""," CAPABILITIES"];if(view.capabilities.length===0)return lines.push(" (none)"),lines;for(let{role,label}of ROLE_GROUPS){let inRole=view.capabilities.filter(c=>c.role===role);if(inRole.length!==0){lines.push(` ${label} (${inRole.length})`);for(let cap of inRole){let tag=cap.source==="manual"?" (manual)":"";lines.push(` ${BULLET} ${cap.name}${tag}`)}}}return lines}function groupByRole(caps){let groups={};for(let{role,label}of ROLE_GROUPS){let inRole=caps.filter(c=>c.role===role);inRole.length!==0&&(groups[label]=inRole.map(c=>({value:`${c.role}:${c.name}`,label:c.name})))}return groups}function buildAddSelection(index,installed2){let isInstalled=c=>installed2.some(i=>i.name===c.name&&i.role===c.role),available=index.capabilities.filter(c=>!isInstalled(c));return{groups:groupByRole(available),availableCount:available.length,installed:[...installed2]}}function buildRemoveSelection(installed2){return{groups:groupByRole(installed2)}}function interactiveAllowed(streams){return!!streams.stdinIsTTY&&!!streams.stdoutIsTTY}import{closeSync as closeSync2,constants as fsConstants,existsSync as existsSync2,fstatSync as fstatSync2,openSync as openSync2,readFileSync,readdirSync as readdirSync2}from"node:fs";import{existsSync}from"node:fs";import{dirname,resolve as resolve2,sep}from"node:path";var ManifestValidationError=class extends Error{constructor(message){super(message),this.name="ManifestValidationError"}},VERSION_RE=/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/,CAPABILITY_NAME_RE=/^[a-z0-9]+(-[a-z0-9]+)*$/,COPY_FILENAME_RE=/^[a-z0-9]+(-[a-z0-9]+)*\.(md|cjs|mjs|json)$/,COMMIT_RE=/^[0-9a-f]{40}$/,RELEASE_TAG_RE=/^v(\d+\.\d+\.\d+)$/,ROLE_VALUES=["griller","lens"],APPLIES_TOKEN_VALUES=["universal","ssr","backend","spa","lib"],CONTROL_CHARS_RE=/[\x00-\x1f\x7f-\x9f]/;function assertRole(value,label){if(typeof value!="string")throw new ManifestValidationError(`${label} role must be a string`);let match=ROLE_VALUES.find(r=>r===value);if(!match)throw new ManifestValidationError(`${label} has invalid role ${JSON.stringify(value)} (expected one of ${ROLE_VALUES.join(", ")})`);return match}function assertAppliesToken(value,label){let match=APPLIES_TOKEN_VALUES.find(t=>t===value);if(!match)throw new ManifestValidationError(`${label} has invalid applies value ${JSON.stringify(value)} (expected one of ${APPLIES_TOKEN_VALUES.join(", ")})`);return match}function assertSafeString(value,label,pattern){if(typeof value!="string")throw new ManifestValidationError(`${label} must be a string`);if(CONTROL_CHARS_RE.test(value))throw new ManifestValidationError(`${label} contains control characters`);if(!pattern.test(value))throw new ManifestValidationError(`${label} has invalid format: ${JSON.stringify(value)}`);return value}function assertNoDotDot(value,label){if(value.includes(".."))throw new ManifestValidationError(`${label} must not contain '..'`)}function isPlainObject(v){return typeof v=="object"&&v!==null&&!Array.isArray(v)}function safeJoin(base,rel){let target=resolve2(base,rel),root=resolve2(base);if(target!==root&&!target.startsWith(root+sep))throw new ManifestValidationError(`Refusing path escape: ${rel} resolves outside ${base}`);return target}function safeChildJoin(base,name){let root=resolve2(base),target=resolve2(root,name);if(target===root||dirname(target)!==root)throw new ManifestValidationError(`Refusing path: ${JSON.stringify(name)} is not a single directory entry directly under ${base}`);return target}function toPosix(rel){return rel.split(sep).join("/").replace(/\/+$/,"")}function detectLayout(rootDir){return existsSync(safeJoin(rootDir,PHARN_CONTRACTS_DIR))?"pharn":"flat"}function layoutPaths(layout){return layout==="pharn"?{layout,grillers:PHARN_GRILLERS_DIR,lenses:PHARN_LENSES_DIR,contracts:PHARN_CONTRACTS_DIR,core:PHARN_CORE_DIR,floor:PHARN_FLOOR_DIR,docs:PHARN_TRUSTED_DOCS,featuresReadme:PHARN_FEATURES_README,license:{from:UPSTREAM_LICENSE,to:PHARN_LICENSE_DEST}}:{layout,grillers:GRILLERS_DIR,lenses:LENSES_DIR,contracts:CONTRACTS_DIR,core:CORE_DIR,floor:FLOOR_DIR,docs:TRUSTED_DOCS,featuresReadme:FEATURES_README,license:{from:UPSTREAM_LICENSE,to:FLAT_LICENSE_DEST}}}function capabilitySubtree(paths,role){return role==="griller"?paths.grillers:paths.lenses}function configLayout(config){return config.layout==="pharn"?"pharn":"flat"}function resolveFeaturesReadme(repoDir,layout){if(layout==="pharn"){let preferred=safeJoin(repoDir,PHARN_FEATURES_README);if(!existsSync(preferred))return FEATURES_README}return layoutPaths(layout).featuresReadme}function parseCapabilityIndex(repoDir){let capabilities=[],unknown=[],paths=layoutPaths(detectLayout(repoDir)),subtrees=[{dir:paths.grillers,role:"griller"},{dir:paths.lenses,role:"lens"}];for(let subtree of subtrees){let subtreeDir=safeJoin(repoDir,subtree.dir);if(!existsSync2(subtreeDir))throw new ManifestValidationError(`Capability subtree "${subtree.dir}" is missing in the fetched repo.`);let names=readdirSync2(subtreeDir,{withFileTypes:!0}).filter(e=>e.isDirectory()&&!e.isSymbolicLink()).map(e=>e.name).sort((a,b)=>a<b?-1:a>b?1:0);for(let name of names)try{assertSafeString(name,`capability "${name}"`,CAPABILITY_NAME_RE),assertNoDotDot(name,`capability "${name}"`);let capFile=safeJoin(subtreeDir,`${name}/${name}.md`),frontmatter=extractFrontmatter(readCapabilityMarkdown(capFile,name,subtree.dir),name),role=assertRole(readField(frontmatter,"role",name),name);if(role!==subtree.role)throw new ManifestValidationError(`Capability "${name}" declares role "${role}" but lives under ${subtree.dir} (expected "${subtree.role}").`);let applies=parseApplies(readField(frontmatter,"applies",name),name);capabilities.push({name,role,applies})}catch(err){if(!(err instanceof ManifestValidationError))throw err;unknown.push({name,role:subtree.role,subtree:subtree.dir,reason:err.message})}}return{capabilities,unknown}}var OPEN_FLAGS=fsConstants.O_RDONLY|(fsConstants.O_NOFOLLOW??0)|(fsConstants.O_NONBLOCK??0);function readCapabilityMarkdown(capFile,name,subtreeDir){let fd;try{fd=openSync2(capFile,OPEN_FLAGS)}catch(err){let code=err.code;throw code==="ENOENT"?new ManifestValidationError(`Capability "${name}" in ${subtreeDir} is missing its markdown ${name}/${name}.md.`):code==="ELOOP"||code==="EISDIR"?new ManifestValidationError(`Capability "${name}" in ${subtreeDir}: ${name}/${name}.md is not a regular file.`):err}try{if(!fstatSync2(fd).isFile())throw new ManifestValidationError(`Capability "${name}" in ${subtreeDir}: ${name}/${name}.md is not a regular file.`);return readFileSync(fd,"utf8")}finally{closeSync2(fd)}}function extractFrontmatter(content,name){let end=content.startsWith("---")?content.indexOf(`
3
3
  ---`,3):-1;if(end===-1)throw new ManifestValidationError(`Capability "${name}" is missing a "---"-fenced frontmatter block.`);return content.slice(3,end).trim()}function readField(frontmatter,field,name){let re=new RegExp(`^${field}:[ \\t]*(.+?)[ \\t]*$`,"gm"),matches=[...frontmatter.matchAll(re)];if(matches.length===0)throw new ManifestValidationError(`Capability "${name}" is missing the "${field}" frontmatter field.`);if(matches.length>1)throw new ManifestValidationError(`Capability "${name}" declares the "${field}" frontmatter field ${matches.length} times.`);return stripQuotes(matches[0][1])}function stripQuotes(value){let trimmed=value.trim();return trimmed.startsWith('"')&&trimmed.endsWith('"')||trimmed.startsWith("'")&&trimmed.endsWith("'")?trimmed.slice(1,-1):trimmed}function parseApplies(raw,name){if(!/^\[.*\]$/.test(raw))throw new ManifestValidationError(`Capability "${name}" has a malformed "applies" value ${JSON.stringify(raw)} (expected a "[...]" array).`);let tokens=raw.slice(1,-1).split(",").map(t=>stripQuotes(t)).filter(t=>t.length>0);if(tokens.length===0)throw new ManifestValidationError(`Capability "${name}" has an empty "applies" array \u2014 every capability must declare its applicability.`);let validated=tokens.map(t=>assertAppliesToken(t,`capability "${name}"`)),universal=validated.includes("universal");if(universal&&validated.length>1)throw new ManifestValidationError(`Capability "${name}" mixes "universal" with archetypes in "applies" \u2014 use exactly ["universal"] or archetypes only.`);return universal?"universal":validated}var MAX_FIELD=200,MAX_LISTED=10;function safe(value){return terminalSafe(value,{max:MAX_FIELD})}function unknownCapabilitiesWarning(unknown){if(unknown.length===0)return null;let listed=unknown.slice(0,MAX_LISTED),lines=[`${unknown.length} upstream ${unknown.length===1?"capability":"capabilities"} could not be read and ${unknown.length===1?"was":"were"} SKIPPED \u2014 not installed:`];for(let u of listed)lines.push(` ${u.role}:${safe(u.name)} (${safe(u.subtree)}) \u2014 ${safe(u.reason)}`);return unknown.length>listed.length&&lines.push(` \u2026and ${unknown.length-listed.length} more.`),lines.push("This usually means pharn-oss changed something your pharn version does not understand yet. Upgrade with `npm install -g @pharn-dev/pharn@latest`."),lines.join(`
4
- `)}var CORE_RE=/^(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?$/;function core(version){let match=CORE_RE.exec(version);return match?[Number(match[1]),Number(match[2]),Number(match[3])]:null}function compareVersionCore(a,b){let left=core(a),right=core(b);if(left===null||right===null)return null;for(let i=0;i<3;i++){let l=left[i],r=right[i];if(l!==r)return l>r?1:-1}return 0}async function withDeadline(ms,onTimeout,work){let controller=new AbortController,timer,expired=new Promise((_,reject)=>{timer=setTimeout(()=>{controller.abort(),reject(onTimeout())},ms)}),running=work(controller.signal);running.catch(()=>{});try{return await Promise.race([running,expired])}finally{clearTimeout(timer)}}import{closeSync as closeSync3,existsSync as existsSync3,openSync as openSync3,readFileSync as readFileSync2,readSync as readSync2}from"node:fs";var RAW="https://raw.githubusercontent.com",MAX_MIN_CLI_BYTES=1024,FETCH_TIMEOUT_MS=8e3,MAX_BODY_BYTES=256*1024;function readSkillsVersion(repoDir){let path=safeJoin(repoDir,SKILLS_VERSION_FILE);if(!existsSync3(path))throw new ManifestValidationError(`${SKILLS_VERSION_FILE} is missing in the fetched repo.`);return assertSafeString(readFileSync2(path,"utf8").trim(),SKILLS_VERSION_FILE,VERSION_RE)}function readMinCli(repoDir){let path=safeJoin(repoDir,MIN_CLI_FILE),fd;try{fd=openSync3(path,"r")}catch(err){return err.code==="ENOENT"?{version:null,warning:null}:{version:null,warning:unusableMinCli(err)}}let raw;try{let buf=Buffer.alloc(MAX_MIN_CLI_BYTES+1),bytes=readSync2(fd,buf,0,buf.length,0);if(bytes>MAX_MIN_CLI_BYTES)return{version:null,warning:`${MIN_CLI_FILE} in the fetched repo is too large to be a version; continuing without a minimum-version constraint.`};raw=buf.subarray(0,bytes).toString("utf8")}catch(err){return{version:null,warning:unusableMinCli(err)}}finally{closeSync3(fd)}let value=raw.trim();return VERSION_RE.test(value)?{version:value,warning:null}:{version:null,warning:`${MIN_CLI_FILE} in the fetched repo is not a valid version; continuing without a minimum-version constraint.`}}function unusableMinCli(err){let code=err?.code??(err instanceof Error?err.name:"unknown error");return`${MIN_CLI_FILE} in the fetched repo could not be read (${code}); continuing without a minimum-version constraint.`}function rethrowUnreachable(url){return err=>{let message=err instanceof Error?err.message:String(err),inner=err instanceof Error?err.cause:void 0,cause=inner===void 0?null:inner instanceof Error?inner.message:String(inner);throw new Error(`Could not reach ${url}: ${message}${cause?` (${cause})`:""}`,{cause:err})}}async function fetchRemoteSkillsVersion(){let url=`${RAW}/${REPO}/${REPO_BRANCH}/${SKILLS_VERSION_FILE}`,unreachable=rethrowUnreachable(url);return withDeadline(FETCH_TIMEOUT_MS,()=>new Error(`Could not reach ${url}: This operation was aborted (timed out after ${FETCH_TIMEOUT_MS/1e3}s)`),async signal=>{let res=await fetch(url,{redirect:"error",signal}).catch(unreachable);if(!res.ok)throw new Error(`SKILLS_VERSION fetch failed (${res.status}) from ${url}`);let declared=Number(res.headers.get("content-length"));if(Number.isFinite(declared)&&declared>MAX_BODY_BYTES)throw new Error(`SKILLS_VERSION too large (${declared} bytes) from ${url}`);let text=await readCappedBody(res,url,unreachable,signal);return assertSafeString(text.trim(),SKILLS_VERSION_FILE,VERSION_RE)})}async function readCappedBody(res,url,unreachable,signal){if(!res.body)return"";let reader=res.body.getReader();signal.addEventListener("abort",()=>{reader.cancel().catch(()=>{})});let chunks=[],total=0;for(;;){let{done,value}=await reader.read().catch(unreachable);if(done)break;if(total+=value.byteLength,total>MAX_BODY_BYTES)throw await reader.cancel().catch(()=>{}),new Error(`SKILLS_VERSION too large (${total} bytes) from ${url}`);chunks.push(value)}let merged=new Uint8Array(total),offset=0;for(let chunk of chunks)merged.set(chunk,offset),offset+=chunk.byteLength;return new TextDecoder().decode(merged)}function minCliGate(repoDir,cliVersion){let read=readMinCli(repoDir);if(read.version===null)return{refusal:null,warning:read.warning};let cmp=compareVersionCore(read.version,cliVersion);return cmp===null?{refusal:null,warning:`Could not compare the installed pharn version (${cliVersion}) against the minimum ${REPO_URL} declares (${read.version}); continuing without a minimum-version constraint.`}:cmp<=0?{refusal:null,warning:null}:{refusal:`This pharn is too old for the current ${REPO_URL}: it requires @pharn-dev/pharn v${read.version} or newer, and v${cliVersion} is installed. Upgrade (\`npm install -g @pharn-dev/pharn@latest\`, or run \`npx @pharn-dev/pharn@latest\`), then re-run this command. Nothing was written.`,warning:null}}function resolveCapabilities(archetypes,index){let detected=new Set(archetypes),selected=[],skipped=[];for(let entry of index.capabilities){if(entry.applies==="universal"){selected.push({name:entry.name,role:entry.role,matched:"universal"});continue}let matched=entry.applies.filter(a=>detected.has(a));matched.length>0?selected.push({name:entry.name,role:entry.role,matched}):skipped.push({name:entry.name,role:entry.role,reason:`applies to [${entry.applies.join(", ")}]; detected [${archetypes.join(", ")}]`})}return{selected,skipped}}import{mkdtempSync,rmSync}from"node:fs";import{tmpdir}from"node:os";import{join as join2}from"node:path";var FATAL_SIGNALS=["SIGINT","SIGTERM"],SIGNAL_NUMBER={SIGINT:2,SIGTERM:15},cleanups=new Set,installed=!1;function onFatalSignal(cleanup){install();let entry=()=>cleanup();return cleanups.add(entry),()=>{cleanups.delete(entry)}}function install(){if(!installed){installed=!0;for(let sig of FATAL_SIGNALS)process.on(sig,()=>die(sig))}}function die(sig){let pending=[...cleanups].reverse();cleanups.clear();for(let cleanup of pending)try{cleanup()}catch{}process.removeAllListeners(sig);try{process.kill(process.pid,sig)}catch{process.exit(128+SIGNAL_NUMBER[sig])}}import{gunzipSync}from"node:zlib";import{mkdirSync,writeFileSync}from"node:fs";import{dirname as dirname2}from"node:path";var BLOCK=512,OFF_NAME=0,LEN_NAME=100,OFF_SIZE=124,LEN_SIZE=12,OFF_CHKSUM=148,LEN_CHKSUM=8,OFF_TYPEFLAG=156,OFF_MAGIC=257,OFF_PREFIX=345,LEN_PREFIX=155,TarExtractError=class extends Error{constructor(message){super(message),this.name="TarExtractError"}};function readString(block,offset,length){let raw=block.subarray(offset,offset+length),end=raw.indexOf(0);return raw.subarray(0,end===-1?raw.length:end).toString("latin1")}function describeBytes(bytes,max=200){let out="";for(let byte of bytes.subarray(0,max))out+=byte>=32&&byte<=126&&byte!==92?String.fromCharCode(byte):`\\x${byte.toString(16).padStart(2,"0")}`;return bytes.length>max?`${out}\u2026`:out}var PATH_DECODER=new TextDecoder("utf-8",{fatal:!0,ignoreBOM:!0});function readPathField(block,offset,length){let raw=block.subarray(offset,offset+length),end=raw.indexOf(0),bytes=raw.subarray(0,end===-1?raw.length:end);try{return PATH_DECODER.decode(bytes)}catch{throw new TarExtractError(`tar entry path is not valid UTF-8: "${describeBytes(bytes)}"`)}}function readOctal(block,offset,length){let raw=block.subarray(offset,offset+length);if(raw.length>0&&(raw[0]&128)!==0)throw new TarExtractError("tar header uses the GNU base-256 numeric encoding, which pharn does not accept.");let text=readString(block,offset,length).trim();if(text==="")return 0;if(!/^[0-7]+$/.test(text))throw new TarExtractError(`tar header has a non-octal numeric field: "${describeBytes(Buffer.from(text,"latin1"),32)}"`);return parseInt(text,8)}var PAX_OVERRIDE_KEYWORDS=new Set(["path","linkpath","size"]),PAX_KEYWORD_RE=/^[A-Za-z0-9._-]{1,64}$/,PAX_KEYWORDS_IN_MESSAGE=8;function readPaxKeywords(payload){let keywords=[],at=0;for(;at<payload.length;){let space=payload.indexOf(32,at);if(space===-1)return null;let digits=payload.subarray(at,space).toString("latin1");if(!/^[0-9]{1,10}$/.test(digits))return null;let end=at+Number(digits);if(end>payload.length||end<=space||payload[end-1]!==10)return null;let eq=payload.indexOf(61,space+1);if(eq===-1||eq<space+2||eq>end-2)return null;keywords.push(payload.subarray(space+1,eq).toString("latin1")),at=end}return keywords}function describeKeywords(keywords){if(keywords===null)return"its records could not be read";if(keywords.length===0)return"it carries no records";let shown=[...new Set(keywords)].map(keyword=>PAX_KEYWORD_RE.test(keyword)?keyword:"<unprintable>").slice(0,PAX_KEYWORDS_IN_MESSAGE),more=new Set(keywords).size-shown.length;return`records ${shown.join(", ")}${more>0?` (+${more} more)`:""}`}function assertDisplayablePath(fullPath){if(hasUnsafeChars(fullPath))throw new TarExtractError(`tar entry path contains a control or format character: ${JSON.stringify(terminalSafe(fullPath,{max:200}))}`)}function describeTypeflag(typeflag){return hasUnsafeChars(typeflag)?`0x${typeflag.charCodeAt(0).toString(16).padStart(2,"0")}`:`'${typeflag}'`}var MAX_PAX_BYTES=64*1024;function assertZeroTail(tar,from){for(let i=from;i<tar.length;i+=1)if(tar[i]!==0)throw new TarExtractError("tar archive has data after its end-of-archive marker.")}function isZeroBlock(block){for(let byte of block)if(byte!==0)return!1;return!0}function checksumOk(block){let stored=readOctal(block,OFF_CHKSUM,LEN_CHKSUM),unsigned=0,signed=0;for(let i=0;i<BLOCK;i+=1){let byte=i>=OFF_CHKSUM&&i<OFF_CHKSUM+LEN_CHKSUM?32:block[i];unsigned+=byte,signed+=byte>127?byte-256:byte}return stored===unsigned||stored===signed}function resolveEntryPath(fullPath,isDirectory,expectedRoot){if(fullPath==="")throw new TarExtractError("tar entry has an empty path.");if(fullPath.startsWith("/"))throw new TarExtractError(`tar entry has an absolute path: ${fullPath}`);let segments=fullPath.replace(/\/+$/,"").split("/");for(let segment of segments)if(segment==="..")throw new TarExtractError(`tar entry escapes the archive root: ${fullPath}`);let root=segments[0];if(root===""||root===".")throw new TarExtractError(`tar entry has no leading component: ${fullPath}`);if(expectedRoot!==null&&root!==expectedRoot)throw new TarExtractError(`tar archive has more than one root component: ${expectedRoot} and ${root}`);let rest=segments.slice(1);if(rest.some(segment=>segment===""||segment==="."))throw new TarExtractError(`tar entry has an empty or "." path segment: ${fullPath}`);if(rest.length===0){if(isDirectory)return{rel:null,root};throw new TarExtractError(`tar entry has no path below the archive root: ${fullPath}`)}return{rel:rest.join("/"),root}}function extractTarGz(archive,destDir,limits){let tar;try{tar=gunzipSync(archive,{maxOutputLength:limits.maxTotalBytes})}catch(err){throw new TarExtractError(`could not decompress the fetched archive: ${err.message}`)}extractTar(tar,destDir,limits)}function extractTar(tar,destDir,limits){let offset=0,entries=0,totalBytes=0,paxBytes=0,expectedRoot=null;for(;offset+BLOCK<=tar.length;){let header=tar.subarray(offset,offset+BLOCK);if(isZeroBlock(header)){assertZeroTail(tar,offset+BLOCK);return}if(!checksumOk(header))throw new TarExtractError("tar header failed its checksum \u2014 the archive is corrupt or truncated.");if(header.toString("latin1",OFF_MAGIC,OFF_MAGIC+5)!=="ustar")throw new TarExtractError('tar header is not a ustar header (missing "ustar" magic).');let size=readOctal(header,OFF_SIZE,LEN_SIZE),dataStart=offset+BLOCK,dataEnd=dataStart+size;if(dataEnd>tar.length)throw new TarExtractError("tar entry runs past the end of the archive \u2014 truncated.");let next=dataStart+Math.ceil(size/BLOCK)*BLOCK,typeflag=String.fromCharCode(header[OFF_TYPEFLAG]);if((typeflag==="x"||typeflag==="g")&&size>MAX_PAX_BYTES)throw new TarExtractError(`tar archive has a pax header of ${size} bytes, over the ${MAX_PAX_BYTES}-byte limit pharn accepts.`);if(typeflag==="x")throw new TarExtractError(`tar archive contains a pax extended header (typeflag 'x'), which pharn does not support \u2014 its records override the next entry's path and size: ${describeKeywords(readPaxKeywords(tar.subarray(dataStart,dataEnd)))}.`);if(typeflag==="g"){if(entries+=1,entries>limits.maxEntries)throw new TarExtractError(`tar archive has more than ${limits.maxEntries} entries.`);if(paxBytes+=size,paxBytes>MAX_PAX_BYTES)throw new TarExtractError(`tar archive's pax global headers total ${paxBytes} bytes, over the ${MAX_PAX_BYTES}-byte limit pharn accepts.`);let keywords=readPaxKeywords(tar.subarray(dataStart,dataEnd));if(keywords===null)throw new TarExtractError("tar archive has a pax global header whose records could not be read.");let overrides=keywords.filter(keyword=>PAX_OVERRIDE_KEYWORDS.has(keyword));if(overrides.length>0)throw new TarExtractError(`tar archive has a pax global header setting a default that overrides every following entry, which pharn does not support: ${describeKeywords(overrides)}.`);offset=next;continue}let name=readPathField(header,OFF_NAME,LEN_NAME),prefix=readPathField(header,OFF_PREFIX,LEN_PREFIX),fullPath=prefix===""?name:`${prefix}/${name}`;assertDisplayablePath(fullPath);let isFile=typeflag==="0"||typeflag==="\0",isDirectory=typeflag==="5";if(!isFile&&!isDirectory)throw new TarExtractError(`tar entry has an unsupported type ${describeTypeflag(typeflag)}: ${fullPath}`);let{rel,root}=resolveEntryPath(fullPath,isDirectory,expectedRoot);if(expectedRoot=root,rel!==null){if(entries+=1,entries>limits.maxEntries)throw new TarExtractError(`tar archive has more than ${limits.maxEntries} entries.`);if(totalBytes+=size,totalBytes>limits.maxTotalBytes)throw new TarExtractError(`tar archive expands past ${limits.maxTotalBytes} bytes.`);let target=safeJoin(destDir,rel);isDirectory?mkdirSync(target,{recursive:!0}):(mkdirSync(dirname2(target),{recursive:!0}),writeFileSync(target,tar.subarray(dataStart,dataEnd)))}offset=next}throw new TarExtractError("tar archive has no end-of-archive marker \u2014 truncated.")}var API="https://api.github.com",CODELOAD="https://codeload.github.com",FETCH_TIMEOUT_MS2=8e3,CLONE_TIMEOUT_MS=6e4,MAX_ARCHIVE_BYTES=32*1024*1024,MAX_EXTRACTED_BYTES=128*1024*1024,MAX_ENTRIES2=2e4,liveClones=new Set,handlersInstalled=!1;function rmQuiet(dir){try{rmSync(dir,{recursive:!0,force:!0})}catch{}}function installCleanupHandlers(){handlersInstalled||(handlersInstalled=!0,process.on("exit",()=>{for(let dir of liveClones)rmQuiet(dir)}),onFatalSignal(()=>{for(let dir of liveClones)rmQuiet(dir);liveClones.clear()}))}async function fetchRepo(){let rawSha=await fetchCommitSha(),sha=rawSha===null?null:assertSafeString(rawSha,"commit SHA",COMMIT_RE),ref=sha??`refs/heads/${REPO_BRANCH}`;installCleanupHandlers();let dir=mkdtempSync(join2(tmpdir(),"pharn-"));liveClones.add(dir);try{let archive=await downloadArchive(ref);extractTarGz(archive,dir,{maxEntries:MAX_ENTRIES2,maxTotalBytes:MAX_EXTRACTED_BYTES})}catch(err){throw liveClones.delete(dir),rmSync(dir,{recursive:!0,force:!0}),err}return{dir,sha,cleanup:()=>{liveClones.delete(dir),rmSync(dir,{recursive:!0,force:!0})}}}async function downloadArchive(ref){let url=`${CODELOAD}/${REPO}/tar.gz/${ref}`;return withDeadline(CLONE_TIMEOUT_MS,()=>new Error(`Timed out downloading ${url} after ${CLONE_TIMEOUT_MS/1e3}s.`),async signal=>{let res=await fetch(url,{redirect:"error",signal});if(!res.ok)throw await discardBody(res),new Error(`Failed to download ${url}: HTTP ${res.status}`);if(!res.body)throw new Error(`Failed to download ${url}: empty response body`);let reader=res.body.getReader();signal.addEventListener("abort",()=>{reader.cancel().catch(()=>{})});let chunks=[],total=0;for(;;){let{done,value}=await reader.read();if(done)break;let buf=Buffer.from(value);if(total+=buf.byteLength,total>MAX_ARCHIVE_BYTES)throw await reader.cancel().catch(()=>{}),new Error(`Refusing ${url}: archive exceeds ${MAX_ARCHIVE_BYTES} bytes.`);chunks.push(buf)}if(signal.aborted)throw new Error(`Timed out downloading ${url} after ${CLONE_TIMEOUT_MS/1e3}s.`);return Buffer.concat(chunks)})}async function fetchCommitSha(){let url=`${API}/repos/${REPO}/commits/${REPO_BRANCH}`;try{return await withDeadline(FETCH_TIMEOUT_MS2,()=>new Error(`Timed out resolving ${url}`),async signal=>{let res=await fetch(url,{redirect:"error",signal,headers:{Accept:"application/vnd.github+json","X-GitHub-Api-Version":"2022-11-28"}});if(!res.ok)return await discardBody(res),null;let body=JSON.parse(await readText(res,signal));return typeof body.sha=="string"?body.sha:null})}catch{return null}}async function discardBody(res){try{await res.body?.cancel()}catch{}}async function readText(res,signal){if(!res.body)return"";let reader=res.body.getReader(),cancel=()=>{reader.cancel().catch(()=>{})};signal.aborted?cancel():signal.addEventListener("abort",cancel,{once:!0});let chunks=[];for(;;){let{done,value}=await reader.read();if(done)break;chunks.push(value)}return Buffer.concat(chunks).toString("utf8")}import{randomBytes}from"node:crypto";import{closeSync as closeSync5,linkSync,openSync as openSync5,readdirSync as readdirSync3,renameSync,rmSync as rmSync2,statSync,writeSync}from"node:fs";import{hostname}from"node:os";import{closeSync as closeSync4,constants as constants2,fstatSync as fstatSync3,openSync as openSync4,readSync as readSync3}from"node:fs";var MAX_LOCAL_FILE_BYTES=16*1024*1024,OPEN_FLAGS2=constants2.O_RDONLY|(constants2.O_NONBLOCK??0),NOT_A_FILE="is not a regular file";function readBoundedFile(path,maxBytes=MAX_LOCAL_FILE_BYTES){let fd;try{fd=openSync4(path,OPEN_FLAGS2)}catch(err){let code=err.code??"EUNKNOWN";return code==="ENOENT"?{kind:"absent"}:code==="EISDIR"?{kind:"unusable",code,reason:NOT_A_FILE}:{kind:"unusable",code,reason:`could not be read (${code})`}}try{let stat=fstatSync3(fd);if(!stat.isFile())return{kind:"unusable",code:stat.isDirectory()?"EISDIR":"not-a-file",reason:NOT_A_FILE};let tooLarge={kind:"unusable",code:"too-large",reason:`is larger than ${formatBytes(maxBytes)}`};if(stat.size>maxBytes)return tooLarge;let chunks=[],total=0,chunk=Buffer.alloc(Math.min(64*1024,maxBytes+1));for(;;){let n=readSync3(fd,chunk,0,chunk.length,null);if(n===0)break;if(total+=n,total>maxBytes)return tooLarge;chunks.push(Buffer.from(chunk.subarray(0,n)))}return{kind:"ok",bytes:Buffer.concat(chunks,total)}}catch(err){let code=err.code??"EUNKNOWN";return{kind:"unusable",code,reason:`could not be read (${code})`}}finally{closeSync4(fd)}}function formatBytes(n){return n%1048576===0&&n>=1048576?`${n/1048576} MiB`:`${n} bytes`}var LOCK_FILE=".pharn.lock",CORPSE_RE=/^\.pharn\.lock\.[0-9]+\.[0-9a-f]{8}$/,STALE_MS=360*60*1e3,MALFORMED_GRACE_MS=10*1e3,ProjectLockedError=class extends Error{constructor(message){super(message),this.name="ProjectLockedError"}},ProjectChangedError=class extends ProjectLockedError{constructor(message){super(message),this.name="ProjectChangedError"}};function lockPath(cwd){return safeJoin(cwd,LOCK_FILE)}function parsePayload(raw){let parsed;try{parsed=JSON.parse(raw)}catch{return null}if(typeof parsed!="object"||parsed===null)return null;let{pid,host,command,startedAt}=parsed;return typeof pid!="number"||!Number.isInteger(pid)||pid<=0||typeof host!="string"||host===""||typeof startedAt!="string"||Number.isNaN(Date.parse(startedAt))?null:{pid,host,command:typeof command=="string"?command:"unknown",startedAt}}function readRawAt(path){let read=readBoundedFile(path);return read.kind==="ok"?read.bytes.toString("utf8"):null}function readPayload(cwd){let raw=readRawAt(lockPath(cwd));return raw===null?null:parsePayload(raw)}function isDead(held){if(held.host!==hostname())return!1;try{return process.kill(held.pid,0),!1}catch(err){return err.code==="ESRCH"}}function lockAgeMs(cwd){try{return Date.now()-statSync(lockPath(cwd)).mtimeMs}catch{return null}}function isStale(held,ageMs){return held===null?ageMs===null||ageMs>=MALFORMED_GRACE_MS:Date.now()-Date.parse(held.startedAt)>STALE_MS?!0:isDead(held)}function refusal(held){return`Another pharn process is writing to this project: ${held===null?"another pharn process":`pharn ${held.command} (pid ${held.pid} on ${held.host}, since ${held.startedAt})`}.
4
+ `)}var CORE_RE=/^(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?$/;function core(version){let match=CORE_RE.exec(version);return match?[Number(match[1]),Number(match[2]),Number(match[3])]:null}function compareVersionCore(a,b){let left=core(a),right=core(b);if(left===null||right===null)return null;for(let i=0;i<3;i++){let l=left[i],r=right[i];if(l!==r)return l>r?1:-1}return 0}async function withDeadline(ms,onTimeout,work){let controller=new AbortController,timer,expired=new Promise((_,reject)=>{timer=setTimeout(()=>{controller.abort(),reject(onTimeout())},ms)}),running=work(controller.signal);running.catch(()=>{});try{return await Promise.race([running,expired])}finally{clearTimeout(timer)}}import{closeSync as closeSync3,existsSync as existsSync3,openSync as openSync3,readFileSync as readFileSync2,readSync as readSync2}from"node:fs";var RAW="https://raw.githubusercontent.com",MAX_MIN_CLI_BYTES=1024,FETCH_TIMEOUT_MS=8e3,MAX_BODY_BYTES=256*1024;function readSkillsVersion(repoDir){let path=safeJoin(repoDir,SKILLS_VERSION_FILE);if(!existsSync3(path))throw new ManifestValidationError(`${SKILLS_VERSION_FILE} is missing in the fetched repo.`);return assertSafeString(readFileSync2(path,"utf8").trim(),SKILLS_VERSION_FILE,VERSION_RE)}function readMinCli(repoDir){let path=safeJoin(repoDir,MIN_CLI_FILE),fd;try{fd=openSync3(path,"r")}catch(err){return err.code==="ENOENT"?{version:null,warning:null}:{version:null,warning:unusableMinCli(err)}}let raw;try{let buf=Buffer.alloc(MAX_MIN_CLI_BYTES+1),bytes=readSync2(fd,buf,0,buf.length,0);if(bytes>MAX_MIN_CLI_BYTES)return{version:null,warning:`${MIN_CLI_FILE} in the fetched repo is too large to be a version; continuing without a minimum-version constraint.`};raw=buf.subarray(0,bytes).toString("utf8")}catch(err){return{version:null,warning:unusableMinCli(err)}}finally{closeSync3(fd)}let value=raw.trim();return VERSION_RE.test(value)?{version:value,warning:null}:{version:null,warning:`${MIN_CLI_FILE} in the fetched repo is not a valid version; continuing without a minimum-version constraint.`}}function unusableMinCli(err){let code=err?.code??(err instanceof Error?err.name:"unknown error");return`${MIN_CLI_FILE} in the fetched repo could not be read (${code}); continuing without a minimum-version constraint.`}function rethrowUnreachable(url){return err=>{let message=err instanceof Error?err.message:String(err),inner=err instanceof Error?err.cause:void 0,cause=inner===void 0?null:inner instanceof Error?inner.message:String(inner);throw new Error(`Could not reach ${url}: ${message}${cause?` (${cause})`:""}`,{cause:err})}}async function fetchRemoteSkillsVersion(){let url=`${RAW}/${REPO}/${REPO_BRANCH}/${SKILLS_VERSION_FILE}`,unreachable=rethrowUnreachable(url);return withDeadline(FETCH_TIMEOUT_MS,()=>new Error(`Could not reach ${url}: This operation was aborted (timed out after ${FETCH_TIMEOUT_MS/1e3}s)`),async signal=>{let res=await fetch(url,{redirect:"error",signal}).catch(unreachable);if(!res.ok)throw new Error(`SKILLS_VERSION fetch failed (${res.status}) from ${url}`);let declared=Number(res.headers.get("content-length"));if(Number.isFinite(declared)&&declared>MAX_BODY_BYTES)throw new Error(`SKILLS_VERSION too large (${declared} bytes) from ${url}`);let text=await readCappedBody(res,url,unreachable,signal);return assertSafeString(text.trim(),SKILLS_VERSION_FILE,VERSION_RE)})}async function readCappedBody(res,url,unreachable,signal){if(!res.body)return"";let reader=res.body.getReader();signal.addEventListener("abort",()=>{reader.cancel().catch(()=>{})});let chunks=[],total=0;for(;;){let{done,value}=await reader.read().catch(unreachable);if(done)break;if(total+=value.byteLength,total>MAX_BODY_BYTES)throw await reader.cancel().catch(()=>{}),new Error(`SKILLS_VERSION too large (${total} bytes) from ${url}`);chunks.push(value)}let merged=new Uint8Array(total),offset=0;for(let chunk of chunks)merged.set(chunk,offset),offset+=chunk.byteLength;return new TextDecoder().decode(merged)}function minCliGate(repoDir,cliVersion){let read=readMinCli(repoDir);if(read.version===null)return{refusal:null,warning:read.warning};let cmp=compareVersionCore(read.version,cliVersion);return cmp===null?{refusal:null,warning:`Could not compare the installed pharn version (${cliVersion}) against the minimum ${REPO_URL} declares (${read.version}); continuing without a minimum-version constraint.`}:cmp<=0?{refusal:null,warning:null}:{refusal:`This pharn is too old for the current ${REPO_URL}: it requires @pharn-dev/pharn v${read.version} or newer, and v${cliVersion} is installed. Upgrade (\`npm install -g @pharn-dev/pharn@latest\`, or run \`npx @pharn-dev/pharn@latest\`), then re-run this command. Nothing was written.`,warning:null}}function resolveCapabilities(archetypes,index){let detected=new Set(archetypes),selected=[],skipped=[];for(let entry of index.capabilities){if(entry.applies==="universal"){selected.push({name:entry.name,role:entry.role,matched:"universal"});continue}let matched=entry.applies.filter(a=>detected.has(a));matched.length>0?selected.push({name:entry.name,role:entry.role,matched}):skipped.push({name:entry.name,role:entry.role,reason:`applies to [${entry.applies.join(", ")}]; detected [${archetypes.join(", ")}]`})}return{selected,skipped}}import{mkdtempSync,rmSync}from"node:fs";import{tmpdir}from"node:os";import{join as join2}from"node:path";var FATAL_SIGNALS=["SIGINT","SIGTERM"],SIGNAL_NUMBER={SIGINT:2,SIGTERM:15},cleanups=new Set,installed=!1;function onFatalSignal(cleanup){install();let entry=()=>cleanup();return cleanups.add(entry),()=>{cleanups.delete(entry)}}function install(){if(!installed){installed=!0;for(let sig of FATAL_SIGNALS)process.on(sig,()=>die(sig))}}function die(sig){let pending=[...cleanups].reverse();cleanups.clear();for(let cleanup of pending)try{cleanup()}catch{}process.removeAllListeners(sig);try{process.kill(process.pid,sig)}catch{process.exit(128+SIGNAL_NUMBER[sig])}}import{gunzipSync}from"node:zlib";import{mkdirSync,writeFileSync}from"node:fs";import{dirname as dirname2}from"node:path";var BLOCK=512,OFF_NAME=0,LEN_NAME=100,OFF_SIZE=124,LEN_SIZE=12,OFF_CHKSUM=148,LEN_CHKSUM=8,OFF_TYPEFLAG=156,OFF_MAGIC=257,OFF_PREFIX=345,LEN_PREFIX=155,TarExtractError=class extends Error{constructor(message){super(message),this.name="TarExtractError"}};function readString(block,offset,length){let raw=block.subarray(offset,offset+length),end=raw.indexOf(0);return raw.subarray(0,end===-1?raw.length:end).toString("latin1")}function describeBytes(bytes,max=200){let out="";for(let byte of bytes.subarray(0,max))out+=byte>=32&&byte<=126&&byte!==92?String.fromCharCode(byte):`\\x${byte.toString(16).padStart(2,"0")}`;return bytes.length>max?`${out}\u2026`:out}var PATH_DECODER=new TextDecoder("utf-8",{fatal:!0,ignoreBOM:!0});function readPathField(block,offset,length){let raw=block.subarray(offset,offset+length),end=raw.indexOf(0),bytes=raw.subarray(0,end===-1?raw.length:end);try{return PATH_DECODER.decode(bytes)}catch{throw new TarExtractError(`tar entry path is not valid UTF-8: "${describeBytes(bytes)}"`)}}function readOctal(block,offset,length){let raw=block.subarray(offset,offset+length);if(raw.length>0&&(raw[0]&128)!==0)throw new TarExtractError("tar header uses the GNU base-256 numeric encoding, which pharn does not accept.");let text=readString(block,offset,length).trim();if(text==="")return 0;if(!/^[0-7]+$/.test(text))throw new TarExtractError(`tar header has a non-octal numeric field: "${describeBytes(Buffer.from(text,"latin1"),32)}"`);return parseInt(text,8)}var PAX_OVERRIDE_KEYWORDS=new Set(["path","linkpath","size"]),PAX_KEYWORD_RE=/^[A-Za-z0-9._-]{1,64}$/,PAX_KEYWORDS_IN_MESSAGE=8;function readPaxKeywords(payload){let keywords=[],at=0;for(;at<payload.length;){let space=payload.indexOf(32,at);if(space===-1)return null;let digits=payload.subarray(at,space).toString("latin1");if(!/^[0-9]{1,10}$/.test(digits))return null;let end=at+Number(digits);if(end>payload.length||end<=space||payload[end-1]!==10)return null;let eq=payload.indexOf(61,space+1);if(eq===-1||eq<space+2||eq>end-2)return null;keywords.push(payload.subarray(space+1,eq).toString("latin1")),at=end}return keywords}function describeKeywords(keywords){if(keywords===null)return"its records could not be read";if(keywords.length===0)return"it carries no records";let shown2=[...new Set(keywords)].map(keyword=>PAX_KEYWORD_RE.test(keyword)?keyword:"<unprintable>").slice(0,PAX_KEYWORDS_IN_MESSAGE),more=new Set(keywords).size-shown2.length;return`records ${shown2.join(", ")}${more>0?` (+${more} more)`:""}`}function assertDisplayablePath(fullPath){if(hasUnsafeChars(fullPath))throw new TarExtractError(`tar entry path contains a control or format character: ${JSON.stringify(terminalSafe(fullPath,{max:200}))}`)}function describeTypeflag(typeflag){return hasUnsafeChars(typeflag)?`0x${typeflag.charCodeAt(0).toString(16).padStart(2,"0")}`:`'${typeflag}'`}var MAX_PAX_BYTES=64*1024;function assertZeroTail(tar,from){for(let i=from;i<tar.length;i+=1)if(tar[i]!==0)throw new TarExtractError("tar archive has data after its end-of-archive marker.")}function isZeroBlock(block){for(let byte of block)if(byte!==0)return!1;return!0}function checksumOk(block){let stored=readOctal(block,OFF_CHKSUM,LEN_CHKSUM),unsigned=0,signed=0;for(let i=0;i<BLOCK;i+=1){let byte=i>=OFF_CHKSUM&&i<OFF_CHKSUM+LEN_CHKSUM?32:block[i];unsigned+=byte,signed+=byte>127?byte-256:byte}return stored===unsigned||stored===signed}function resolveEntryPath(fullPath,isDirectory,expectedRoot){if(fullPath==="")throw new TarExtractError("tar entry has an empty path.");if(fullPath.startsWith("/"))throw new TarExtractError(`tar entry has an absolute path: ${fullPath}`);let segments=fullPath.replace(/\/+$/,"").split("/");for(let segment of segments)if(segment==="..")throw new TarExtractError(`tar entry escapes the archive root: ${fullPath}`);let root=segments[0];if(root===""||root===".")throw new TarExtractError(`tar entry has no leading component: ${fullPath}`);if(expectedRoot!==null&&root!==expectedRoot)throw new TarExtractError(`tar archive has more than one root component: ${expectedRoot} and ${root}`);let rest=segments.slice(1);if(rest.some(segment=>segment===""||segment==="."))throw new TarExtractError(`tar entry has an empty or "." path segment: ${fullPath}`);if(rest.length===0){if(isDirectory)return{rel:null,root};throw new TarExtractError(`tar entry has no path below the archive root: ${fullPath}`)}return{rel:rest.join("/"),root}}function extractTarGz(archive,destDir,limits){let tar;try{tar=gunzipSync(archive,{maxOutputLength:limits.maxTotalBytes})}catch(err){throw new TarExtractError(`could not decompress the fetched archive: ${err.message}`)}extractTar(tar,destDir,limits)}function extractTar(tar,destDir,limits){let offset=0,entries=0,totalBytes=0,paxBytes=0,expectedRoot=null;for(;offset+BLOCK<=tar.length;){let header=tar.subarray(offset,offset+BLOCK);if(isZeroBlock(header)){assertZeroTail(tar,offset+BLOCK);return}if(!checksumOk(header))throw new TarExtractError("tar header failed its checksum \u2014 the archive is corrupt or truncated.");if(header.toString("latin1",OFF_MAGIC,OFF_MAGIC+5)!=="ustar")throw new TarExtractError('tar header is not a ustar header (missing "ustar" magic).');let size=readOctal(header,OFF_SIZE,LEN_SIZE),dataStart=offset+BLOCK,dataEnd=dataStart+size;if(dataEnd>tar.length)throw new TarExtractError("tar entry runs past the end of the archive \u2014 truncated.");let next=dataStart+Math.ceil(size/BLOCK)*BLOCK,typeflag=String.fromCharCode(header[OFF_TYPEFLAG]);if((typeflag==="x"||typeflag==="g")&&size>MAX_PAX_BYTES)throw new TarExtractError(`tar archive has a pax header of ${size} bytes, over the ${MAX_PAX_BYTES}-byte limit pharn accepts.`);if(typeflag==="x")throw new TarExtractError(`tar archive contains a pax extended header (typeflag 'x'), which pharn does not support \u2014 its records override the next entry's path and size: ${describeKeywords(readPaxKeywords(tar.subarray(dataStart,dataEnd)))}.`);if(typeflag==="g"){if(entries+=1,entries>limits.maxEntries)throw new TarExtractError(`tar archive has more than ${limits.maxEntries} entries.`);if(paxBytes+=size,paxBytes>MAX_PAX_BYTES)throw new TarExtractError(`tar archive's pax global headers total ${paxBytes} bytes, over the ${MAX_PAX_BYTES}-byte limit pharn accepts.`);let keywords=readPaxKeywords(tar.subarray(dataStart,dataEnd));if(keywords===null)throw new TarExtractError("tar archive has a pax global header whose records could not be read.");let overrides=keywords.filter(keyword=>PAX_OVERRIDE_KEYWORDS.has(keyword));if(overrides.length>0)throw new TarExtractError(`tar archive has a pax global header setting a default that overrides every following entry, which pharn does not support: ${describeKeywords(overrides)}.`);offset=next;continue}let name=readPathField(header,OFF_NAME,LEN_NAME),prefix=readPathField(header,OFF_PREFIX,LEN_PREFIX),fullPath=prefix===""?name:`${prefix}/${name}`;assertDisplayablePath(fullPath);let isFile=typeflag==="0"||typeflag==="\0",isDirectory=typeflag==="5";if(!isFile&&!isDirectory)throw new TarExtractError(`tar entry has an unsupported type ${describeTypeflag(typeflag)}: ${fullPath}`);let{rel,root}=resolveEntryPath(fullPath,isDirectory,expectedRoot);if(expectedRoot=root,rel!==null){if(entries+=1,entries>limits.maxEntries)throw new TarExtractError(`tar archive has more than ${limits.maxEntries} entries.`);if(totalBytes+=size,totalBytes>limits.maxTotalBytes)throw new TarExtractError(`tar archive expands past ${limits.maxTotalBytes} bytes.`);let target=safeJoin(destDir,rel);isDirectory?mkdirSync(target,{recursive:!0}):(mkdirSync(dirname2(target),{recursive:!0}),writeFileSync(target,tar.subarray(dataStart,dataEnd)))}offset=next}throw new TarExtractError("tar archive has no end-of-archive marker \u2014 truncated.")}var API="https://api.github.com",CODELOAD="https://codeload.github.com",FETCH_TIMEOUT_MS2=8e3,CLONE_TIMEOUT_MS=6e4,MAX_ARCHIVE_BYTES=32*1024*1024,MAX_EXTRACTED_BYTES=128*1024*1024,MAX_ENTRIES2=2e4,liveClones=new Set,handlersInstalled=!1;function rmQuiet(dir){try{rmSync(dir,{recursive:!0,force:!0})}catch{}}function installCleanupHandlers(){handlersInstalled||(handlersInstalled=!0,process.on("exit",()=>{for(let dir of liveClones)rmQuiet(dir)}),onFatalSignal(()=>{for(let dir of liveClones)rmQuiet(dir);liveClones.clear()}))}async function fetchRepo(source){return source.kind==="release"?fetchAt(source,source.sha):fetchAt(source,await fetchCommitSha())}async function fetchAt(source,rawSha){let sha=rawSha===null?null:assertSafeString(rawSha,"commit SHA",COMMIT_RE),ref=sha??`refs/heads/${REPO_BRANCH}`;installCleanupHandlers();let dir=mkdtempSync(join2(tmpdir(),"pharn-"));liveClones.add(dir);try{let archive=await downloadArchive(ref);if(extractTarGz(archive,dir,{maxEntries:MAX_ENTRIES2,maxTotalBytes:MAX_EXTRACTED_BYTES}),source.kind==="release"){let found=readSkillsVersion(dir);if(found!==source.version)throw new ManifestValidationError(`Release ${source.tag} (${source.sha}) ships ${SKILLS_VERSION_FILE} ${found}, not ${source.version}; refusing to install a release whose content does not match its tag.`)}}catch(err){throw liveClones.delete(dir),rmSync(dir,{recursive:!0,force:!0}),err}return{dir,sha,source,cleanup:()=>{liveClones.delete(dir),rmSync(dir,{recursive:!0,force:!0})}}}async function downloadArchive(ref){let url=`${CODELOAD}/${REPO}/tar.gz/${ref}`;return withDeadline(CLONE_TIMEOUT_MS,()=>new Error(`Timed out downloading ${url} after ${CLONE_TIMEOUT_MS/1e3}s.`),async signal=>{let res=await fetch(url,{redirect:"error",signal});if(!res.ok)throw await discardBody(res),new Error(`Failed to download ${url}: HTTP ${res.status}`);if(!res.body)throw new Error(`Failed to download ${url}: empty response body`);let reader=res.body.getReader();signal.addEventListener("abort",()=>{reader.cancel().catch(()=>{})});let chunks=[],total=0;for(;;){let{done,value}=await reader.read();if(done)break;let buf=Buffer.from(value);if(total+=buf.byteLength,total>MAX_ARCHIVE_BYTES)throw await reader.cancel().catch(()=>{}),new Error(`Refusing ${url}: archive exceeds ${MAX_ARCHIVE_BYTES} bytes.`);chunks.push(buf)}if(signal.aborted)throw new Error(`Timed out downloading ${url} after ${CLONE_TIMEOUT_MS/1e3}s.`);return Buffer.concat(chunks)})}async function fetchCommitSha(){let url=`${API}/repos/${REPO}/commits/${REPO_BRANCH}`;try{return await withDeadline(FETCH_TIMEOUT_MS2,()=>new Error(`Timed out resolving ${url}`),async signal=>{let res=await fetch(url,{redirect:"error",signal,headers:{Accept:"application/vnd.github+json","X-GitHub-Api-Version":"2022-11-28"}});if(!res.ok)return await discardBody(res),null;let body=JSON.parse(await readText(res,signal));return typeof body.sha=="string"?body.sha:null})}catch{return null}}async function discardBody(res){try{await res.body?.cancel()}catch{}}async function readText(res,signal){if(!res.body)return"";let reader=res.body.getReader(),cancel=()=>{reader.cancel().catch(()=>{})};signal.aborted?cancel():signal.addEventListener("abort",cancel,{once:!0});let chunks=[];for(;;){let{done,value}=await reader.read();if(done)break;chunks.push(value)}return Buffer.concat(chunks).toString("utf8")}var API2="https://api.github.com",FETCH_TIMEOUT_MS3=8e3,MAX_RELEASE_BYTES=1024*1024,MAX_SHA_BYTES=1024,REF_CHOICES=["main","latest"],ReleaseResolveError=class extends Error{constructor(message,options){super(message,options),this.name="ReleaseResolveError"}};function channelOf(config){return config.ref==="latest"?"latest":"main"}function sourceLabel(source){return`${REPO}@${source.kind==="release"?source.tag:REPO_BRANCH}`}function sourceDescription(source){return source.kind==="release"?`verified release ${source.tag}`:`${REPO_BRANCH} (tip)`}async function resolveSource(ref){return ref==="main"?{kind:"main"}:resolveLatestRelease()}async function resolveLatestRelease(){let releaseUrl=`${API2}/repos/${REPO}/releases/latest`,release2=await get(releaseUrl,"application/vnd.github+json",MAX_RELEASE_BYTES);if(release2.status===404)throw refusal(`${REPO} has published no release yet (HTTP 404 from ${releaseUrl})`);let body=parseRelease(release2,releaseUrl);if(body.draft===!0||body.prerelease===!0)throw refusal(`the release ${releaseUrl} returned is marked ${body.draft===!0?"draft":"prerelease"}`);let tag=body.tag_name,match=typeof tag=="string"?RELEASE_TAG_RE.exec(tag):null;if(typeof tag!="string"||match===null)throw refusal(`the latest release's tag ${shown(tag)} is not a plain vX.Y.Z version`);let shaUrl=`${API2}/repos/${REPO}/commits/${tag}`,commit=await get(shaUrl,"application/vnd.github.sha",MAX_SHA_BYTES);if(commit.text===null)throw refusal(`could not resolve ${tag} to a commit (${httpReason(commit.status,shaUrl)})`);let sha=commit.text.trim();if(!COMMIT_RE.test(sha))throw refusal(`${shaUrl} did not answer with a commit id`);return{kind:"release",tag,version:match[1],sha}}async function get(url,accept,maxBytes){try{return await withDeadline(FETCH_TIMEOUT_MS3,()=>new Error(`timed out after ${FETCH_TIMEOUT_MS3/1e3}s`),async signal=>{let res=await fetch(url,{redirect:"error",signal,headers:{Accept:accept,"X-GitHub-Api-Version":"2022-11-28"}});return res.ok?{status:res.status,text:await readCapped(res,signal,maxBytes,url)}:(await res.body?.cancel().catch(()=>{}),{status:res.status,text:null})})}catch(err){if(err instanceof ReleaseResolveError)throw err;let message=err instanceof Error?err.message:String(err);throw refusal(`could not reach ${url}: ${message}`,err)}}function parseRelease(got,url){if(got.text===null)throw refusal(httpReason(got.status,url));let body;try{body=JSON.parse(got.text)}catch{throw refusal(`${url} did not answer with JSON`)}if(body===null||typeof body!="object"||Array.isArray(body))throw refusal(`${url} did not answer with a release object`);return body}function httpReason(status,url){return status===403||status===429?`GitHub refused the request (HTTP ${status} from ${url}) \u2014 the unauthenticated API allows 60 requests an hour per address`:`HTTP ${status} from ${url}`}async function readCapped(res,signal,maxBytes,url){if(!res.body)return"";let reader=res.body.getReader(),cancel=()=>{reader.cancel().catch(()=>{})};signal.aborted?cancel():signal.addEventListener("abort",cancel,{once:!0});let chunks=[],total=0;for(;;){let{done,value}=await reader.read();if(done)break;if(total+=value.byteLength,total>maxBytes)throw cancel(),refusal(`${url} answered with more than ${maxBytes} bytes`);chunks.push(value)}return Buffer.concat(chunks).toString("utf8")}function shown(value){return typeof value=="string"?JSON.stringify(terminalSafe(value,{max:60})):`(${value===void 0?"missing":typeof value})`}function refusal(reason,cause){return new ReleaseResolveError(`Could not resolve the latest verified ${REPO} release: ${reason}. This project follows verified releases (\`--ref latest\`) \u2014 tags pharn-oss creates once its post-merge CI has passed. To install the tip of main instead (the default channel), run \`pharn init --ref main\` (or \`pharn update --ref main\` for an existing install).`,cause===void 0?void 0:{cause})}import{randomBytes}from"node:crypto";import{closeSync as closeSync5,linkSync,openSync as openSync5,readdirSync as readdirSync3,renameSync,rmSync as rmSync2,statSync,writeSync}from"node:fs";import{hostname}from"node:os";import{closeSync as closeSync4,constants as constants2,fstatSync as fstatSync3,openSync as openSync4,readSync as readSync3}from"node:fs";var MAX_LOCAL_FILE_BYTES=16*1024*1024,OPEN_FLAGS2=constants2.O_RDONLY|(constants2.O_NONBLOCK??0),NOT_A_FILE="is not a regular file";function readBoundedFile(path,maxBytes=MAX_LOCAL_FILE_BYTES){let fd;try{fd=openSync4(path,OPEN_FLAGS2)}catch(err){let code=err.code??"EUNKNOWN";return code==="ENOENT"?{kind:"absent"}:code==="EISDIR"?{kind:"unusable",code,reason:NOT_A_FILE}:{kind:"unusable",code,reason:`could not be read (${code})`}}try{let stat=fstatSync3(fd);if(!stat.isFile())return{kind:"unusable",code:stat.isDirectory()?"EISDIR":"not-a-file",reason:NOT_A_FILE};let tooLarge={kind:"unusable",code:"too-large",reason:`is larger than ${formatBytes(maxBytes)}`};if(stat.size>maxBytes)return tooLarge;let chunks=[],total=0,chunk=Buffer.alloc(Math.min(64*1024,maxBytes+1));for(;;){let n=readSync3(fd,chunk,0,chunk.length,null);if(n===0)break;if(total+=n,total>maxBytes)return tooLarge;chunks.push(Buffer.from(chunk.subarray(0,n)))}return{kind:"ok",bytes:Buffer.concat(chunks,total)}}catch(err){let code=err.code??"EUNKNOWN";return{kind:"unusable",code,reason:`could not be read (${code})`}}finally{closeSync4(fd)}}function formatBytes(n){return n%1048576===0&&n>=1048576?`${n/1048576} MiB`:`${n} bytes`}var LOCK_FILE=".pharn.lock",CORPSE_RE=/^\.pharn\.lock\.[0-9]+\.[0-9a-f]{8}$/,STALE_MS=360*60*1e3,MALFORMED_GRACE_MS=10*1e3,ProjectLockedError=class extends Error{constructor(message){super(message),this.name="ProjectLockedError"}},ProjectChangedError=class extends ProjectLockedError{constructor(message){super(message),this.name="ProjectChangedError"}};function lockPath(cwd){return safeJoin(cwd,LOCK_FILE)}function parsePayload(raw){let parsed;try{parsed=JSON.parse(raw)}catch{return null}if(typeof parsed!="object"||parsed===null)return null;let{pid,host,command,startedAt}=parsed;return typeof pid!="number"||!Number.isInteger(pid)||pid<=0||typeof host!="string"||host===""||typeof startedAt!="string"||Number.isNaN(Date.parse(startedAt))?null:{pid,host,command:typeof command=="string"?command:"unknown",startedAt}}function readRawAt(path){let read=readBoundedFile(path);return read.kind==="ok"?read.bytes.toString("utf8"):null}function readPayload(cwd){let raw=readRawAt(lockPath(cwd));return raw===null?null:parsePayload(raw)}function isDead(held){if(held.host!==hostname())return!1;try{return process.kill(held.pid,0),!1}catch(err){return err.code==="ESRCH"}}function lockAgeMs(cwd){try{return Date.now()-statSync(lockPath(cwd)).mtimeMs}catch{return null}}function isStale(held,ageMs){return held===null?ageMs===null||ageMs>=MALFORMED_GRACE_MS:Date.now()-Date.parse(held.startedAt)>STALE_MS?!0:isDead(held)}function refusal2(held){return`Another pharn process is writing to this project: ${held===null?"another pharn process":`pharn ${held.command} (pid ${held.pid} on ${held.host}, since ${held.startedAt})`}.
5
5
  Wait for it to finish, or delete ${LOCK_FILE} if you are sure no pharn process is running.`}function tryCreate(cwd,command){let fd;try{fd=openSync5(lockPath(cwd),"wx")}catch(err){if(err.code==="EEXIST")return!1;throw err}try{let payload={pid:process.pid,host:hostname(),command,startedAt:new Date().toISOString()};writeSync(fd,`${JSON.stringify(payload,null,2)}
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
- `)}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.
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(refusal2(readPayload(cwd)))}try{if(readRawAt(corpse)!==observedRaw){try{linkSync(corpse,lockPath(cwd))}catch{}throw new ProjectLockedError(refusal2(readPayload(cwd)))}if(!tryCreate(cwd,command))throw new ProjectLockedError(refusal2(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(refusal2(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
+ `)}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 shown2=`${notice.name} is set (${redactProxyUrl(notice.value)})`;if(notice.envProxy==="on")return`${shown2} 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`${shown2}, but pharn will not use it: Node reads only https_proxy, HTTPS_PROXY, http_proxy and HTTP_PROXY, spelled exactly so. ${direct}`;let base=`${shown2}, 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,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(`
9
+ Then re-run: npx @pharn-dev/pharn init`)}function fail(message){logError(message),process.exit(1)}var NODE_FLOOR="24.2.0";function nodeFloorStatus(current){if(typeof current!="string")return{kind:"unparseable",raw:current};let cmp=compareVersionCore(current,NODE_FLOOR);return cmp===null?{kind:"unparseable",raw:current}:cmp<0||cmp===0&&current.includes("-")?{kind:"below",current}:{kind:"ok",current}}function nodeFloorRefusal(status,command){if(status.kind==="ok")return null;if(status.kind==="below")return`pharn ${command} requires Node ${NODE_FLOOR} or newer, and this is Node ${terminalSafe(status.current,{max:40})}. The PHARN floor checks it installs refuse to run on older Node, so an install made here would fail mid-run. Upgrade Node, then re-run: npx @pharn-dev/pharn ${command}. Nothing was written.`;let shown2=status.raw===void 0?"no version reported":JSON.stringify(terminalSafe(status.raw,{max:40}));return`pharn ${command} requires Node ${NODE_FLOOR} or newer, and could not read this Node's version (${shown2}). It will not continue on a version it cannot verify. Run it on Node ${NODE_FLOOR} or newer. Nothing was written.`}function nodeFloorLines(status){let running=status.kind==="unparseable"?status.raw===void 0?"(not reported)":JSON.stringify(terminalSafe(status.raw,{max:40})):`v${terminalSafe(status.current,{max:40})}`,lines=[` ${"Node floor".padEnd(26)}>= ${NODE_FLOOR}`,` ${"Node (running)".padEnd(26)}${running}`];return status.kind==="ok"||lines.push("",status.kind==="below"?` MISMATCH \u2014 below the floor. PHARN's floor checks refuse to run on this Node (exit 2); use Node ${NODE_FLOOR} or newer.`:` MISMATCH \u2014 this Node's version could not be verified against the floor. Use Node ${NODE_FLOOR} or newer.`),lines}function runNodePrereq(command){let refusal3=nodeFloorRefusal(nodeFloorStatus(process.versions.node),command);refusal3!==null&&(logError(refusal3),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)),shown2=ordered.slice(0,MAX_LISTED2),more=ordered.length-shown2.length,list=shown2.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(`
@@ -14,20 +14,21 @@ ${counts.map(([label,n])=>labelLine(label,n)).join(`
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
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(`
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:
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 shown2=[...items].sort();return shown2.length>MAX_LINKED_SHOWN?`${shown2.slice(0,MAX_LINKED_SHOWN).join(", ")} and ${shown2.length-MAX_LINKED_SHOWN} more`:shown2.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,ref:!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.ref!==void 0&&config.ref!=="latest"&&delete config.ref,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,...carry.ref==="latest"?{ref:"latest"}:{},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(`
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(opts={}){showBanner(),intro("init wizard"),runNodePrereq("init"),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(opts.ref??"main")}async function runInitArchetype(channel){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(await resolveSource(channel))}catch(err){s.stop("Failed to fetch PHARN"),reportFatal(err instanceof ReleaseResolveError?err.message:`Could not reach ${REPO_URL}: ${errorMessage(err)}`,{err}),process.exit(1)}s.stop(`PHARN fetched from ${sourceLabel(repo.source)}`);let outcome="cancelled",failure=null,refusal3=null;try{let gate=minCliGate(repo.dir,PHARN_VERSION);if(gate.warning&&log5.warn(gate.warning),gate.refusal)refusal3=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,channel==="latest"?{...carry,ref:"latest"}:carry,manifest))),outcome="installed"}}}catch(err){err instanceof ProjectLockedError?refusal3=err.message:failure={err}}finally{repo.cleanup()}refusal3&&(reportFatal(refusal3),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,source){let fetched=readSkillsVersion(repoDir);if(fetched===config.skillsVersion||fetched===config.pendingSkillsVersion)return null;if(source.kind==="release"&&compareVersionCore(config.skillsVersion,fetched)===1)return`Your install (skills v${config.skillsVersion}) is ahead of the latest verified ${REPO_URL} release (${source.tag}) \u2014 it came from pharn-oss main, and \`pharn add\` installs only at the version your project is already on. Run \`pharn update --ref main\` to follow main (\`pharn add\` then fetches from it too), or wait for a release at v${config.skillsVersion}.`;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(await resolveSource(channelOf(config))),s.stop(`Capabilities fetched from ${sourceLabel(repo.source)}`)}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 refusal3=gate.refusal??versionGate(repo.dir,config,repo.source)??layoutGate(repo.dir,config);return refusal3?{kind:"error",message:refusal3}: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(await resolveSource(channelOf(config))),s.stop(`Capabilities fetched from ${sourceLabel(repo.source)}`)}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 refusal3=gate.refusal??versionGate(repo.dir,config,repo.source)??layoutGate(repo.dir,config);return refusal3?{kind:"error",message:refusal3}: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:
20
20
  ${valid.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(`
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"),runNodePrereq("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,opts.ref??channelOf(config))}async function runArchetypeUpdate(config,cwd,force,yes,channel){let proxyNotice=detectProxyNotice(process.env);proxyNotice&&log8.warn(proxyNoticeMessage(proxyNotice));let s=spinner4();s.start("Checking for updates");let latest,source;try{source=await resolveSource(channel),latest=source.kind==="release"?source.version:await fetchRemoteSkillsVersion(),s.stop(`Latest skills v${latest} (${sourceDescription(source)})`)}catch(err){s.stop("Failed to check for updates"),reportFatal(errorMessage(err),{err}),process.exit(1)}if(source.kind==="release"&&!force&&compareVersionCore(config.skillsVersion,latest)===1){outro4(`Your install (skills v${config.skillsVersion}) is ahead of the latest verified release (${source.tag}) \u2014 it came from pharn-oss main. Nothing changed. \`pharn update --ref main\` switches back to main; \`pharn update --force\` goes back to ${source.tag}.`);return}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),switchChannel=channel!==channelOf(config);if(current&&!force&&!recheckFrozen&&!convertModels&&!switchChannel){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("Source",sourceDescription(source)),"",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(source);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,channel,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 refusal3=refusalRef.current;refusal3&&(reportFatal(refusal3),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,channel,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,ref:_previousRef,...rest}=config;return await writePharnConfig(cwd,{...rest,skillsVersion:nextSkillsVersion,commit:nextCommit,...channel==="latest"?{ref:"latest"}:{},...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(`
23
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(`
24
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(`
25
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
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(`
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
+ `),"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}}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;note5(nodeFloorLines(nodeFloorStatus(process.versions.node)).join(`
28
+ `),"NODE");let proxyNotice=detectProxyNotice(process.env);proxyNotice&&log9.warn(proxyNoticeMessage(proxyNotice));let channel=channelOf(config);if(!drift){let s2=spinner5();s2.start("Checking for updates");let latest,source;try{source=await resolveSource(channel),latest=source.kind==="release"?source.version: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,source);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 ${REPO}`);let repo;try{repo=await fetchRepo(await resolveSource(channel)),s.stop(`Compared against ${sourceLabel(repo.source)}`)}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),repo.source);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,sourceLabel(repo.source));let hooks=diffHookWiring(repo.dir,cwd),hookLines=hookWiringLines(hooks);hookLines&&note5(hookLines.join(`
29
+ `),"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,source){let ahead=source.kind==="release"&&compareVersionCore(config.skillsVersion,latest)===1,outdated=!ahead&&config.skillsVersion!==latest,skillsLine=ahead?`${row("Skills version",`v${config.skillsVersion}`)} ${pc7.dim(`(ahead of the latest verified release v${latest} \u2014 installed from main)`)}`: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("Source",sourceDescription(source)),"",row("Archetypes",(config.archetypes??[]).join(", ")||"(none)"),row("Capabilities",String((config.capabilities??[]).length))].join(`
29
30
  `),"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(`
31
+ `),"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,ref){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(`
31
32
  `),"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.
32
33
 
33
34
  Usage:
@@ -43,12 +44,14 @@ Commands:
43
44
 
44
45
  Options (each belongs to ONE command; passing it to another is an error):
45
46
  --archetype init: deprecated no-op \u2014 archetype detection is now the default
47
+ --ref <ref> init, update: install from "main" (the pharn-oss tip \u2014 the default)
48
+ or "latest" (its newest verified release); recorded
46
49
  --force update: overwrite files you changed (each is copied to .pharn-backup/ first)
47
50
  -y, --yes update: skip the confirmation prompt (for CI and scripts)
48
51
  --strict status: exit 1 on any outdated/modified/missing file
49
52
  --no-drift status: skip the byte-level drift check
50
53
  --json list: emit the inventory as JSON
51
54
  -h, --help Show this help text
52
- -v, --version Show the version number`,MAX_POSITIONALS=new Map([["init",1],["update",1],["list",1],["status",1],["add",2],["remove",2],["rm",2]]),ALLOWED_FLAGS=new Map([["init",["archetype"]],["add",[]],["remove",[]],["rm",[]],["update",["force","yes"]],["list",["json"]],["status",["strict","drift"]]]),GLOBAL_FLAGS=["help","version"],ALIASES={h:"help",v:"version",y:"yes"},DECLARED_BOOLEANS=[...new Set([...GLOBAL_FLAGS,...Array.from(ALLOWED_FLAGS.values()).flat()])];function collectFlags(offenders){return arg=>arg.startsWith("-")?(offenders.push(arg),!1):!0}function aliasesFor(declared){return Object.fromEntries(Object.entries(ALIASES).filter(([,canonical])=>declared.includes(canonical)))}function unsupportedFlags(allowed){let offenders=[],declared=[...GLOBAL_FLAGS,...allowed];return minimist(process.argv.slice(2),{boolean:declared,alias:aliasesFor(declared),unknown:collectFlags(offenders)}),offenders}function refuse(label,offenders){console.error(`${label}: ${[...new Set(offenders)].map(o=>JSON.stringify(o)).join(", ")}
53
- `),console.error(USAGE),process.exit(1)}async function main(){let unknownOptions=[],argv=minimist(process.argv.slice(2),{boolean:DECLARED_BOOLEANS,string:["_"],default:{drift:!0},alias:ALIASES,unknown:collectFlags(unknownOptions)});unknownOptions.length>0&&refuse("Unknown option",unknownOptions);let cmd=argv._[0]??"init",allowedFlags=ALLOWED_FLAGS.get(cmd);if(allowedFlags!==void 0){let unsupported=unsupportedFlags(allowedFlags);unsupported.length>0&&refuse(`Unsupported option for \`${cmd}\``,unsupported)}if(argv.version){console.log(PHARN_VERSION);return}if(argv.help){console.log(USAGE);return}let maxPositionals=MAX_POSITIONALS.get(cmd);switch(maxPositionals!==void 0&&argv._.length>maxPositionals&&refuse("Unexpected argument",argv._.slice(maxPositionals)),cmd){case"init":await runInit();return;case"add":await runAdd(argv._[1]);return;case"remove":case"rm":await runRemove(argv._[1]);return;case"update":await runUpdate({force:!!argv.force,yes:!!argv.yes});return;case"list":await runList({json:!!argv.json});return;case"status":await runStatus({strict:!!argv.strict,drift:argv.drift!==!1});return;default:console.error(`Unknown command: ${cmd}
54
- `),console.error(USAGE),process.exit(1)}}function isEntryPoint(){if(!process.argv[1])return!1;try{return realpathSync(process.argv[1])===fileURLToPath(import.meta.url)}catch{return!1}}isEntryPoint()&&main().catch(err=>{console.error(err),process.exit(1)});export{main};
55
+ -v, --version Show the version number`,MAX_POSITIONALS=new Map([["init",1],["update",1],["list",1],["status",1],["add",2],["remove",2],["rm",2]]),ALLOWED_FLAGS=new Map([["init",["archetype","ref"]],["add",[]],["remove",[]],["rm",[]],["update",["force","yes","ref"]],["list",["json"]],["status",["strict","drift"]]]),GLOBAL_FLAGS=["help","version"],ALIASES={h:"help",v:"version",y:"yes"},STRING_FLAGS=new Set(["ref"]),DECLARED_FLAGS=[...new Set([...GLOBAL_FLAGS,...Array.from(ALLOWED_FLAGS.values()).flat()])],DECLARED_BOOLEANS=DECLARED_FLAGS.filter(f=>!STRING_FLAGS.has(f)),DECLARED_STRINGS=DECLARED_FLAGS.filter(f=>STRING_FLAGS.has(f));function collectFlags(offenders){return arg=>arg.startsWith("-")?(offenders.push(arg),!1):!0}function aliasesFor(declared){return Object.fromEntries(Object.entries(ALIASES).filter(([,canonical])=>declared.includes(canonical)))}function unsupportedFlags(allowed){let offenders=[],declared=[...GLOBAL_FLAGS,...allowed];return minimist(process.argv.slice(2),{boolean:declared.filter(f=>!STRING_FLAGS.has(f)),string:declared.filter(f=>STRING_FLAGS.has(f)),alias:aliasesFor(declared),unknown:collectFlags(offenders)}),offenders}function refuse(label,offenders){console.error(`${label}: ${[...new Set(offenders)].map(o=>JSON.stringify(o)).join(", ")}
56
+ `),console.error(USAGE),process.exit(1)}async function main(){let unknownOptions=[],argv=minimist(process.argv.slice(2),{boolean:DECLARED_BOOLEANS,string:["_",...DECLARED_STRINGS],default:{drift:!0},alias:ALIASES,unknown:collectFlags(unknownOptions)});unknownOptions.length>0&&refuse("Unknown option",unknownOptions);let cmd=argv._[0]??"init",allowedFlags=ALLOWED_FLAGS.get(cmd);if(allowedFlags!==void 0){let unsupported=unsupportedFlags(allowedFlags);unsupported.length>0&&refuse(`Unsupported option for \`${cmd}\``,unsupported)}let ref=refOption(argv.ref);if(ref===null&&refuse(`Invalid value for --ref (expected ${REF_CHOICES.map(r=>`"${r}"`).join(" or ")})`,[typeof argv.ref=="string"?argv.ref:JSON.stringify(argv.ref)]),argv.version){console.log(PHARN_VERSION);return}if(argv.help){console.log(USAGE);return}let maxPositionals=MAX_POSITIONALS.get(cmd);switch(maxPositionals!==void 0&&argv._.length>maxPositionals&&refuse("Unexpected argument",argv._.slice(maxPositionals)),cmd){case"init":await runInit({ref});return;case"add":await runAdd(argv._[1]);return;case"remove":case"rm":await runRemove(argv._[1]);return;case"update":await runUpdate({force:!!argv.force,yes:!!argv.yes,ref});return;case"list":await runList({json:!!argv.json});return;case"status":await runStatus({strict:!!argv.strict,drift:argv.drift!==!1});return;default:console.error(`Unknown command: ${cmd}
57
+ `),console.error(USAGE),process.exit(1)}}function refOption(value){if(value!==void 0)return REF_CHOICES.find(choice=>choice===value)??null}function isEntryPoint(){if(!process.argv[1])return!1;try{return realpathSync(process.argv[1])===fileURLToPath(import.meta.url)}catch{return!1}}isEntryPoint()&&main().catch(err=>{console.error(err),process.exit(1)});export{main};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pharn-dev/pharn",
3
- "version": "0.7.1",
3
+ "version": "0.9.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",
@@ -27,7 +27,7 @@
27
27
  "dist"
28
28
  ],
29
29
  "engines": {
30
- "node": ">=20.13.0"
30
+ "node": ">=24.2.0"
31
31
  },
32
32
  "publishConfig": {
33
33
  "access": "public",