Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **`pharn remove <module | category:skill>`** β€” the inverse of `pharn add`. Removing a
skill is a precise single-directory delete with no clone or network; removing a module
clones once, computes the exact files that module contributed (so shared directories like
`commands/` keep other modules' files), deletes only those, and prunes emptied directories.
Refuses to remove `pharn-core` or a module with installed dependents, never touches
`CONSTITUTION.md` / `memory-bank/`, and updates `pharn.config.json` to match. No arg opens
an interactive picker; `--yes`/`-y` skips the confirmation and `rm` is an alias.
- **`pharn list`** β€” a read-only inventory of installed vs. available modules and
`category:skill` skills, with update markers when the manifest is newer. Adds `--json`
for scripting/CI (single object on stdout; diagnostics on stderr). Never writes or clones.
- **`pharn status`** β€” a read-only audit of the install: a version section (is `skillsVersion`
/ each module current?) and a drift section that clones `pharn-dev/pharn-oss@main` and
byte-compares every PHARN-owned file against `.claude/`, reporting locally-modified and
missing files. Never writes, deletes, or overwrites β€” the temporary clone is always cleaned
up. `.claude/CONSTITUTION.md` and `.claude/memory-bank/` are excluded (hand-edited, anchored
at the root, so `templates/` is still diffed). `--strict` exits 1 on any drift/outdated for
CI; `--no-drift` skips the clone and checks the version only.
- Repo-health tooling: `CHANGELOG.md`, GitHub issue/PR templates, `CODEOWNERS`,
Dependabot config, markdownlint for docs, an aggregate `npm run check` script, and
an enforced test-coverage gate in CI.
Expand All @@ -21,6 +38,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Docs: completed the getting-started day-to-day loop with `/pharn-regress` and refreshed the
`pharn.config.json` example module versions to match the current `pharn-oss` manifest
(`skillsVersion` 0.70.0).
- Docs: the root `README.md` Commands table and the `pharn -h` help text now document the
already-implemented `add <category>:<skill>` form (install one technology skill, e.g.
`orm:prisma`) alongside the whole-module `add <module>` form. No CLI code change.

## [0.2.0] β€” 2026-06-11

Expand Down
6 changes: 5 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ ESM-only (`"type": "module"`, NodeNext). **Relative imports must use `.js` exten

## Architecture

`src/index.ts` parses argv with minimist and dispatches to `commands/{init,add,update}.ts`. `init` is the default command.
`src/index.ts` parses argv with minimist and dispatches to `commands/{init,add,remove,update,list,status}.ts`. `init` is the default command. `list` is read-only (reads `pharn.config.json` + the remote manifest; no clone, no writes) and shares the `add`/`update` no-config exit + manifest-fetch patterns; `--json` emits a single inventory object on stdout (diagnostics to stderr). `status` is also read-only (the read side of `update`) β€” see its addressing note below. `remove` (alias `rm`, `--yes`/`-y` skips its confirm) is the inverse of `add` β€” see its addressing note below.

**`commands/init.ts` is a step pipeline.** Each `steps/*.ts` file is one stage and uses `@clack/prompts` for I/O:

Expand All @@ -54,6 +54,10 @@ ESM-only (`"type": "module"`, NodeNext). **Relative imports must use `.js` exten

**`pharn add` addressing.** `add <module>` installs a whole methodology module / stack pack (v1 + v2). `add <category>:<skill>` (v2 only, e.g. `add orm:prisma`) maps `<category>` β†’ `pharn-skills-<category>`, resolves the wizard option, and installs just that skill: already-installed β†’ no-op; a different sibling of the same category already installed β†’ confirm before installing alongside (appends to `installedSkills`, **never** edits `stackAnswers`); unknown β†’ lists valid addresses. `pharn update` re-resolves `installedSkills` against the new wizard and drops (reporting, never guessing) any `from` path that no longer exists upstream.

**`pharn remove` addressing** (`commands/remove.ts`) is the inverse, with the same two forms. `remove <category>:<skill>` deletes that one isolated `.claude/skills/<basename>/` dir and drops its `installedSkills` entry β€” **no clone, no network** (everything is derivable from `installedSkills` + the filesystem), **never** edits `stackAnswers`/`modules`; not-installed β†’ no-op listing the installed (removable) skills. `remove <module>` clones once and reads the manifest **from the clone** (`@main` HEAD, not the pinned `commit` β€” hence a possible orphan if a path was renamed upstream; documented), then **refuses** (before any write/delete) `pharn-core`, an uninstalled module, or one with installed dependents (named). Because modules merge into shared dest dirs, it never `rm -rf`s a dest dir: it `statSync`-branches each `installs` entry (file vs. dir), walks the source to map the **exact** contributed files into `.claude/`, deletes only those, prunes emptied dirs (never one holding a surviving module's files), and **never** touches `CONSTITUTION.md`/`memory-bank/`. No arg β†’ an interactive picker that pre-applies these guards so it only offers genuinely removable items. The shared delete core is `planAndApplyModuleRemoval`; cleanup runs in a `finally` and all `process.exit`/`cancelAndExit` calls happen *after* it (Node skips `finally` on `process.exit`).

**`pharn status` (`commands/status.ts`) is strictly read-only** β€” it never writes, deletes, or overwrites (fixing is `update`/`add`). Two sections: a **version** check (reuses `update`'s `skillsVersion` + per-module `changes` diff; overlaps `list` by design) and a **drift** check. Default clones `@main` once and reuses it for both (the HEAD manifest is the "latest" for version *and* the source for drift); `--no-drift` skips the clone and uses `fetchRemoteManifest` for the version section only; `--strict` exits 1 on any outdated/modified/missing (CI gate, default exit 0). Cleanup runs in a `finally`, and every `process.exit` happens *after* it. The pure (no I/O) engine is **`lib/diff.ts` β†’ `diffInstalled`**: it mirrors `installModule`/`installSkills` to derive the **expected** file set (`statSync`-branching each `installs` entry β€” file vs. dir β€” and walking dirs; skills β†’ `skills/<basename>/`), then `sha256`-compares each against `.claude/`, returning `{modified, missing, okCount}`. It reuses `install-modules.ts`'s exported `safeJoin` to guard every read. **It excludes the two user-owned surfaces β€” `CONSTITUTION.md` and `memory-bank/` β€” anchored at the `.claude/` root** (so the PHARN-owned `templates/memory-bank/` sources are still diffed); this exclusion is a correctness requirement. Drift is derived live from the clone (no stored hashes), always against `@main`, never the pinned `commit`.

## Testing

Tests live in `tests/*.test.ts` (vitest, node env). `tests/helpers.ts` provides `stubProcessExit` (turns `process.exit` into a throwable `ProcessExit`), `useTmpDir`, and `CANCEL`. The lib tests build fake fetched-repos on disk to exercise copy/materialize without network. When changing behavior, update the matching test before touching code.
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ The wizard also asks a **privacy-posture** question and writes the matching cons
Full reference: **[docs/](docs/README.md)**

- [Getting started](docs/getting-started.md)
- [Commands](docs/commands/init.md) β€” `init`, `add`, `update`
- [Commands](docs/commands/init.md) β€” `init`, `add`, `remove`, `update`, `list`, `status`
- [pharn.config.json](docs/reference/pharn-config.md)
- [Troubleshooting](docs/troubleshooting.md)

Expand All @@ -44,7 +44,11 @@ See [Getting started](docs/getting-started.md) for the full flow and fresh-proje
| ------- | ----------- |
| `pharn init` | Interactive setup wizard (default) |
| `pharn add <module>` | Add a module to an existing PHARN project |
| `pharn add <category>:<skill>` | Add one technology skill (e.g. `orm:prisma`) |
| `pharn remove <module\|category:skill>` | Remove a module or skill from this project |
| `pharn update` | Update installed modules to the latest skills version |
| `pharn list` | List installed and available modules/skills |
| `pharn status` | Show version + local-drift status (read-only; `--strict`, `--no-drift`) |
| `pharn -h`, `--help` | Show help |
| `pharn -v`, `--version` | Show version |

Expand Down
3 changes: 3 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@

- [init](commands/init.md) β€” interactive setup wizard
- [add](commands/add.md) β€” add a module to an existing project
- [remove](commands/remove.md) β€” remove a module or skill from an existing project
- [update](commands/update.md) β€” update installed modules to the latest skills version
- [list](commands/list.md) β€” list installed and available modules/skills
- [status](commands/status.md) β€” read-only version + local-drift audit

## Reference

Expand Down
51 changes: 51 additions & 0 deletions docs/commands/list.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# pharn list

Show what PHARN modules and skills are installed in this project (one with a `pharn.config.json`) and what is still available to add. **Read-only** β€” it never writes files and never clones the repo.

```bash
pharn list # human-readable inventory
pharn list --json # machine-readable JSON (for scripts / CI)
```

## Behavior

1. Reads `pharn.config.json`. If none exists, exits with a hint to run `pharn init` first.
2. Fetches `manifest.json` to learn the latest versions and the available catalog. (No clone β€” only the same lightweight fetch `pharn update` uses.)
3. Prints **INSTALLED**:
- your skills version, flagged `β†’ vX (update available, run pharn update)` when the manifest is newer;
- each installed module with its version, flagged `β†’ vX` when the manifest has a newer one;
- any individually installed technology skills (schemaVersion 2).
4. Prints **AVAILABLE TO ADD**:
- optional modules and stack packs you don't have yet β€” the same set `pharn add` offers;
- on a `schemaVersion 2` manifest, every `category:skill` you haven't installed.

A group with nothing left shows `(all installed)`.

Nothing is ever written, and the repo is never cloned.

## JSON output

`pharn list --json` prints a single JSON object and nothing else (no spinner, intro, or outro), suitable for scripting:

```json
{
"skillsVersion": "0.69.0",
"latestSkillsVersion": "0.70.0",
"installed": {
"modules": [{ "name": "pharn-core", "version": "0.1.0", "latest": "0.1.0" }],
"skills": [{ "skill": "prisma", "from": "pharn-skills-orm/skills/prisma" }]
},
"available": {
"modules": [{ "name": "pharn-review", "version": "0.4.0", "description": "…" }],
"skills": [{ "category": "orm", "skill": "drizzle", "install": "pharn-skills-orm/skills/drizzle" }]
}
}
```

Diagnostics (such as a missing config or a failed fetch) go to **stderr**, and the exit code is non-zero on failure β€” so stdout always parses cleanly.

## Related

- [add](add.md)
- [update](update.md)
- [pharn.config.json](../reference/pharn-config.md)
55 changes: 55 additions & 0 deletions docs/commands/remove.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# pharn remove

Remove a PHARN module or a single technology skill from an existing project (one with a `pharn.config.json`). The inverse of [`add`](add.md): it deletes exactly the files the module or skill contributed and updates `pharn.config.json` to match.

```bash
pharn remove <module> # a whole methodology module / stack pack
pharn remove <category>:<skill> # a single technology skill (schemaVersion 2)
pharn remove # interactive picker
pharn remove <module> --yes # skip the confirmation prompt
```

`rm` is accepted as an alias for `remove`.

## Behavior

1. Reads `pharn.config.json`. If none exists, exits with a hint to run `pharn init` first.
2. An argument containing `:` removes a **skill** (Β§A); any other argument removes a **module** (Β§B); no argument opens the **interactive picker**, which lists only items that are genuinely removable (so you never pick something the command would refuse).

`CONSTITUTION.md` and everything under `memory-bank/` are **never** deleted β€” they are user-owned and materialized only at `init`.

## Removing a skill (`<category>:<skill>`)

Each skill lives in its own isolated `.claude/skills/<name>/` directory, so removal is precise and needs **no network and no clone** β€” the CLI works entirely from `installedSkills` plus your filesystem.

```bash
pharn remove orm:prisma # deletes .claude/skills/prisma/
pharn remove auth:clerk # deletes .claude/skills/clerk/
```

- Deletes the skill's directory and drops its entry from `installedSkills`.
- **Does not** touch `stackAnswers`, `modules`, `skillsVersion`, or `commit` β€” your recorded wizard answer stays authoritative.
- **Not installed** β†’ a no-op (nothing is written). For an unrecognized address, the CLI lists the skills you actually have installed as the valid values.
- Already-deleted directory β†’ treated as done (idempotent).

## Removing a module (`<module>`)

Modules share destination directories (`pharn-core`, `pharn-pipeline`, `pharn-review`, and `pharn-audits` all merge into `commands/`, `skills/`, `hooks/`, …), so `remove` never wipes a directory. It clones `pharn-dev/pharn-oss` once, reads the module's `installs` from the clone, computes the **exact** set of files that module contributed, and deletes only those β€” then prunes any directories left empty (never one that still holds another module's files).

Refusals (checked **before** anything is deleted or written β€” no partial removals):

- **`pharn-core`** is required by every other module and cannot be removed.
- A module that **is not installed** is reported and nothing happens.
- A module with **installed dependents** is refused, naming them β€” remove those first. (There is no automatic cascade.) This also blocks removing a stack pack's base, e.g. `pharn-stack-react` while `pharn-stack-nextjs` is installed.

Unless `--yes` / `-y` is given, the CLI confirms before deleting. `modules` in `pharn.config.json` is rewritten to the resolved survivor set; `skillsVersion`, `commit`, `stackAnswers`, and `installedSkills` are left unchanged.

### Orphan caveat

The contributed file set is computed from the repository's `main` branch, **not** the pinned `commit` recorded at install. If a file was renamed or removed upstream since you installed, an orphaned copy may remain in `.claude/`. This is acceptable for now; `pharn status` is planned to surface such orphans (see the [roadmap](../roadmap.md)).

## Related

- [add](add.md)
- [list](list.md)
- [pharn.config.json](../reference/pharn-config.md)
40 changes: 40 additions & 0 deletions docs/commands/status.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# pharn status

Read-only audit of your install: is it on the latest version, and have any PHARN-owned files drifted from upstream?

```bash
pharn status
pharn status --no-drift # version check only (skips the clone)
pharn status --strict # exit 1 if outdated, modified, or missing (for CI)
```

`status` is the read side of [`update`](update.md): it surfaces the same state `update` would overwrite, but **never writes, deletes, or overwrites anything**. It is a report, not a guard.

## Behavior

1. Reads `pharn.config.json`. If none exists, exits with a hint to run `pharn init` first.
2. By default, clones `pharn-dev/pharn-oss@main` once and reuses it for both sections below (the temporary clone is always cleaned up). `--no-drift` skips the clone and uses the lightweight `manifest.json` fetch for the version section only.
3. **Version** β€” compares your `skillsVersion` and per-module versions against the manifest, then reports either "up to date" or that an update is available (run `pharn update`). This overlaps with part of [`pharn list`](list.md) by design.
4. **Drift** β€” derives the set of files each installed module and skill is expected to contribute (mirroring how `init` / `add` / `update` install them), then byte-compares each against your `.claude/`:
- **Locally modified (PHARN-owned)** β€” files present but whose contents differ. `pharn update` will overwrite these.
- **Missing (expected but absent)** β€” files an installed module/skill expects that aren't on disk. Re-run `pharn update` (or `pharn add`) to restore them.
- If neither, reports **No drift**.

The comparison is always against `pharn-dev/pharn-oss@main` (the same ref the rest of the CLI installs from), not the `commit` pinned in your config.

## What is intentionally excluded

`.claude/CONSTITUTION.md` and everything under `.claude/memory-bank/` are **never** flagged. They are materialized once at `init` and are meant to be hand-edited β€” `update` never touches them, so reporting local edits to them would be noise.

This exclusion is anchored at the `.claude/` root. The pristine template **sources** under `.claude/templates/` (including `templates/memory-bank/` and `templates/constitution/`) are PHARN-owned and **are** diffed β€” only the materialized working copies at the root are skipped.

## Exit code

Exits `0` by default, even when drift or an available update is found (it is a report). Pass `--strict` to exit `1` whenever anything is outdated, modified, or missing β€” useful as a CI gate.

## Related

- [update](update.md) β€” apply the fixes `status` reports (overwrite drift, restore missing)
- [list](list.md) β€” read-only inventory of installed vs. available modules/skills
- [add](add.md) β€” install a module or a single `category:skill`
- [pharn.config.json](../reference/pharn-config.md) β€” `skillsVersion`, `modules`, `installedSkills`
7 changes: 7 additions & 0 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ What PHARN CLI does today versus what is planned.
| Materialize `memory-bank/` and the chosen `CONSTITUTION.md` | Shipped |
| Pin commit SHA + write `pharn.config.json` | Shipped |
| `pharn add <module>` β€” add a module to an existing project | Shipped |
| `pharn remove <module \| category:skill>` β€” remove a module or skill | Shipped |
| `pharn update` β€” refresh installed modules to the latest version | Shipped |
| `pharn list` β€” show installed + available modules/skills (read-only, `--json`) | Shipped |
| `pharn status` β€” read-only version + local-drift report (modified/missing PHARN-owned files; `--strict`, `--no-drift`) | Shipped |

## Planned

Expand All @@ -25,12 +28,16 @@ What PHARN CLI does today versus what is planned.
| Stack scaffolding | Install npm packages / generate app code from the stack pack |
| Additional stack packs | Beyond `pharn-stack-nextjs` |
| Migration for existing projects | Onboard repos with significant git history (today the CLI only warns) |
| Orphaned-file detection in `pharn status` | `status` today reports modified + missing PHARN-owned files; flagging files left orphaned in `.claude/` after an upstream rename (see [remove](commands/remove.md)) is not built yet |
| Other agents | Codex and Cursor in addition to Claude Code |

## Related

- [Getting started](getting-started.md)
- [init](commands/init.md)
- [add](commands/add.md)
- [remove](commands/remove.md)
- [update](commands/update.md)
- [list](commands/list.md)
- [status](commands/status.md)
- [pharn.config.json](reference/pharn-config.md)
Loading