@pharn-dev/pharn 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +151 -0
  3. package/dist/index.js +37 -0
  4. package/package.json +71 -0
package/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2026 Przemysław Galarowicz
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,151 @@
1
+ <div align="center">
2
+
3
+ # pharn
4
+
5
+ **Install PHARN into your project in one command.**
6
+
7
+ The installer for [PHARN](https://github.com/pharn-dev/pharn-oss) — an audit-grade methodology for Claude Code that keeps comprehension debt legible instead of silent.
8
+
9
+ [![npm](https://img.shields.io/npm/v/@pharn-dev/pharn)](https://www.npmjs.com/package/@pharn-dev/pharn)
10
+ [![CI](https://github.com/pharn-dev/pharn-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/pharn-dev/pharn-cli/actions/workflows/ci.yml)
11
+ [![CodeQL](https://github.com/pharn-dev/pharn-cli/actions/workflows/codeql.yml/badge.svg)](https://github.com/pharn-dev/pharn-cli/actions/workflows/codeql.yml)
12
+ [![License: Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-green)](./LICENSE)
13
+ [![Node](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](./package.json)
14
+
15
+ ```bash
16
+ npx @pharn-dev/pharn init
17
+ ```
18
+
19
+ </div>
20
+
21
+ ---
22
+
23
+ ## Contents
24
+
25
+ - [Install](#install)
26
+ - [Why](#why)
27
+ - [What it installs](#what-it-installs)
28
+ - [The pipeline](#the-pipeline)
29
+ - [Commands](#commands)
30
+ - [Prerequisites](#prerequisites)
31
+ - [Documentation](#documentation)
32
+ - [Development](#development)
33
+ - [Security](#security)
34
+ - [License](#license)
35
+
36
+ ---
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ npx @pharn-dev/pharn@latest init
42
+ ```
43
+
44
+ `pharn` runs straight from npm — `npx` fetches the latest published version and runs it in your project; no global install. Requires a git-initialized project and Node >= 20.
45
+
46
+ ---
47
+
48
+ ## Why
49
+
50
+ Vibe-coding with an AI agent is fast — until the chat history scrolls away and takes the _understanding_ with it. That gap is **comprehension debt** ([coined by Addy Osmani](https://addyosmani.com/blog/comprehension-debt/)), and it compounds faster than any other kind. PHARN keeps a markdown-canonical record — spec, constitution, diff, audit trail — in your repo, readable and diffable.
51
+
52
+ `pharn` is how you get it. Run `pharn init` in your project; the CLI detects your project's **archetype(s)**, fetches the applicable PHARN **capabilities** from `pharn-dev/pharn-oss`, copies them plus the canonical constitution into the mirrored layout (`.claude/` + `pharn/`), and writes `pharn.config.json`.
53
+
54
+ > The npm package is `@pharn-dev/pharn`; it installs a single `pharn` binary.
55
+
56
+ ---
57
+
58
+ ## What it installs
59
+
60
+ `pharn init` detects your project's **archetype(s)** and installs the PHARN **capabilities** that apply to them — nothing you didn't ask for. There is no module catalog and no `manifest.json`: capabilities are the install unit.
61
+
62
+ - **Archetype** — a closed set describing what your project _is_: `ssr`, `backend`, `spa`, or `lib` (the frameworkless base). Detection merges your `package.json` dependency **names** with a bounded, symlink-safe file-tree scan (names only, never file bodies). A project can match several (Next + Express → `ssr` + `backend`); a signal-less project resolves to `lib`.
63
+ - **Capability** — one **griller** (a pipeline auditor) or **lens** (a review lens). Each declares `applies: 'universal'` or a set of archetypes; it is **selected** when universal or when its `applies` intersects your detected archetypes, and **skipped** otherwise (with the reason shown).
64
+
65
+ After a summary of what was selected vs. skipped and your confirmation, the CLI copies the selected capabilities plus the fixed product surfaces into the mirrored layout and writes `pharn.config.json`:
66
+
67
+ | Artifact | What lands in your project |
68
+ | --------------------------------------------------------- | ------------------------------------------------------------------------------------ |
69
+ | `pharn-pipeline/grillers/<name>/`, `pharn-review/<name>/` | The selected grillers + lenses (flat layout, or the same under `pharn/`) |
70
+ | `.claude/commands/` | The `pharn-*` product slash commands |
71
+ | `.claude/hooks/` | The deterministic `.cjs` floor hooks |
72
+ | `pharn-contracts/`, `.dev/floor/` | Inter-layer schemas + the floor checkers the commands invoke |
73
+ | `CONSTITUTION.md` | The canonical PHARN constitution, copied verbatim |
74
+ | `pharn.config.json` | `skillsVersion`, commit SHA, detected archetypes, installed capabilities, and layout |
75
+
76
+ An existing `.claude/settings.json` is **never** overwritten. To adjust the selection afterward, use [`pharn add`](docs/commands/add.md) / [`pharn remove`](docs/commands/remove.md).
77
+
78
+ ---
79
+
80
+ ## The pipeline
81
+
82
+ Once installed, PHARN gives Claude Code a spine of typed stages — each links back to the spec:
83
+
84
+ ```text
85
+ spec → plan → grill → build → regress → verify → review → ship
86
+ ```
87
+
88
+ After `pharn init`, open Claude Code and run **`/pharn-spec`** to capture your first feature's intent — it feeds **`/pharn-plan`**. (For a small, well-scoped change you can start at **`/pharn-plan`**.)
89
+
90
+ ---
91
+
92
+ ## Commands
93
+
94
+ | Command | Description |
95
+ | --------------------------- | ------------------------------------------------------------------------------- |
96
+ | `pharn init` | Detect archetypes and install the applicable capabilities (default) |
97
+ | `pharn add [capability]` | Add a capability, e.g. `a11y` or `lens:n-plus-one` (no arg: pick interactively) |
98
+ | `pharn remove <capability>` | Remove an installed capability (no arg: pick one interactively) |
99
+ | `pharn update` | Re-fetch installed capabilities at the latest skills version |
100
+ | `pharn list` | List installed archetypes + capabilities (`--json`) |
101
+ | `pharn status` | Show version + local-drift status (read-only; `--strict`, `--no-drift`) |
102
+ | `pharn -h`, `--help` | Show help |
103
+ | `pharn -v`, `--version` | Show version |
104
+
105
+ ---
106
+
107
+ ## Prerequisites
108
+
109
+ - **Git** initialized in the project — the only requirement, checked up front before detection.
110
+
111
+ There is no stack-pack selection and no package prerequisite to satisfy. See [Getting started](docs/getting-started.md) for the full flow, including the pre-install write-target conflict check.
112
+
113
+ ---
114
+
115
+ ## Documentation
116
+
117
+ Full reference: **[docs/](docs/README.md)**
118
+
119
+ - [Getting started](docs/getting-started.md)
120
+ - [Commands](docs/commands/init.md) — `init`, `add`, `remove`, `update`, `list`, `status`
121
+ - [pharn.config.json](docs/reference/pharn-config.md)
122
+ - [Roadmap](docs/roadmap.md)
123
+ - [Troubleshooting](docs/troubleshooting.md)
124
+
125
+ ---
126
+
127
+ ## Development
128
+
129
+ ```bash
130
+ cd pharn-cli
131
+ npm install
132
+ npm run dev # e.g. npm run dev -- init
133
+ npm run build
134
+ npm run test
135
+ npm run check # format:check + lint + typecheck + test
136
+ npm run build:install-local # link pharn into the local test-app/
137
+ ```
138
+
139
+ `PHARN_DEBUG=1` for verbose errors. Details: [Contributing](CONTRIBUTING.md).
140
+
141
+ ---
142
+
143
+ ## Security
144
+
145
+ All remote input (repo/branch/commit, capability names and paths, and capability frontmatter) is validated against strict allowlists, checked for path escapes, and fetched with `redirect: 'error'`, an 8s timeout, and a 256KB body cap. Contents copied from the clone are never executed or parsed by the CLI. Found a vulnerability? Please follow [`SECURITY.md`](SECURITY.md) rather than opening a public issue.
146
+
147
+ ---
148
+
149
+ ## License
150
+
151
+ [Apache 2.0](LICENSE).
package/dist/index.js ADDED
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ import{realpathSync}from"node:fs";import{fileURLToPath}from"node:url";import minimist from"minimist";import{intro,log as log4,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)}async function confirmWarning(warning,prompt,initialValue){log.warn(warning);let result=await confirm({message:prompt,initialValue});return isCancel(result)&&cancelAndExit(),result===!0}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",TRUSTED_DOCS=["CONSTITUTION.md","ARCHITECTURE.md","THREAT-MODEL.md","LIMITS.md"],CONTRACTS_DIR="pharn-contracts",FLOOR_DIR=".dev/floor",PHARN_GRILLERS_DIR="pharn/pharn-pipeline/grillers",PHARN_LENSES_DIR="pharn/pharn-review",PHARN_CONTRACTS_DIR="pharn/pharn-contracts",PHARN_FLOOR_DIR="pharn/floor",PHARN_TRUSTED_DOCS=["pharn/CONSTITUTION.md","pharn/ARCHITECTURE.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"]),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}}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"}};var VERSION_RE=/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/;var CAPABILITY_NAME_RE=/^[a-z0-9]+(-[a-z0-9]+)*$/,COPY_FILENAME_RE=/^[a-z0-9]+(-[a-z0-9]+)*\.(md|cjs|mjs|json)$/,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 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,floor:PHARN_FLOOR_DIR,docs:PHARN_TRUSTED_DOCS}:{layout,grillers:GRILLERS_DIR,lenses:LENSES_DIR,contracts:CONTRACTS_DIR,floor:FLOOR_DIR,docs:TRUSTED_DOCS}}function configLayout(config){return config.layout==="pharn"?"pharn":"flat"}function parseCapabilityIndex(repoDir){let capabilities=[],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){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})}}return{capabilities}}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}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 degit from"degit";var API="https://api.github.com",FETCH_TIMEOUT_MS=8e3;async function fetchRepo(){let sha=await fetchCommitSha(),ref=sha??REPO_BRANCH,dir=mkdtempSync(join2(tmpdir(),"pharn-"));try{await degit(`${REPO}#${ref}`,{force:!0,cache:!1}).clone(dir)}catch(err){throw rmSync(dir,{recursive:!0,force:!0}),err}return{dir,sha,cleanup:()=>rmSync(dir,{recursive:!0,force:!0})}}async function fetchCommitSha(){let url=`${API}/repos/${REPO}/commits/${REPO_BRANCH}`,controller=new AbortController,timer=setTimeout(()=>controller.abort(),FETCH_TIMEOUT_MS);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{existsSync as existsSync4}from"node:fs";import{resolve as resolve3}from"node:path";import{cancel}from"@clack/prompts";function runGitPrereq(){let cwd=process.cwd();existsSync4(resolve3(cwd,".git"))||fail(`\u2717 git not found.
3
+ Run: git init && git add -A && git commit -m 'init'
4
+ Then re-run: npx @pharn-dev/pharn init`)}function fail(message){cancel(message),process.exit(1)}import{existsSync as existsSync5,readdirSync as readdirSync3,statSync}from"node:fs";import{join as join3,resolve as resolve4,sep as sep2}from"node:path";var PHARN_CONFIG_FILE="pharn.config.json";function*walkFiles(dir,prefix=""){for(let entry of readdirSync3(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 toPosix(rel){return rel.split(sep2).join("/").replace(/\/+$/,"")}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)=>{let from=safeJoin(repoDir,relDir);if(!(!existsSync5(from)||!statSync(from).isDirectory()))for(let rel of walkFiles(from))keep&&!keep(rel)||add(join3(relDir,rel),resolve4(from,rel))};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)),addDir(CLAUDE_HOOKS_DIR,rel=>!rel.includes("/")&&rel.endsWith(".cjs")&&!rel.endsWith(".test.cjs"));for(let doc of paths.docs){let from=safeJoin(repoDir,doc);existsSync5(from)&&add(doc,from)}return addDir(paths.contracts),addDir(paths.floor,rel=>!/\.test\.(mjs|cjs)$/.test(rel)),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)existsSync5(safeJoin(projectRoot,rel))&&conflicts.push(rel);return conflicts.sort()}var MAX_LISTED=10;async function confirmWriteTargets(repoDir,cwd,selection){let conflicts=conflictingWriteTargets({repoDir,projectRoot:cwd,capabilities:selection.selected,layout:detectLayout(repoDir)});if(conflicts.length===0)return!0;let shown=conflicts.slice(0,MAX_LISTED),more=conflicts.length-shown.length,list=shown.map(p=>` \u2022 ${p}`).join(`
5
+ `),tail=more>0?`
6
+ \u2026and ${more} more`:"";return confirmWarning(`PHARN installs into your existing project. These paths already exist and may be overwritten:
7
+ ${list}${tail}`,"Continue and overwrite?",!1)}import{isCancel as isCancel2,note,select}from"@clack/prompts";function row(label,value){return`${` ${label}`.padEnd(28)}${value}`}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(`
8
+ `));let action=await select({message:"Ready to install?",initialValue:"install",options:[{value:"install",label:"Yes, install"},{value:"cancel",label:"Cancel"}]});return isCancel2(action)?"cancel":action}import{log as log3,outro,spinner}from"@clack/prompts";import pc2 from"picocolors";import{cpSync,existsSync as existsSync6,lstatSync,mkdirSync,readdirSync as readdirSync4}from"node:fs";var isTestFile=p=>/\.test\.(mjs|cjs)$/.test(p),isSymlink=p=>lstatSync(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(!existsSync6(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=existsSync6(settingsTo);!settingsPreserved&&existsSync6(settingsFrom)&&!isSymlink(settingsFrom)&&(mkdirSync(safeJoin(projectRoot,".claude"),{recursive:!0}),cpSync(settingsFrom,settingsTo,{force:!0}));for(let doc of paths.docs){let from=safeJoin(repoDir,doc);existsSync6(from)&&!isSymlink(from)&&cpSync(from,safeJoin(projectRoot,doc),{force:!0})}let contractsFrom=safeJoin(repoDir,paths.contracts);existsSync6(contractsFrom)&&!isSymlink(contractsFrom)&&cpSync(contractsFrom,safeJoin(projectRoot,paths.contracts),{recursive:!0,force:!0,filter:noSymlinks});let floorFrom=safeJoin(repoDir,paths.floor);return existsSync6(floorFrom)&&!isSymlink(floorFrom)&&cpSync(floorFrom,safeJoin(projectRoot,paths.floor),{recursive:!0,force:!0,filter:src=>!isTestFile(src)&&noSymlinks(src)}),{capabilities,settingsPreserved,layout:paths.layout}}function copyFilteredDir(repoDir,projectRoot,relDir,keep){let fromDir=safeJoin(repoDir,relDir);if(!existsSync6(fromDir))return;let entries=readdirSync4(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||(mkdirSync(safeJoin(projectRoot,relDir),{recursive:!0}),ensured=!0),cpSync(safeJoin(fromDir,entry.name),safeJoin(projectRoot,`${relDir}/${entry.name}`),{force:!0}))}var ModelRoutingError=class extends Error{constructor(message){super(message),this.name="ModelRoutingError"}},MODEL_IDS=["opus-4-8","sonnet-5","fable-5","haiku-4-5"],EFFORT_LEVELS=["low","high","max"],PIPELINE_STAGES=["plan","grill","build","regress","verify","review","ship"],DEFAULT_MODEL_ROUTING={default:{model:"sonnet-5",effort:"high"},stages:{plan:{model:"opus-4-8",effort:"max"},review:{model:"opus-4-8",effort:"high"}}},ROUTING_KEYS=["default","stages"],STAGE_MODEL_KEYS=["model","effort"];function assertNoUnknownKeys(obj,known,label){for(let key of Object.keys(obj))if(!known.includes(key))throw new ModelRoutingError(`${label} has unknown key ${JSON.stringify(key)} (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 key of Object.keys(obj))if(!known.includes(key))throw new SeamConfigError(`${label} has unknown key ${JSON.stringify(key)} (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 existsSync7,readFileSync as readFileSync3}from"node:fs";import{writeFile}from"node:fs/promises";import{resolve as resolve5}from"node:path";import{log as log2}from"@clack/prompts";var CONFIG_FILENAME="pharn.config.json";function configPath(cwd){return resolve5(cwd,CONFIG_FILENAME)}function readPharnConfig(cwd){let path=configPath(cwd);if(!existsSync7(path))return null;let raw;try{raw=JSON.parse(readFileSync3(path,"utf8"))}catch{return null}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,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)&&(log2.error(err.message),process.exit(1)),err}log2.error("No pharn.config.json found. Run `pharn init` first."),process.exit(1)}function isConfigValidationError(err){return err instanceof ModelRoutingError||err instanceof SeamConfigError}async function writePharnConfig(cwd,config){await writeFile(configPath(cwd),`${JSON.stringify(config,null,2)}
9
+ `,"utf8")}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)||(log2.error(LEGACY_CONFIG_MESSAGE),process.exit(1)),config}import{existsSync as existsSync8,readFileSync as readFileSync4}from"node:fs";var RAW="https://raw.githubusercontent.com",FETCH_TIMEOUT_MS2=8e3,MAX_BODY_BYTES=256*1024;function readSkillsVersion(repoDir){let path=safeJoin(repoDir,SKILLS_VERSION_FILE);if(!existsSync8(path))throw new ManifestValidationError(`${SKILLS_VERSION_FILE} is missing in the fetched repo.`);return assertSafeString(readFileSync4(path,"utf8").trim(),SKILLS_VERSION_FILE,VERSION_RE)}async function fetchRemoteSkillsVersion(){let url=`${RAW}/${REPO}/${REPO_BRANCH}/${SKILLS_VERSION_FILE}`,controller=new AbortController,timer=setTimeout(()=>controller.abort(),FETCH_TIMEOUT_MS2),res;try{res=await fetch(url,{redirect:"error",signal:controller.signal})}finally{clearTimeout(timer)}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 res.text();if(text.length>MAX_BODY_BYTES)throw new Error(`SKILLS_VERSION too large (${text.length} bytes) from ${url}`);return assertSafeString(text.trim(),SKILLS_VERSION_FILE,VERSION_RE)}async function runInstallArchetype(repoDir,cwd,archetypes,selection,commit){let startedAt=Date.now(),s=spinner();s.start("Installing capabilities");let capabilities,settingsPreserved,skillsVersion,layout;try{let result=installCapabilities(repoDir,cwd,selection);capabilities=result.capabilities,settingsPreserved=result.settingsPreserved,layout=result.layout,skillsVersion=readSkillsVersion(repoDir)}catch(err){throw s.stop("Failed to install capabilities"),err}s.stop("Capabilities installed"),settingsPreserved&&log3.warn("Existing .claude/settings.json preserved \u2014 compare it against pharn-oss to wire the PHARN hooks if needed.");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,layout};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):[];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 + docs written \u2192 ${pc2.dim(".claude/")}`,`${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("Change per-stage routing anytime in pharn.config.json \u2192 models.stages")}`,"",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(`
10
+ `))}async function runInit(){showBanner(),intro("init wizard"),runGitPrereq(),await runInitArchetype()}async function runInitArchetype(){let cwd=process.cwd(),{archetypes}=detectArchetypesFromProject(cwd);note2(archetypes.join(", "),"Detected archetypes");let s=spinner2();s.start(`Fetching PHARN from ${REPO_URL}`);let repo;try{repo=await fetchRepo()}catch(err){s.stop("Failed to fetch PHARN");let message=err instanceof Error?err.message:String(err);log4.error(`\u26A0 Could not reach ${REPO_URL}: ${message}`),process.env.PHARN_DEBUG&&console.error(err),process.exit(1)}s.stop(`PHARN fetched from ${REPO_URL}`);let outcome="cancelled",failure=null;try{let index=parseCapabilityIndex(repo.dir),selection=resolveCapabilities(archetypes,index);if(await runArchetypeSummary(archetypes,selection)==="install"&&await confirmWriteTargets(repo.dir,cwd,selection)){let commit=repo.sha;await runInstallArchetype(repo.dir,cwd,archetypes,selection,commit),outcome="installed"}}catch(err){failure=err instanceof Error?err.message:String(err),process.env.PHARN_DEBUG&&console.error(err)}finally{repo.cleanup()}failure&&(log4.error(`\u26A0 ${failure}`),process.env.PHARN_DEBUG||log4.info("Re-run with PHARN_DEBUG=1 for full error output."),process.exit(1)),outcome==="cancelled"&&cancelAndExit()}import{groupMultiselect,intro as intro2,isCancel as isCancel3,log as log5,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}}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)lines.push(` ${BULLET} ${cap.name}`)}}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}async function runAdd(capabilityArg){intro2("pharn add");let cwd=process.cwd(),config=loadArchetypeConfigOrExit(cwd);await runArchetypeAdd(config,cwd,capabilityArg)}async function runArchetypeAdd(config,cwd,arg){if(arg===void 0){await runAddPicker(config,cwd);return}let parsed=parseCapabilityArg(arg);parsed.error&&(log5.error(parsed.error),process.exit(1));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){s.stop("Failed to fetch capabilities"),log5.error(`\u26A0 ${err instanceof Error?err.message:String(err)}`),process.env.PHARN_DEBUG&&console.error(err),process.exit(1)}let result;try{result=await resolveArchetypeAdd(repo.dir,repo.sha,config,cwd,parsed,arg)}catch(err){process.env.PHARN_DEBUG&&console.error(err),result={kind:"error",message:err instanceof Error?err.message:String(err)}}finally{repo.cleanup()}if(result.kind==="error"&&(log5.error(`\u26A0 ${result.message}`),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})||(log5.error("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 s=spinner3();s.start(`Fetching capabilities from ${REPO_URL}`);let repo;try{repo=await fetchRepo(),s.stop(`Capabilities fetched from ${REPO_URL}`)}catch(err){s.stop("Failed to fetch capabilities"),log5.error(`\u26A0 ${err instanceof Error?err.message:String(err)}`),process.env.PHARN_DEBUG&&console.error(err),process.exit(1)}let outcome;try{outcome=await resolveAddPicker(repo.dir,repo.sha,config,cwd)}catch(err){process.env.PHARN_DEBUG&&console.error(err),outcome={kind:"error",message:err instanceof Error?err.message:String(err)}}finally{repo.cleanup()}if(outcome.kind==="error"&&(log5.error(`\u26A0 ${outcome.message}`),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),installed=config.capabilities??[],{groups,availableCount}=buildAddSelection(index,installed);if(availableCount===0)return{kind:"all-installed"};installed.length>0&&log5.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(isCancel3(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);result.kind==="added"?(log5.info(`${pc3.green("\u2714")} Added ${result.name}`),added.push(result.name),cfg={...cfg,capabilities:[...cfg.capabilities??[],{name:parsed.name,role:parsed.role}]}):result.kind==="noop"?log5.info(`${result.name} is already installed.`):log5.error(`\u26A0 ${result.message}`)}return{kind:"installed",added}}function plural(n){return n===1?"capability":"capabilities"}async function resolveArchetypeAdd(repoDir,sha,config,cwd,parsed,arg){let index=parseCapabilityIndex(repoDir),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:
11
+ ${valid.join(`
12
+ `)}`}}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};installCapabilityDirs(repoDir,cwd,[{name:cap.name,role:cap.role}]);let version=readSkillsVersion(repoDir),commit=sha,capabilities=[...existing,{name:cap.name,role:cap.role}];return await writePharnConfig(cwd,{...config,skillsVersion:version,commit,capabilities,installedAt:new Date().toISOString()}),{kind:"added",name:cap.name,version}}import{existsSync as existsSync9,rmSync as rmSync2}from"node:fs";import{confirm as confirm2,groupMultiselect as groupMultiselect2,intro as intro3,isCancel as isCancel4,log as log6,outro as outro3}from"@clack/prompts";import pc4 from"picocolors";async function runRemove(arg,_opts={}){intro3("pharn remove");let cwd=process.cwd(),config=loadArchetypeConfigOrExit(cwd);await removeCapability(cwd,config,arg)}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 subtree=target.role==="griller"?paths.grillers:paths.lenses,dir=safeJoin(cwd,`${subtree}/${target.name}`),existed=existsSync9(dir);return existed&&rmSync2(dir,{recursive:!0,force:!0}),existed}async function removeNamed(cwd,config,installed,address){let parsed=parseCapabilityArg(address);parsed.error&&(log6.error(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.";log6.warn(`"${address}" is not an installed capability.${hint}`),outro3("Nothing was removed.");return}matches.length>1&&(log6.error(`"${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=deleteCapabilityDir(cwd,paths,target)?"":pc4.dim(" (its files were already gone)");await writePharnConfig(cwd,{...config,capabilities:installed.filter(c=>!(c.name===target.name&&c.role===target.role)),installedAt:new Date().toISOString()}),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})||(log6.error("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});isCancel4(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 confirm2({message:`Remove ${targets.length} ${plural2(targets.length)}: ${targets.map(t=>`${t.name} (${t.role})`).join(", ")}?`,initialValue:!1});(isCancel4(ok)||ok!==!0)&&cancelAndExit();let paths=layoutPaths(configLayout(config));for(let target of targets)deleteCapabilityDir(cwd,paths,target);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()}),outro3(`${pc4.green("\u2714")} Removed ${targets.length} ${plural2(targets.length)}.`)}function plural2(n){return n===1?"capability":"capabilities"}import{confirm as confirm3,intro as intro4,isCancel as isCancel5,log as log7,note as note3,outro as outro4,spinner as spinner4}from"@clack/prompts";import pc5 from"picocolors";async function runUpdate(){intro4("pharn update");let cwd=process.cwd(),config=loadArchetypeConfigOrExit(cwd);await runArchetypeUpdate(config,cwd)}async function runArchetypeUpdate(config,cwd){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"),log7.error(`\u26A0 ${err instanceof Error?err.message:String(err)}`),process.env.PHARN_DEBUG&&console.error(err),process.exit(1)}if(config.skillsVersion===latest){outro4(`Already up to date (skills v${config.skillsVersion}).`);return}note3([row("Skills version",`v${config.skillsVersion} \u2192 v${latest}`),"",row("Archetypes",(config.archetypes??[]).join(", ")||"(none)"),"",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(`
13
+ `));let ok=await confirm3({message:"Re-fetch capabilities at the latest version?",initialValue:!0});(isCancel5(ok)||ok!==!0)&&cancelAndExit();let s2=spinner4();s2.start(`Updating from ${REPO_URL}`);let repo;try{repo=await fetchRepo()}catch(err){s2.stop("Update failed"),log7.error(`\u26A0 ${err instanceof Error?err.message:String(err)}`),process.env.PHARN_DEBUG&&console.error(err),process.exit(1)}let installedVersion=null,capCount=0,failure=null;try{let index=parseCapabilityIndex(repo.dir),selection=resolveCapabilities(config.archetypes??[],index);installCapabilities(repo.dir,cwd,selection);let capabilities=selection.selected.map(c=>({name:c.name,role:c.role}));installedVersion=readSkillsVersion(repo.dir),capCount=capabilities.length;let commit=repo.sha;await writePharnConfig(cwd,{...config,skillsVersion:installedVersion,commit,capabilities,installedAt:new Date().toISOString()}),s2.stop("Capabilities updated")}catch(err){s2.stop("Update failed"),failure=err instanceof Error?err.message:String(err),process.env.PHARN_DEBUG&&console.error(err)}finally{repo.cleanup()}failure&&(log7.error(`\u26A0 ${failure}`),process.env.PHARN_DEBUG||log7.info("Re-run with PHARN_DEBUG=1 for full error output."),process.exit(1)),outro4(`${pc5.green("\u2714")} Updated to skills v${installedVersion} (${capCount} capabilit${capCount===1?"y":"ies"}). ${pc5.dim("CONSTITUTION.md left untouched.")}`)}import{intro as intro5,log as log8,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}))}}function renderArchetypeHuman(inv){let lines=renderCapabilityLines(inv);note4(lines.join(`
14
+ `),"INSTALLED (archetype)"),outro5(pc6.dim("Read-only \u2014 nothing changed."))}function emitError(message,json){json?console.error(message):log8.error(message)}import{intro as intro6,log as log9,note as note5,outro as outro6,spinner as spinner5}from"@clack/prompts";import pc7 from"picocolors";import{createHash}from"node:crypto";import{existsSync as existsSync10,readFileSync as readFileSync5}from"node:fs";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=[],okCount=0;for(let[rel,repoPath]of expected){let diskPath=safeJoin(baseDir,rel);existsSync10(diskPath)?hash(repoPath)===hash(diskPath)?okCount+=1:modified.push(rel):missing.push(rel)}return modified.sort(),missing.sort(),{modified,missing,okCount}}function hash(path){return createHash("sha256").update(readFileSync5(path)).digest("hex")}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;if(!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"),reportError(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}`),reportError(err),process.exit(1)}let exitCode=0;try{let outdated=printArchetypeVersion(config,readSkillsVersion(repo.dir));printModelRouting(config);let result=diffInstalledCapabilities({repoDir:repo.dir,projectRoot:cwd,capabilities:config.capabilities??[],layout:configLayout(config)});printDriftSection(result),strict&&(outdated||result.modified.length||result.missing.length)&&(exitCode=1)}catch(err){reportError(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(`
15
+ `),"VERSION"),outdated}function printModelRouting(config){config.models!==void 0&&note5(formatModelRoutingLines(config.models).join(`
16
+ `),"MODELS")}function printDriftSection(result){if(result.modified.length===0&&result.missing.length===0){note5(`No drift \u2014 ${result.okCount} file(s) match ${REF}.`,"DRIFT");return}let lines=[];if(result.modified.length){lines.push(" LOCALLY MODIFIED (PHARN-owned)");for(let p of result.modified)lines.push(` ${p}`);lines.push(pc7.dim(" `pharn update` will overwrite these."))}if(result.missing.length){lines.length&&lines.push(""),lines.push(" MISSING (expected but absent)");for(let p of result.missing)lines.push(` ${p}`);lines.push(pc7.dim(" Re-run `pharn update` (or `pharn add`) to restore them."))}lines.push("",pc7.dim(` ${result.okCount} file(s) match ${REF}.`)),note5(lines.join(`
17
+ `),"DRIFT")}function reportError(err){let message=err instanceof Error?err.message:String(err);log9.error(`\u26A0 ${message}`),process.env.PHARN_DEBUG&&console.error(err)}var USAGE=`Pharn - Installs PHARN (an audit-grade methodology for Claude Code) into your project. npx @pharn-dev/pharn init detects your project's archetype and installs the applicable PHARN capabilities; pharn add installs another capability later; pharn update bumps to the latest skills version.
18
+
19
+ Usage:
20
+ pharn [command] [options]
21
+
22
+ Commands:
23
+ init Detect archetypes and install capabilities (default)
24
+ add [capability] Add a capability, e.g. a11y (no arg: pick interactively)
25
+ remove [capability] Remove an installed capability (no arg: pick interactively)
26
+ update Re-fetch installed capabilities at the latest version
27
+ list List installed archetypes + capabilities
28
+ status Show version + local-drift status (read-only)
29
+
30
+ Options:
31
+ --archetype init: deprecated no-op \u2014 archetype detection is now the default
32
+ --strict Make status exit 1 on any outdated/modified/missing file
33
+ --no-drift Skip the status byte-level drift check
34
+ --json Emit list output as JSON
35
+ -h, --help Show this help text
36
+ -v, --version Show the version number`;async function main(){let argv=minimist(process.argv.slice(2),{boolean:["help","version","json","yes","strict","drift","archetype"],default:{drift:!0},alias:{h:"help",v:"version",y:"yes"}});if(argv.version){console.log(PHARN_VERSION);return}if(argv.help){console.log(USAGE);return}let cmd=argv._[0]??"init";switch(cmd){case"init":await runInit();return;case"add":await runAdd(argv._[1]);return;case"remove":case"rm":await runRemove(argv._[1],{yes:!!argv.yes});return;case"update":await runUpdate();return;case"list":await runList({json:!!argv.json});return;case"status":await runStatus({strict:!!argv.strict,drift:argv.drift!==!1});return;default:console.error(`Unknown command: ${cmd}
37
+ `),console.error(USAGE),process.exit(1)}}function isEntryPoint(){if(!process.argv[1])return!1;try{return realpathSync(process.argv[1])===fileURLToPath(import.meta.url)}catch{return!1}}isEntryPoint()&&main().catch(err=>{console.error(err),process.exit(1)});export{main};
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@pharn-dev/pharn",
3
+ "version": "0.3.0",
4
+ "description": "Audit-grade AI development methodology for Claude Code — spec, plan, grill, build, verify, ship.",
5
+ "keywords": [
6
+ "claude-code",
7
+ "ai",
8
+ "methodology",
9
+ "code-review",
10
+ "audit",
11
+ "agents"
12
+ ],
13
+ "license": "Apache-2.0",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/pharn-dev/pharn-cli.git"
17
+ },
18
+ "bugs": {
19
+ "url": "https://github.com/pharn-dev/pharn-cli/issues"
20
+ },
21
+ "homepage": "https://github.com/pharn-dev/pharn-cli#readme",
22
+ "type": "module",
23
+ "bin": {
24
+ "pharn": "dist/index.js"
25
+ },
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "engines": {
30
+ "node": ">=20"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public",
34
+ "provenance": false
35
+ },
36
+ "scripts": {
37
+ "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc --noEmit && node scripts/build.mjs",
38
+ "build:install-local": "npm run build && node scripts/install-local.mjs",
39
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
40
+ "lint": "eslint src",
41
+ "format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\" \"*.config.ts\"",
42
+ "format:check": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\" \"*.config.ts\"",
43
+ "lint:md": "markdownlint-cli2 \"docs/**/*.md\" \"*.md\"",
44
+ "test": "vitest run",
45
+ "test:watch": "vitest",
46
+ "test:coverage": "vitest run --coverage",
47
+ "check": "npm run format:check && npm run lint && npm run typecheck && npm run test",
48
+ "prepack": "npm run build",
49
+ "prepublishOnly": "npm run check"
50
+ },
51
+ "dependencies": {
52
+ "@clack/prompts": "^1.7.0",
53
+ "degit": "^3.6.1",
54
+ "minimist": "^1.2.8",
55
+ "picocolors": "^1.1.1"
56
+ },
57
+ "devDependencies": {
58
+ "@eslint/js": "^10.0.1",
59
+ "@types/minimist": "^1.2.5",
60
+ "@types/node": "^26.1.1",
61
+ "@vitest/coverage-v8": "^4.1.9",
62
+ "eslint": "^10.7.0",
63
+ "markdownlint-cli2": "^0.23.1",
64
+ "prettier": "^3.9.6",
65
+ "tsx": "^4.23.1",
66
+ "esbuild": "^0.28.1",
67
+ "typescript": "^6.0.3",
68
+ "typescript-eslint": "^8.65.0",
69
+ "vitest": "^4.1.10"
70
+ }
71
+ }