@pharn-dev/pharn 0.4.0 → 0.5.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.
- package/README.md +4 -4
- package/dist/index.js +5 -5
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -143,7 +143,7 @@ still understood through the recorded `layout` field.
|
|
|
143
143
|
| `pharn/CONSTITUTION.md`, `pharn/ARCHITECTURE.md` | Trusted PHARN docs copied from upstream in the current layout. |
|
|
144
144
|
| `THREAT-MODEL.md`, `LIMITS.md` | Trusted PHARN docs copied at the project root in the current layout. |
|
|
145
145
|
| `pharn/LICENSE` (flat: `PHARN-LICENSE`) | PHARN's Apache-2.0 license copy. Your root `LICENSE` is never overwritten. |
|
|
146
|
-
| `features/README.md` | The feature-loop boundary contract referenced by installed commands. |
|
|
146
|
+
| `pharn/features/README.md` | The feature-loop boundary contract referenced by installed commands (pharn layout). |
|
|
147
147
|
| `pharn.config.json` | Installed version, commit, layout, archetypes, capabilities, and defaults. |
|
|
148
148
|
| `pharn.records.json` | Per-file hashes for drift-safe updates. Commit this with the config. |
|
|
149
149
|
|
|
@@ -151,7 +151,7 @@ An existing `.claude/settings.json` is preserved. PHARN may create it when
|
|
|
151
151
|
absent, but it never overwrites your Claude Code settings.
|
|
152
152
|
|
|
153
153
|
As you run the workflow, PHARN writes one directory per increment —
|
|
154
|
-
`features/<name>/` — holding that increment's `SPEC.md`, `PLAN.md`, `GRILL.md`,
|
|
154
|
+
`pharn/features/<name>/` — holding that increment's `SPEC.md`, `PLAN.md`, `GRILL.md`,
|
|
155
155
|
`BUILD.md`, `REGRESSION.md`, `VERIFY.md` and `SHIP.md`. Those are the durable
|
|
156
156
|
record; commit them. `.pharn/` (runtime scratch), `.pharn-backup/` (see
|
|
157
157
|
[Safety model](#safety-model)) and `.pharn.lock` are not — add them to your
|
|
@@ -173,8 +173,8 @@ instead of overwriting it — so until you copy the hook wiring across, every
|
|
|
173
173
|
guarantee that depends on a `PreToolUse` hook is inactive.
|
|
174
174
|
|
|
175
175
|
**Once wired, the write guard is fail-closed.** With no active scope, Claude
|
|
176
|
-
Code's Write/Edit/MultiEdit/NotebookEdit tools are restricted to
|
|
177
|
-
and `.pharn/**`; ordinary edits to your own source are denied. That is the
|
|
176
|
+
Code's Write/Edit/MultiEdit/NotebookEdit tools are restricted to
|
|
177
|
+
`pharn/features/**` and `.pharn/**`; ordinary edits to your own source are denied. That is the
|
|
178
178
|
intended posture — a stage sets the scope from the concrete paths your
|
|
179
179
|
`PLAN.md` declared — but it means the guard is not a drop-in for editing
|
|
180
180
|
outside a PHARN run. Clearing the scope returns to this default; it does not
|
package/dist/index.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
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 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(message,{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",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{existsSync,readFileSync,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"]),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);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&&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}function readPackageSignals(cwd){let empty=packageSignals({}),pkgPath=resolve(cwd,"package.json");if(!existsSync(pkgPath))return{pkgSig:empty,packageJsonFound:!1};try{let parsed=JSON.parse(readFileSync(pkgPath,"utf8"));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,installed){let isInstalled=c=>installed.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:[...installed]}}function buildRemoveSelection(installed){return{groups:groupByRole(installed)}}function interactiveAllowed(streams){return!!streams.stdinIsTTY&&!!streams.stdoutIsTTY}import{existsSync as existsSync3,readFileSync as readFileSync2,readdirSync as readdirSync2}from"node:fs";import{existsSync as existsSync2}from"node:fs";import{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 toPosix(rel){return rel.split(sep).join("/").replace(/\/+$/,"")}function detectLayout(rootDir){return existsSync2(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,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,license:{from:UPSTREAM_LICENSE,to:FLAT_LICENSE_DEST}}}function configLayout(config){return config.layout==="pharn"?"pharn":"flat"}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(!existsSync3(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`);if(!existsSync3(capFile))throw new ManifestValidationError(`Capability "${name}" in ${subtree.dir} is missing its markdown ${name}/${name}.md.`);let frontmatter=extractFrontmatter(readFileSync2(capFile,"utf8"),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}}function extractFrontmatter(content,name){let match=/^---\n([\s\S]*?)\n---/.exec(content);if(!match)throw new ManifestValidationError(`Capability "${name}" is missing a "---"-fenced frontmatter block.`);return match[1]}function readField(frontmatter,field,name){let match=new RegExp(`^${field}:[ \\t]*(.+?)[ \\t]*$`,"m").exec(frontmatter);if(!match)throw new ManifestValidationError(`Capability "${name}" is missing the "${field}" frontmatter field.`);return stripQuotes(match[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 CONTROL_CHARS_RE2=/[\x00-\x1f\x7f-\x9f]/g;function safe(value){let stripped=value.replace(CONTROL_CHARS_RE2,"");return stripped.length>200?`${stripped.slice(0,200)}\u2026`:stripped}function unknownCapabilitiesWarning(unknown){if(unknown.length===0)return null;let listed=unknown.slice(0,10),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(`
|
|
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 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(message,{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{existsSync,readFileSync,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"]),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);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&&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}function readPackageSignals(cwd){let empty=packageSignals({}),pkgPath=resolve(cwd,"package.json");if(!existsSync(pkgPath))return{pkgSig:empty,packageJsonFound:!1};try{let parsed=JSON.parse(readFileSync(pkgPath,"utf8"));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,installed){let isInstalled=c=>installed.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:[...installed]}}function buildRemoveSelection(installed){return{groups:groupByRole(installed)}}function interactiveAllowed(streams){return!!streams.stdinIsTTY&&!!streams.stdoutIsTTY}import{existsSync as existsSync3,readFileSync as readFileSync2,readdirSync as readdirSync2}from"node:fs";import{existsSync as existsSync2}from"node:fs";import{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 toPosix(rel){return rel.split(sep).join("/").replace(/\/+$/,"")}function detectLayout(rootDir){return existsSync2(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 configLayout(config){return config.layout==="pharn"?"pharn":"flat"}function resolveFeaturesReadme(repoDir,layout){if(layout==="pharn"){let preferred=safeJoin(repoDir,PHARN_FEATURES_README);if(!existsSync2(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(!existsSync3(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`);if(!existsSync3(capFile))throw new ManifestValidationError(`Capability "${name}" in ${subtree.dir} is missing its markdown ${name}/${name}.md.`);let frontmatter=extractFrontmatter(readFileSync2(capFile,"utf8"),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}}function extractFrontmatter(content,name){let match=/^---\n([\s\S]*?)\n---/.exec(content);if(!match)throw new ManifestValidationError(`Capability "${name}" is missing a "---"-fenced frontmatter block.`);return match[1]}function readField(frontmatter,field,name){let match=new RegExp(`^${field}:[ \\t]*(.+?)[ \\t]*$`,"m").exec(frontmatter);if(!match)throw new ManifestValidationError(`Capability "${name}" is missing the "${field}" frontmatter field.`);return stripQuotes(match[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 CONTROL_CHARS_RE2=/[\x00-\x1f\x7f-\x9f]/g;function safe(value){let stripped=value.replace(CONTROL_CHARS_RE2,"");return stripped.length>200?`${stripped.slice(0,200)}\u2026`:stripped}function unknownCapabilitiesWarning(unknown){if(unknown.length===0)return null;let listed=unknown.slice(0,10),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(`
|
|
3
3
|
`)}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}import{closeSync,existsSync as existsSync4,openSync,readFileSync as readFileSync3,readSync}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(!existsSync4(path))throw new ManifestValidationError(`${SKILLS_VERSION_FILE} is missing in the fetched repo.`);return assertSafeString(readFileSync3(path,"utf8").trim(),SKILLS_VERSION_FILE,VERSION_RE)}function readMinCli(repoDir){let path=safeJoin(repoDir,MIN_CLI_FILE),fd;try{fd=openSync(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=readSync(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{closeSync(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),controller=new AbortController,timer=setTimeout(()=>controller.abort(),FETCH_TIMEOUT_MS);try{let res=await fetch(url,{redirect:"error",signal:controller.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);return assertSafeString(text.trim(),SKILLS_VERSION_FILE,VERSION_RE)}finally{clearTimeout(timer)}}async function readCappedBody(res,url,unreachable){if(!res.body)return"";let reader=res.body.getReader(),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";import{gunzipSync}from"node:zlib";import{mkdirSync,writeFileSync}from"node:fs";import{dirname}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_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 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: ${text}`);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 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.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,expectedRoot=null;for(;offset+BLOCK<=tar.length;){let header=tar.subarray(offset,offset+BLOCK);if(isZeroBlock(header))break;if(!checksumOk(header))throw new TarExtractError("tar header failed its checksum \u2014 the archive is corrupt or truncated.");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")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"){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 isFile=typeflag==="0"||typeflag==="\0",isDirectory=typeflag==="5";if(!isFile&&!isDirectory)throw new TarExtractError(`tar entry has an unsupported type '${typeflag}': ${readString(header,OFF_NAME,LEN_NAME)}`);let name=readString(header,OFF_NAME,LEN_NAME),prefix=readString(header,OFF_PREFIX,LEN_PREFIX),fullPath=prefix===""?name:`${prefix}/${name}`,{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(dirname(target),{recursive:!0}),writeFileSync(target,tar.subarray(dataStart,dataEnd)))}offset=next}}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(){if(!handlersInstalled){handlersInstalled=!0,process.on("exit",()=>{for(let dir of liveClones)rmQuiet(dir)});for(let sig of["SIGINT","SIGTERM"])process.on(sig,()=>{for(let dir of liveClones)rmQuiet(dir);liveClones.clear(),process.removeAllListeners(sig),process.kill(process.pid,sig)})}}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}`,controller=new AbortController,timer=setTimeout(()=>controller.abort(),CLONE_TIMEOUT_MS);try{let res=await fetch(url,{redirect:"error",signal:controller.signal});if(!res.ok)throw new Error(`Failed to download ${url}: HTTP ${res.status}`);if(!res.body)throw new Error(`Failed to download ${url}: empty response body`);let chunks=[],total=0;for await(let chunk of res.body){let buf=Buffer.from(chunk);if(total+=buf.byteLength,total>MAX_ARCHIVE_BYTES)throw new Error(`Refusing ${url}: archive exceeds ${MAX_ARCHIVE_BYTES} bytes.`);chunks.push(buf)}return Buffer.concat(chunks)}finally{clearTimeout(timer)}}async function fetchCommitSha(){let url=`${API}/repos/${REPO}/commits/${REPO_BRANCH}`,controller=new AbortController,timer=setTimeout(()=>controller.abort(),FETCH_TIMEOUT_MS2);try{let res=await fetch(url,{redirect:"error",signal:controller.signal,headers:{Accept:"application/vnd.github+json","X-GitHub-Api-Version":"2022-11-28"}});if(!res.ok)return null;let body=await res.json();return typeof body.sha=="string"?body.sha:null}catch{return null}finally{clearTimeout(timer)}}import{randomBytes}from"node:crypto";import{closeSync as closeSync2,linkSync,openSync as openSync2,readFileSync as readFileSync4,readdirSync as readdirSync3,renameSync,rmSync as rmSync2,statSync,writeSync}from"node:fs";import{hostname}from"node:os";var LOCK_FILE=".pharn.lock",CORPSE_RE=/^\.pharn\.lock\.[0-9]+\.[0-9a-f]{8}$/,STALE_MS=360*60*1e3,ProjectLockedError=class extends Error{constructor(message){super(message),this.name="ProjectLockedError"}};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){try{return readFileSync4(path,"utf8")}catch{return 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 isStale(held){return held===null||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
4
|
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=openSync2(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)}
|
|
5
5
|
`)}finally{closeSync2(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))throw new ProjectLockedError(refusal(held));breakStaleLock(cwd,command,observedRaw)}try{return await fn()}finally{release(cwd)}}var LOWER="https_proxy",UPPER="HTTPS_PROXY";function isSet(value){return value!==void 0&&value!==""}function proxyVariantKeys(env){let keys=Object.keys(env).filter(key2=>key2.toLowerCase()===LOWER&&isSet(env[key2])).sort();return keys.includes(UPPER)?[UPPER,...keys.filter(k=>k!==UPPER)]:keys}function detectProxyNotice(env){let name=proxyVariantKeys(env)[0];if(name===void 0)return null;let value=env[name];return isSet(value)?{name,value}:null}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){return`${notice.name} is set (${redactProxyUrl(notice.value)}), but pharn will not use it: its network calls go through Node's global fetch, which reads no proxy environment variable on any platform. The download connects DIRECTLY, and fails if direct egress is blocked (LIMITS.md \xA73a).`}import{existsSync as existsSync5}from"node:fs";import{resolve as resolve3}from"node:path";function runGitPrereq(){let cwd=process.cwd();existsSync5(resolve3(cwd,".git"))||fail(`git not found.
|
|
6
6
|
Run: git init && git add -A && git commit -m 'init'
|
|
7
|
-
Then re-run: npx @pharn-dev/pharn init`)}function fail(message){logError(message),process.exit(1)}import{readFileSync as readFileSync5}from"node:fs";import{confirm as confirm2,isCancel as isCancel2,log as log3}from"@clack/prompts";import{existsSync as existsSync6,lstatSync as lstatSync2,readdirSync as readdirSync4}from"node:fs";import{join as join3,resolve as resolve4}from"node:path";import{lstatSync}from"node:fs";function findSymlinkComponent(base,rel){let current="";for(let segment of toPosix(rel).split("/"))if(segment&&(current=current?`${current}/${segment}`:segment,lstatSync(safeJoin(base,current),{throwIfNoEntry:!1})?.isSymbolicLink()))return current;return null}var PHARN_CONFIG_FILE="pharn.config.json";function*walkFiles(dir,prefix=""){for(let entry of readdirSync4(dir,{withFileTypes:!0})){if(entry.isSymbolicLink())continue;let rel=prefix?`${prefix}/${entry.name}`:entry.name;entry.isDirectory()?yield*walkFiles(resolve4(dir,entry.name),rel):yield rel}}function collectExpectedInstallPaths(params){let{repoDir,capabilities,layout}=params,paths=layoutPaths(layout),expected=new Map,add=(rel,repoPath)=>{expected.set(toPosix(rel),repoPath)},addDir=(relDir,keep,validate)=>{let from=safeJoin(repoDir,relDir);if(findSymlinkComponent(repoDir,relDir)===null&&lstatSync2(from,{throwIfNoEntry:!1})?.isDirectory())for(let rel of walkFiles(from))keep&&!keep(rel)||(validate?.(rel),add(join3(relDir,rel),resolve4(from,rel)))},copyNameFloor=dir=>rel=>{let label=`${dir}/${rel}`;assertSafeString(rel,label,COPY_FILENAME_RE),assertNoDotDot(rel,label)};for(let cap of capabilities){let subtree=cap.role==="griller"?paths.grillers:paths.lenses;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&&lstatSync2(from,{throwIfNoEntry:!1})?.isFile()&&add(doc,from)}if(findSymlinkComponent(repoDir,paths.license.from)===null){let licenseFrom=safeJoin(repoDir,paths.license.from);lstatSync2(licenseFrom,{throwIfNoEntry:!1})?.isFile()&&add(paths.license.to,licenseFrom)}if(findSymlinkComponent(repoDir,
|
|
7
|
+
Then re-run: npx @pharn-dev/pharn init`)}function fail(message){logError(message),process.exit(1)}import{readFileSync as readFileSync5}from"node:fs";import{confirm as confirm2,isCancel as isCancel2,log as log3}from"@clack/prompts";import{existsSync as existsSync6,lstatSync as lstatSync2,readdirSync as readdirSync4}from"node:fs";import{join as join3,resolve as resolve4}from"node:path";import{lstatSync}from"node:fs";function findSymlinkComponent(base,rel){let current="";for(let segment of toPosix(rel).split("/"))if(segment&&(current=current?`${current}/${segment}`:segment,lstatSync(safeJoin(base,current),{throwIfNoEntry:!1})?.isSymbolicLink()))return current;return null}var PHARN_CONFIG_FILE="pharn.config.json";function*walkFiles(dir,prefix=""){for(let entry of readdirSync4(dir,{withFileTypes:!0})){if(entry.isSymbolicLink())continue;let rel=prefix?`${prefix}/${entry.name}`:entry.name;entry.isDirectory()?yield*walkFiles(resolve4(dir,entry.name),rel):yield rel}}function collectExpectedInstallPaths(params){let{repoDir,capabilities,layout}=params,paths=layoutPaths(layout),expected=new Map,add=(rel,repoPath)=>{expected.set(toPosix(rel),repoPath)},addDir=(relDir,keep,validate)=>{let from=safeJoin(repoDir,relDir);if(findSymlinkComponent(repoDir,relDir)===null&&lstatSync2(from,{throwIfNoEntry:!1})?.isDirectory())for(let rel of walkFiles(from))keep&&!keep(rel)||(validate?.(rel),add(join3(relDir,rel),resolve4(from,rel)))},copyNameFloor=dir=>rel=>{let label=`${dir}/${rel}`;assertSafeString(rel,label,COPY_FILENAME_RE),assertNoDotDot(rel,label)};for(let cap of capabilities){let subtree=cap.role==="griller"?paths.grillers:paths.lenses;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&&lstatSync2(from,{throwIfNoEntry:!1})?.isFile()&&add(doc,from)}if(findSymlinkComponent(repoDir,paths.license.from)===null){let licenseFrom=safeJoin(repoDir,paths.license.from);lstatSync2(licenseFrom,{throwIfNoEntry:!1})?.isFile()&&add(paths.license.to,licenseFrom)}let featuresRel=resolveFeaturesReadme(repoDir,layout);if(findSymlinkComponent(repoDir,featuresRel)===null){let featuresFrom=safeJoin(repoDir,featuresRel);lstatSync2(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=collectExpectedInstallPaths({repoDir,capabilities,layout}),candidates=new Set(expected.keys());candidates.add(PHARN_CONFIG_FILE);let conflicts=[];for(let rel of candidates)existsSync6(safeJoin(projectRoot,rel))&&conflicts.push(rel);return conflicts.sort()}function capabilityCloneFiles(repoDir,paths,capability){let relDir=`${capability.role==="griller"?paths.grillers:paths.lenses}/${capability.name}`;if(findSymlinkComponent(repoDir,relDir)!==null)return[];let from=safeJoin(repoDir,relDir);return lstatSync2(from,{throwIfNoEntry:!1})?.isDirectory()?[...walkFiles(from)].map(rel=>`${relDir}/${rel}`).sort():[]}var MAX_LISTED=10;function recordedSkillsVersion(cwd){try{let raw=readFileSync5(safeJoin(cwd,PHARN_CONFIG_FILE),"utf8"),parsed=JSON.parse(raw);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}}async function confirmWriteTargets(repoDir,cwd,selection){let conflicts=conflictingWriteTargets({repoDir,projectRoot:cwd,capabilities:selection.selected,layout:detectLayout(repoDir)});if(conflicts.length===0)return"proceed";let shown=conflicts.slice(0,MAX_LISTED),more=conflicts.length-shown.length,list=shown.map(p=>` \u2022 ${p}`).join(`
|
|
8
8
|
`),tail=more>0?`
|
|
9
9
|
\u2026and ${more} more`:"",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:
|
|
10
10
|
${list}${tail}`);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.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(`
|
|
11
|
-
`));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}import{log as log4,outro,spinner}from"@clack/prompts";import pc2 from"picocolors";import{cpSync,existsSync as existsSync7,lstatSync as lstatSync3,mkdirSync as mkdirSync2,readdirSync as readdirSync5}from"node:fs";import{relative}from"node:path";var isTestFile=p=>/\.test\.(mjs|cjs)$/.test(p),inFloorFixtures=(floorFrom,src)=>{let rel=toPosix(relative(floorFrom,src));return rel===FLOOR_TEST_FIXTURES_DIR||rel.startsWith(`${FLOOR_TEST_FIXTURES_DIR}/`)},isSymlink=p=>lstatSync3(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=cap.role==="griller"?paths.grillers:paths.lenses,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}}),installed=[];for(let cap of planned){let to=safeJoin(projectRoot,`${cap.subtree}/${cap.name}`);cpSync(cap.from,to,{recursive:!0,force:!0,filter:noSymlinks}),installed.push({name:cap.name,role:cap.role})}return installed}function installCapabilities(repoDir,projectRoot,selection){let paths=layoutPaths(detectLayout(repoDir)),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)&&(mkdirSync2(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,
|
|
11
|
+
`));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}import{log as log4,outro,spinner}from"@clack/prompts";import pc2 from"picocolors";import{cpSync,existsSync as existsSync7,lstatSync as lstatSync3,mkdirSync as mkdirSync2,readdirSync as readdirSync5}from"node:fs";import{relative}from"node:path";var isTestFile=p=>/\.test\.(mjs|cjs)$/.test(p),inFloorFixtures=(floorFrom,src)=>{let rel=toPosix(relative(floorFrom,src));return rel===FLOOR_TEST_FIXTURES_DIR||rel.startsWith(`${FLOOR_TEST_FIXTURES_DIR}/`)},isSymlink=p=>lstatSync3(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=cap.role==="griller"?paths.grillers:paths.lenses,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}}),installed=[];for(let cap of planned){let to=safeJoin(projectRoot,`${cap.subtree}/${cap.name}`);cpSync(cap.from,to,{recursive:!0,force:!0,filter:noSymlinks}),installed.push({name:cap.name,role:cap.role})}return installed}function installCapabilities(repoDir,projectRoot,selection){let paths=layoutPaths(detectLayout(repoDir)),featuresRel=resolveFeaturesReadme(repoDir,paths.layout),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)&&(mkdirSync2(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 destAcceptsWrite(projectRoot,rel){try{return findSymlinkComponent(projectRoot,rel)===null}catch{return!1}}function copyFilteredDir(repoDir,projectRoot,relDir,keep){let fromDir=safeJoin(repoDir,relDir);if(!existsSync7(fromDir))return;let entries=readdirSync5(fromDir,{withFileTypes:!0}),ensured=!1;for(let entry of entries)!entry.isFile()||entry.isSymbolicLink()||keep(entry.name)&&(assertSafeString(entry.name,`${relDir}/${entry.name}`,COPY_FILENAME_RE),assertNoDotDot(entry.name,`${relDir}/${entry.name}`),ensured||(mkdirSync2(safeJoin(projectRoot,relDir),{recursive:!0}),ensured=!0),cpSync(safeJoin(fromDir,entry.name),safeJoin(projectRoot,`${relDir}/${entry.name}`),{force:!0}))}import{existsSync as existsSync8,lstatSync as lstatSync4,readFileSync as readFileSync7}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)}
|
|
12
12
|
`,"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";import{createHash}from"node:crypto";import{readFileSync as readFileSync6}from"node:fs";function sha256File(path){return createHash("sha256").update(readFileSync6(path)).digest("hex")}var RECORDS_FILE="pharn.records.json",RECORDS_SCHEMA_VERSION=1,SHA256_RE=/^[0-9a-f]{64}$/;function isInvalidRecordKey(key2){let normalized=toPosix(key2);return normalized===""||normalized.startsWith("/")||DRIVE_ABSOLUTE_RE.test(normalized)?!0:normalized.split("/").some(segment=>segment===".."||segment===".")}var DRIVE_ABSOLUTE_RE=/^[A-Za-z]:\//;function recordsPath(cwd){return resolve5(cwd,RECORDS_FILE)}function readRecords(cwd){let path=recordsPath(cwd);if(!existsSync8(path))return{kind:"absent"};let raw;try{raw=JSON.parse(readFileSync7(path,"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=lstatSync4(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=>`${cap.role==="griller"?paths.grillers:paths.lenses}/${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 ModelRoutingError=class extends Error{constructor(message){super(message),this.name="ModelRoutingError"}},MODEL_IDS=["opus-4-8","sonnet-5","fable-5","haiku-4-5"],EFFORT_LEVELS=["low","high","max"],PIPELINE_STAGES=["plan","grill","build","regress","verify","review","ship"],DEFAULT_MODEL_ROUTING={default:{model:"sonnet-5",effort:"high"},stages:{plan:{model:"opus-4-8",effort:"max"},review:{model:"opus-4-8",effort:"high"}}},ROUTING_KEYS=["default","stages"],STAGE_MODEL_KEYS=["model","effort"];function assertNoUnknownKeys(obj,known,label){for(let key2 of Object.keys(obj))if(!known.includes(key2))throw new ModelRoutingError(`${label} has unknown key ${JSON.stringify(key2)} (expected one of ${known.join(", ")})`)}function assertStageModel(value,label){if(!isPlainObject(value))throw new ModelRoutingError(`${label} must be an object`);assertNoUnknownKeys(value,STAGE_MODEL_KEYS,label);let{model,effort}=value;if(typeof model!="string"||!MODEL_IDS.includes(model))throw new ModelRoutingError(`${label} has invalid model ${JSON.stringify(model)} (expected one of ${MODEL_IDS.join(", ")})`);if(typeof effort!="string"||!EFFORT_LEVELS.includes(effort))throw new ModelRoutingError(`${label} has invalid effort ${JSON.stringify(effort)} (expected one of ${EFFORT_LEVELS.join(", ")})`);return{model,effort}}function validateModelRouting(input){if(!isPlainObject(input))throw new ModelRoutingError("models must be an object");assertNoUnknownKeys(input,ROUTING_KEYS,"models");let routingDefault=assertStageModel(input.default,"models.default"),stages={};if(input.stages!==void 0){if(!isPlainObject(input.stages))throw new ModelRoutingError("models.stages must be an object");for(let[stage,value]of Object.entries(input.stages)){if(!PIPELINE_STAGES.includes(stage))throw new ModelRoutingError(`models.stages has unknown stage ${JSON.stringify(stage)} (expected one of ${PIPELINE_STAGES.join(", ")})`);stages[stage]=assertStageModel(value,`models.stages.${stage}`)}}return{default:routingDefault,stages}}function formatModelRoutingLines(routing){let entries=[["default",routing.default]];for(let stage of PIPELINE_STAGES){let target=routing.stages?.[stage];target&&entries.push([stage,target])}let width=Math.max(...entries.map(([label])=>label.length))+3;return entries.map(([label,{model,effort}])=>`${label.padEnd(width)}${model} \xB7 ${effort}`)}var SeamConfigError=class extends Error{constructor(message){super(message),this.name="SeamConfigError"}},RESOLUTION_STEPS=["official-skill","pinned-docs","fetch","model","ask"],SEAM_CONFIDENCE_LEVELS=["low","medium","high"],TERMINAL_STEP="ask",DEFAULT_SEAM_CONFIG={resolutionOrder:["official-skill","pinned-docs","model","fetch","ask"],modelConfidenceThreshold:"high",haltOnUnknown:!0},SEAM_KEYS=["resolutionOrder","modelConfidenceThreshold","haltOnUnknown"];function assertNoUnknownKeys2(obj,known,label){for(let key2 of Object.keys(obj))if(!known.includes(key2))throw new SeamConfigError(`${label} has unknown key ${JSON.stringify(key2)} (expected one of ${known.join(", ")})`)}function validateSeamConfig(input){if(!isPlainObject(input))throw new SeamConfigError("seam must be an object");assertNoUnknownKeys2(input,SEAM_KEYS,"seam");let{resolutionOrder}=input;if(!Array.isArray(resolutionOrder))throw new SeamConfigError(`seam.resolutionOrder must be an array (got ${JSON.stringify(resolutionOrder)})`);for(let i=0;i<resolutionOrder.length;i++){let step=resolutionOrder[i];if(!RESOLUTION_STEPS.includes(step))throw new SeamConfigError(`seam.resolutionOrder[${i}] has invalid step ${JSON.stringify(step)} (expected one of ${RESOLUTION_STEPS.join(", ")})`)}let seen=new Set;for(let step of resolutionOrder){if(seen.has(step))throw new SeamConfigError(`seam.resolutionOrder has duplicate step ${JSON.stringify(step)} (each step may appear at most once)`);seen.add(step)}if(!resolutionOrder.includes(TERMINAL_STEP))throw new SeamConfigError(`seam.resolutionOrder must contain "${TERMINAL_STEP}" \u2014 the terminal fallback cannot be configured away (fail-closed)`);let config={resolutionOrder};if(input.modelConfidenceThreshold!==void 0){let threshold=input.modelConfidenceThreshold;if(typeof threshold!="string"||!SEAM_CONFIDENCE_LEVELS.includes(threshold))throw new SeamConfigError(`seam.modelConfidenceThreshold ${JSON.stringify(threshold)} is invalid (expected one of ${SEAM_CONFIDENCE_LEVELS.join(", ")})`);config.modelConfidenceThreshold=threshold}if(input.modelConfidenceThreshold!==void 0&&!resolutionOrder.includes("model"))throw new SeamConfigError('seam.modelConfidenceThreshold is set but resolutionOrder has no "model" step for it to gate (add "model" or remove the threshold)');if(input.haltOnUnknown!==void 0){if(typeof input.haltOnUnknown!="boolean")throw new SeamConfigError(`seam.haltOnUnknown must be a boolean (got ${JSON.stringify(input.haltOnUnknown)})`);config.haltOnUnknown=input.haltOnUnknown}return config}import{existsSync as existsSync9,readFileSync as readFileSync8}from"node:fs";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"}};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_RE3=/[\x00-\x1f\x7f-\x9f]/g;function displayPath(path){return path.replace(CONTROL_CHARS_RE3,c=>`\\x${c.charCodeAt(0).toString(16).padStart(2,"0")}`)}function configPath(cwd){return resolve6(cwd,CONFIG_FILENAME)}function readPharnConfig(cwd){let path=configPath(cwd);if(!existsSync9(path))return null;let text;try{text=readFileSync8(path,"utf8")}catch{return null}let raw;try{raw=JSON.parse(text)}catch(err){throw new ConfigParseError(`${displayPath(path)} is not valid JSON${parseLocation(err)}. The file exists \u2014 fix its syntax by hand. Do NOT run \`pharn init\` to clear it: init OVERWRITES this config, discarding your recorded capabilities, any manual \`pharn add\` provenance, and hand-edited models/seam blocks. If it is beyond repair, move it aside first (\`mv ${CONFIG_FILENAME} ${CONFIG_FILENAME}.bak\`), then run \`pharn init\`.`)}if(!isPlainObject(raw)||typeof raw.skillsVersion!="string"||!Array.isArray(raw.modules))return null;let models=raw.models!==void 0?validateModelRouting(raw.models):void 0,seam=raw.seam!==void 0?validateSeamConfig(raw.seam):void 0;validateCapabilitySources(raw.capabilities);let config={...raw,...models!==void 0?{models}:{},...seam!==void 0?{seam}:{}};return config.layout!=="pharn"&&config.layout!=="flat"&&delete config.layout,config}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 isConfigValidationError(err){return err instanceof ConfigParseError||err instanceof ModelRoutingError||err instanceof SeamConfigError||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}async function runInstallArchetype(repoDir,cwd,archetypes,selection,commit){let startedAt=Date.now(),s=spinner();s.start("Installing capabilities");let capabilities,settingsPreserved,skillsVersion,layout,docsWritten;try{let result=installCapabilities(repoDir,cwd,selection);capabilities=result.capabilities,settingsPreserved=result.settingsPreserved,layout=result.layout,docsWritten=result.docs,skillsVersion=readSkillsVersion(repoDir)}catch(err){throw s.stop("Failed to install capabilities"),err}s.stop("Capabilities installed"),settingsPreserved&&log4.warn("Existing .claude/settings.json preserved \u2014 compare it against pharn-oss to wire the PHARN hooks if needed.");let docsMissing=layoutPaths(layout).docs.filter(doc=>!docsWritten.includes(doc));docsMissing.length&&log4.warn(`The fetched repo shipped no ${docsMissing.join(", ")} \u2014 not installed. Installed commands and checkers that cite ${docsMissing.length===1?"it":"them"} by path will not resolve.`);let config={pharnVersion:PHARN_VERSION,skillsVersion,repo:REPO_URL.replace(/^github\.com\//,""),commit,modules:[],installedAt:new Date().toISOString(),models:DEFAULT_MODEL_ROUTING,seam:DEFAULT_SEAM_CONFIG,archetypes,capabilities:capabilities.map(c=>({...c,source:"auto"})),layout};await writeRecords(cwd,{skillsVersion,commit,files:buildRecords(cwd,collectExpectedInstallPaths({repoDir,capabilities,layout}).keys())}),await writePharnConfig(cwd,config);let elapsed=((Date.now()-startedAt)/1e3).toFixed(1),check=pc2.green("\u2714"),grillers=capabilities.filter(c=>c.role==="griller").length,lenses=capabilities.filter(c=>c.role==="lens").length,modelLines=config.models?formatModelRoutingLines(config.models):[],docsLine=docsWritten.length?`${check} ${docsWritten.length} trusted doc${docsWritten.length===1?"":"s"} written \u2192 ${pc2.dim(docsWritten.join(", "))}`:`${pc2.yellow("!")} no trusted docs written ${pc2.dim("(the fetched repo shipped none at their expected paths)")}`;outro([`${check} ${capabilities.length} capabilit${capabilities.length===1?"y":"ies"} installed \u2192 ${pc2.dim(`(${grillers} griller${grillers===1?"":"s"}, ${lenses} lens${lenses===1?"":"es"})`)}`,`${check} PHARN commands + hooks written \u2192 ${pc2.dim(".claude/")}`,docsLine,`${check} pharn.config.json written ${pc2.dim(`(skills v${skillsVersion}, archetypes: ${archetypes.join(", ")})`)}`,`${pc2.dim(`Done in ${elapsed}s`)}`,"",pc2.bold("Models per stage"),...modelLines.map(line=>` ${line}`),` ${pc2.dim("Recorded in pharn.config.json \u2192 models.stages \u2014 no installed stage reads it yet")}`,"",pc2.bold("Next steps"),` ${pc2.cyan("1.")} ${pc2.bold("claude")} ${pc2.dim("open Claude Code")}`,` ${pc2.cyan("2.")} ${pc2.bold(FIRST_FEATURE_COMMAND)} ${pc2.dim("capture your first feature's intent")}`].join(`
|
|
13
13
|
`))}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 selection=resolveCapabilities(archetypes,index);if((await runArchetypeSummary(archetypes,selection)==="install"?await confirmWriteTargets(repo.dir,cwd,selection):"cancel")==="proceed"){let commit=repo.sha;await withProjectLock(cwd,"init",()=>runInstallArchetype(repo.dir,cwd,archetypes,selection,commit)),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()}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}}import{copyFileSync,existsSync as existsSync10,lstatSync as lstatSync5,mkdirSync as mkdirSync3}from"node:fs";import{dirname as dirname2}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);mkdirSync3(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);mkdirSync3(dirname2(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(!existsSync10(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.`)}import{lstatSync as lstatSync6}from"node:fs";function scanDest(params){let{repoDir,projectRoot,rels}=params,drifted=[],unsafe=[];for(let rel of rels){let dest=safeJoin(projectRoot,rel),src=safeJoin(repoDir,rel),destLink=symlinkComponent(projectRoot,rel);if(destLink!==UNREADABLE){if(destLink!==null){unsafe.push({rel,link:destLink});continue}lstatSync6(dest,{throwIfNoEntry:!1})?.isFile()&&symlinkComponent(repoDir,rel)===null&&lstatSync6(src,{throwIfNoEntry:!1})?.isFile()&&sha256File(dest)!==sha256File(src)&&drifted.push(rel)}}return{drifted:drifted.sort(),unsafe:unsafe.sort((a,b)=>a.rel<b.rel?-1:a.rel>b.rel?1:0)}}var UNREADABLE=Symbol("unreadable");function symlinkComponent(base,rel){try{return findSymlinkComponent(base,rel)}catch{return UNREADABLE}}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);return fetched===config.skillsVersion?null:`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 \u2014 run \`pharn update\` first, then re-run \`pharn add\`.`}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()=>{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()=>{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 installed=config.capabilities??[],{groups,availableCount}=buildAddSelection(index,installed);if(availableCount===0)return{kind:"all-installed"};installed.length>0&&log6.info(`Installed (${installed.length}): ${installed.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,skillsVersion:result.version,commit:sha,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:
|
|
14
14
|
${valid.join(`
|
|
15
15
|
`)}`}}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),commit=sha,capabilities=[...existing,{name:cap.name,role:cap.role,source:"manual"}];return await mergeCapabilityRecords(cwd,config,cloneRels,version,commit),await writePharnConfig(cwd,{...config,skillsVersion:version,commit,capabilities,installedAt:new Date().toISOString()}),{kind:"added",name:cap.name,version}}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 existsSync11,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 installed=config.capabilities??[];if(arg===void 0){await runRemovePicker(cwd,config,installed);return}await removeNamed(cwd,config,installed,arg)}function deleteCapabilityDir(cwd,paths,target){let dir=safeJoin(cwd,capabilityRelDir(paths,target)),existed=existsSync11(dir);return existed&&rmSync3(dir,{recursive:!0,force:!0}),existed}function capabilityRelDir(paths,capability){return`${capability.role==="griller"?paths.grillers:paths.lenses}/${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,installed,address){let parsed=parseCapabilityArg(address);parsed.error&&(logError(parsed.error),process.exit(1));let matches=installed.filter(c=>c.name===parsed.name&&(parsed.role===void 0||c.role===parsed.role));if(matches.length===0){let valid=installed.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)),note6=await withProjectLock(cwd,"remove",async()=>{let existed=deleteCapabilityDir(cwd,paths,target);return await pruneCapabilityRecords(cwd,config,paths,[target]),await writePharnConfig(cwd,{...config,capabilities:installed.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,installed){if(installed.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(installed),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=installed.find(c=>c.name===parsed.name&&c.role===parsed.role);target&&targets.push(target)}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();let paths=layoutPaths(configLayout(config));await withProjectLock(cwd,"remove",async()=>{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:installed.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{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 lstatSync7,mkdirSync as mkdirSync4}from"node:fs";import{dirname as dirname3}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=lstatSync7(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(dirname3(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}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])}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;if(current&&!force){outro4(`Already up to date (skills v${config.skillsVersion}).`);return}if(note3([current?row("Skills version",`v${config.skillsVersion} (re-applying)`):row("Skills version",`v${config.skillsVersion} \u2192 v${latest}`),"",row("Archetypes",(config.archetypes??[]).join(", ")||"(none)"),"",force?pc5.dim(` --force: files you changed will be OVERWRITTEN (each is copied to ${BACKUP_DIR}/ first).`):pc5.dim(" Files you have changed are kept, not overwritten \u2014 they are listed at the end."),pc5.dim(" Re-resolves your archetypes against the latest capabilities and re-copies them."),pc5.dim(" https://github.com/pharn-dev/pharn-oss/blob/main/CHANGELOG.md")].join(`
|
|
16
|
-
`)),!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()=>{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});return s2.stop(applied.plan.writes.length?"Capabilities updated":"Nothing to write"),applied}finally{repo.cleanup()}})}catch(err){err instanceof ProjectLockedError?(spinnerRef.current?.stop("Update refused"),refusalRef.current=err.message):(spinnerRef.current?.stop("Update failed"),failure={err})}let refusal2=refusalRef.current;refusal2&&(reportFatal(refusal2),process.exit(1)),(failure||!outcome)&&(failure?reportFatal(errorMessage(failure.err),failure):reportFatal("Update failed."),backupRef.current&&printBackupNotice(backupRef.current,{aborted:!0}),process.exit(1)),reportOutcome(outcome,force)}async function applyUpdate(repoDir,sha,config,cwd,force,onBackup){let index=parseCapabilityIndex(repoDir),unknownWarning=unknownCapabilitiesWarning(index.unknown);unknownWarning&&log8.warn(unknownWarning);let frozen=new Set(index.unknown.map(u=>`${u.role}:${u.name}`)),selection=resolveCapabilities(config.archetypes??[],index),merged=mergeCapabilities(selection,config.capabilities??[],frozen),configCapabilities=merged.capabilities,manifestCapabilities=configCapabilities.filter(cap=>!frozen.has(`${cap.role}:${cap.name}`)),installedVersion=readSkillsVersion(repoDir),layout=detectLayout(repoDir),previousLayout=configLayout(config),expected=collectExpectedInstallPaths({repoDir,capabilities:manifestCapabilities,layout}),latestHashes=new Map;for(let[rel,source]of expected)latestHashes.set(rel,sha256File(source));let diskStates=new Map;for(let rel of expected.keys())diskStates.set(rel,readDiskState(cwd,rel));let{records,note:recordsNote}=recordsBaseline(readRecords(cwd),{skillsVersion:config.skillsVersion,commit:config.commit}),plan=planUpdate({latestHashes,diskStates,records,force}),backup=plan.backups.length>0?{dir:createBackup(cwd,plan.backups),count:plan.backups.length}:null;backup&&onBackup(backup);let versionWithheld=plan.counts.skipped>0,nextSkillsVersion=versionWithheld?config.skillsVersion:installedVersion,nextCommit=versionWithheld?config.commit:sha,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}`)));return await writeRecords(cwd,{skillsVersion:nextSkillsVersion,commit:nextCommit,files:{...frozenRecords,...plan.nextRecords,...buildRecords(cwd,written)}}),await writePharnConfig(cwd,{...config,skillsVersion:nextSkillsVersion,commit:nextCommit,capabilities:configCapabilities,layout,installedAt:new Date().toISOString()}),{installedVersion,recordedVersion:nextSkillsVersion,capCount:configCapabilities.length,capabilityChanges:merged.changes,plan,backup,recordsNote,versionWithheld,abandonedLayout:previousLayout!==layout&&written.length>0?previousLayout:null}}var FORCEABLE_SKIPS=new Set(["modified","unrecorded","unverifiable"]);function reportOutcome(outcome,force){let{plan,backup,recordsNote,versionWithheld}=outcome,{counts}=plan;recordsNote&&log8.warn(`\u26A0 ${recordsNote}`),reportCapabilityChanges(outcome.capabilityChanges);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(`
|
|
17
|
-
`),"SKIPPED")}backup&&printBackupNotice(backup,{aborted:!1}),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."),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}),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(`
|
|
16
|
+
`)),!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()=>{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});return s2.stop(applied.plan.writes.length?"Capabilities updated":"Nothing to write"),applied}finally{repo.cleanup()}})}catch(err){err instanceof ProjectLockedError?(spinnerRef.current?.stop("Update refused"),refusalRef.current=err.message):(spinnerRef.current?.stop("Update failed"),failure={err})}let refusal2=refusalRef.current;refusal2&&(reportFatal(refusal2),process.exit(1)),(failure||!outcome)&&(failure?reportFatal(errorMessage(failure.err),failure):reportFatal("Update failed."),backupRef.current&&printBackupNotice(backupRef.current,{aborted:!0}),process.exit(1)),reportOutcome(outcome,force)}async function applyUpdate(repoDir,sha,config,cwd,force,onBackup){let index=parseCapabilityIndex(repoDir),unknownWarning=unknownCapabilitiesWarning(index.unknown);unknownWarning&&log8.warn(unknownWarning);let frozen=new Set(index.unknown.map(u=>`${u.role}:${u.name}`)),selection=resolveCapabilities(config.archetypes??[],index),merged=mergeCapabilities(selection,config.capabilities??[],frozen),configCapabilities=merged.capabilities,manifestCapabilities=configCapabilities.filter(cap=>!frozen.has(`${cap.role}:${cap.name}`)),installedVersion=readSkillsVersion(repoDir),layout=detectLayout(repoDir),previousLayout=configLayout(config),expected=collectExpectedInstallPaths({repoDir,capabilities:manifestCapabilities,layout}),latestHashes=new Map;for(let[rel,source]of expected)latestHashes.set(rel,sha256File(source));let diskStates=new Map;for(let rel of expected.keys())diskStates.set(rel,readDiskState(cwd,rel));let{records,note:recordsNote}=recordsBaseline(readRecords(cwd),{skillsVersion:config.skillsVersion,commit:config.commit}),plan=planUpdate({latestHashes,diskStates,records,force}),backup=plan.backups.length>0?{dir:createBackup(cwd,plan.backups),count:plan.backups.length}:null;backup&&onBackup(backup);let versionWithheld=plan.counts.skipped>0,nextSkillsVersion=versionWithheld?config.skillsVersion:installedVersion,nextCommit=versionWithheld?config.commit:sha,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}`)));return await writeRecords(cwd,{skillsVersion:nextSkillsVersion,commit:nextCommit,files:{...frozenRecords,...plan.nextRecords,...buildRecords(cwd,written)}}),await writePharnConfig(cwd,{...config,skillsVersion:nextSkillsVersion,commit:nextCommit,capabilities:configCapabilities,layout,installedAt:new Date().toISOString()}),{installedVersion,recordedVersion:nextSkillsVersion,capCount:configCapabilities.length,capabilityChanges:merged.changes,plan,backup,recordsNote,versionWithheld,abandonedLayout:previousLayout!==layout&&written.length>0?previousLayout:null,featuresReadmeRelocation:records!==null&&FEATURES_README in records&&resolveFeaturesReadme(repoDir,layout)!==FEATURES_README}}var FORCEABLE_SKIPS=new Set(["modified","unrecorded","unverifiable"]);function reportOutcome(outcome,force){let{plan,backup,recordsNote,versionWithheld}=outcome,{counts}=plan;recordsNote&&log8.warn(`\u26A0 ${recordsNote}`),reportCapabilityChanges(outcome.capabilityChanges);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(`
|
|
17
|
+
`),"SKIPPED")}backup&&printBackupNotice(backup,{aborted:!1}),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}),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(`
|
|
18
18
|
`),"CAPABILITIES")}function skipHeading(label){switch(label){case"modified":return"MODIFIED \u2014 you changed these since pharn wrote them";case"unrecorded":return"UNRECORDED \u2014 pharn has no record of writing these";case"unverifiable":return`UNVERIFIABLE \u2014 no usable ${RECORDS_FILE} to compare against`;default:return"UNREADABLE \u2014 not a regular readable file"}}import{intro as intro5,note as note4,outro as outro5}from"@clack/prompts";import pc6 from"picocolors";async function runList(opts={}){let json=opts.json??!1,cwd=process.cwd();json||intro5("pharn list");let config;try{config=readPharnConfig(cwd)}catch(e){throw isConfigValidationError(e)&&(emitError(e.message,json),process.exit(1)),e}config||(emitError("No pharn.config.json found. Run `pharn init` first.",json),process.exit(1)),isArchetypeConfig(config)||(emitError(LEGACY_CONFIG_MESSAGE,json),process.exit(1));let inventory=buildArchetypeInventory(config);if(json){console.log(JSON.stringify(inventory,null,2));return}renderArchetypeHuman(inventory)}function buildArchetypeInventory(config){return{mode:"archetype",skillsVersion:config.skillsVersion,archetypes:config.archetypes??[],capabilities:(config.capabilities??[]).map(c=>({name:c.name,role:c.role,...c.source!==void 0?{source:c.source}:{}}))}}function renderArchetypeHuman(inv){let lines=renderCapabilityLines(inv);note4(lines.join(`
|
|
19
19
|
`),"INSTALLED (archetype)"),outro5(pc6.dim("Read-only \u2014 nothing changed."))}function emitError(message,json){json?console.error(message):logError(message)}import{intro as intro6,log as log9,note as note5,outro as outro6,spinner as spinner5}from"@clack/prompts";import pc7 from"picocolors";function diffInstalledCapabilities(params){let{repoDir,projectRoot,capabilities,layout}=params,expected=collectExpectedInstallPaths({repoDir,capabilities,layout});return compareExpected(expected,projectRoot)}function compareExpected(expected,baseDir){let modified=[],missing=[],unreadable=[],okCount=0;for(let[rel,repoPath]of expected){let state=readDiskState(baseDir,rel);state.kind==="absent"?missing.push(rel):state.kind==="unreadable"?unreadable.push({rel,reason:state.reason}):state.hash===sha256File(repoPath)?okCount+=1:modified.push(rel)}return modified.sort(),missing.sort(),unreadable.sort((a,b)=>+(a.rel>b.rel)-+(a.rel<b.rel)),{modified,missing,unreadable,okCount}}var REF=`${REPO}@${REPO_BRANCH}`;async function runStatus(opts={}){let strict=opts.strict??!1,drift=opts.drift??!0;intro6("pharn status");let cwd=process.cwd(),config=loadArchetypeConfigOrExit(cwd);await runArchetypeStatus(config,{strict,drift},cwd)}async function runArchetypeStatus(config,opts,cwd){let{strict,drift}=opts,proxyNotice=detectProxyNotice(process.env);if(proxyNotice&&log9.warn(proxyNoticeMessage(proxyNotice)),!drift){let s2=spinner5();s2.start("Checking for updates");let latest;try{latest=await fetchRemoteSkillsVersion(),s2.stop(`Latest skills v${latest}`)}catch(err){s2.stop("Failed to check for updates"),reportFatal(errorMessage(err),{err}),process.exit(1)}let outdated=printArchetypeVersion(config,latest);printModelRouting(config),strict&&outdated&&process.exit(1),outro6(pc7.dim("Read-only \u2014 nothing changed (drift check skipped)."));return}let s=spinner5();s.start(`Comparing against ${REF}`);let repo;try{repo=await fetchRepo(),s.stop(`Compared against ${REF}`)}catch(err){s.stop(`Failed to reach ${REPO}`),reportFatal(errorMessage(err),{err}),process.exit(1)}let exitCode=0;try{let outdated=printArchetypeVersion(config,readSkillsVersion(repo.dir));printModelRouting(config);let index=parseCapabilityIndex(repo.dir),unknownWarning=unknownCapabilitiesWarning(index.unknown);unknownWarning&&log9.warn(unknownWarning);let frozen=new Set(index.unknown.map(u=>`${u.role}:${u.name}`)),result=diffInstalledCapabilities({repoDir:repo.dir,projectRoot:cwd,capabilities:(config.capabilities??[]).filter(cap=>!frozen.has(`${cap.role}:${cap.name}`)),layout:configLayout(config)});printDriftSection(result),strict&&(outdated||result.modified.length||result.missing.length||result.unreadable.length)&&(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(`
|
|
20
20
|
`),"VERSION"),outdated}function printModelRouting(config){config.models!==void 0&¬e5([...formatModelRoutingLines(config.models),"",pc7.dim("Recorded only \u2014 no installed stage reads this yet.")].join(`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pharn-dev/pharn",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|
|
@@ -68,5 +68,8 @@
|
|
|
68
68
|
"typescript": "^6.0.3",
|
|
69
69
|
"typescript-eslint": "^8.65.0",
|
|
70
70
|
"vitest": "^5.0.0"
|
|
71
|
+
},
|
|
72
|
+
"overrides": {
|
|
73
|
+
"smol-toml": "~1.7.1"
|
|
71
74
|
}
|
|
72
75
|
}
|