plain-forge 1.0.11 → 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +21 -60
  2. package/bin/cli.mjs +120 -5
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -42,9 +42,9 @@ Each skill operates on the same one-question-at-a-time, write-immediately, refin
42
42
 
43
43
  plain-forge ships as a set of skills, rules, and docs that plug into your AI coding tool of choice. Install it once, then invoke `forge-plain` (or `add-feature` to add a feature to an existing ***plain project) from any project.
44
44
 
45
- ### Install with `npx plain-forge install` (recommended)
45
+ ### Install with `npx plain-forge install`
46
46
 
47
- The primary install path. Works for every supported runtime and is the only installer that ships **all** plain-forge content (skills, rules, **and** docs) — the other methods below are limited or agent-specific.
47
+ The one and only way to install plain-forge. It works for every supported runtime and ships **all** plain-forge content (skills, rules, **and** docs).
48
48
 
49
49
  ```bash
50
50
  npx plain-forge install
@@ -95,39 +95,26 @@ Each deprecated file is confirmed individually before it's deleted — you'll se
95
95
 
96
96
  Installs that predate the manifest (anyone who installed before this feature existed) have no manifest to read. `update` still finds them by their skill footprint: if the `forge-plain`, `add-feature`, `debug-specs`, and `load-plain-reference` skills are all present in an agent directory, it's treated as a plain-forge install. Such installs are refreshed without pruning (overwrite-only), and gain a manifest going forward so later updates can prune.
97
97
 
98
- ### Alternative install paths (skills only — no rules or docs)
99
-
100
- These work but only install the skill files. Rules and docs do **not** travel with them, so use them only if you have a reason not to use `npx plain-forge install`.
101
-
102
- #### `npx skills` CLI
98
+ #### Removing an install
103
99
 
104
100
  ```bash
105
- npx skills add Codeplain-ai/plain-forge --skill '*' --agent claude-code
106
- ```
107
-
108
- Replace `--agent claude-code` with `codex` or `opencode` to target a different runtime, or repeat the flag for several at once.
109
-
110
- #### Claude Code native plugin flow
111
-
112
- Requires the [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code). Inside a Claude Code session, run the following **three commands** one after the other:
113
-
114
- ```text
115
- /plugin marketplace add Codeplain-ai/plain-forge
116
- /plugin install plain-forge@plain-forge
117
- /reload-plugins
101
+ npx plain-forge uninstall
118
102
  ```
119
103
 
120
- Without the reload the skills won't appear in the current session.
104
+ `uninstall` reads the install manifest (`<agent-dir>/.plain-forge/manifest.json`) and deletes **exactly** the files plain-forge wrote, then the manifest itself, then any directory left empty (including the agent directory). Your own skills and third-party content are never in the manifest, so they are never touched.
121
105
 
122
- #### Codex native plugin flow
123
-
124
- Requires the [OpenAI Codex CLI](https://developers.openai.com/codex/cli/reference). From your shell:
106
+ By default it removes **every** agent layout in the **project** scope (the current folder). Narrow it with flags:
125
107
 
126
108
  ```bash
127
- codex plugin marketplace add Codeplain-ai/plain-forge
109
+ npx plain-forge uninstall --agent claude --scope global
128
110
  ```
129
111
 
130
- Then, inside Codex, open the plugin directory, pick the `plain-forge` marketplace, and install the plugin from there. (Codex's CLI does not currently expose a `codex plugin install` equivalent.)
112
+ | Flag | Default | Values |
113
+ |------|---------|--------|
114
+ | `--agent` | `*` (all agents) | `claude`, `codex`, `forgecode`, `universal`, or `*` |
115
+ | `--scope` | `project` | `project` (cwd) or `global` (home directory) |
116
+
117
+ If an install has **no manifest** (e.g. one that predates manifests), `uninstall` cannot tell which files are plain-forge's, so it refuses to delete anything: it prints an error, lists the directories to clean up by hand, and exits non-zero. Refresh such an install with `update` first (which writes a manifest going forward), then `uninstall`.
131
118
 
132
119
  ## Usage
133
120
 
@@ -188,49 +175,23 @@ Hit a bug in the rendered app, a failing test, or behavior that doesn't match wh
188
175
 
189
176
  ## Repository Structure
190
177
 
191
- plain-forge keeps a single canonical source of truth under `forge/` and uses tiny per-runtime adapters to regenerate the directory layout each AI tool expects. The generated outputs are committed so existing install commands keep working — no build step is needed for end users.
178
+ plain-forge keeps a single canonical source of truth under `forge/`. The `plain-forge install` CLI copies that content straight into whichever agent directory you choose there is no build step and no generated, committed output to keep in sync.
192
179
 
193
180
  ```
194
- forge/ # canonical, runtime-neutral content
181
+ forge/ # canonical content, copied verbatim on install
195
182
  skills/ # all skills used during spec writing
196
- rules/ # workspace rules for spec validation
197
-
198
- runtimes/ # per-runtime adapters
199
- claude/
200
- build.ts # generates .claude/ + .claude-plugin/ from forge/
201
- templates/ # Claude-specific files: settings.json, hook script, plugin manifests
202
- codex/
203
- build.ts # generates .codex-plugin/ and .agents/plugins/ (manifest points at forge/skills/)
204
- templates/ # Codex-specific files: plugin.json, marketplace catalog
205
- opencode/
206
- build.ts # generates .opencode/ from forge/
207
- templates/ # OpenCode-specific files: package.json, .gitignore
183
+ rules/ # spec-writing rules (installed as workspace instructions)
208
184
 
209
185
  bin/
210
- forge-build.ts # orchestrator: runs every runtimes/*/build.ts
211
- lib.ts # shared symlink/copy helpers
212
-
213
- # Generated outputs (committed, do not edit by hand):
214
- .claude/ # Claude Code plugin layout
215
- .claude-plugin/ # Claude Code plugin manifests
216
- .codex-plugin/ # Codex plugin manifest (its "skills" field points at forge/skills/)
217
- .agents/plugins/ # Codex marketplace catalog
218
- .opencode/ # OpenCode plugin layout
219
- ```
186
+ cli.mjs # the `plain-forge` CLI — `install` and `update` commands
220
187
 
221
- ### Contributing
188
+ test/
189
+ cli.test.mjs # tests for the install / update CLI
222
190
 
223
- After editing anything under `forge/` or `runtimes/*/templates/`, regenerate the runtime outputs:
224
-
225
- ```bash
226
- npm install # required after every fresh clone (node_modules/ is gitignored)
227
- npm run build # regenerate runtime outputs for Claude, Codex, OpenCode
228
- npm run clean # remove generated outputs and rebuild from scratch
191
+ package.json # ships only `bin/cli.mjs` and `forge/` to npm
229
192
  ```
230
193
 
231
- If `npm run build` errors with `sh: tsx: command not found`, it means `node_modules/` is missing run `npm install` first.
232
-
233
- The build is idempotent — re-running it produces no `git diff`.
194
+ On `install`, the CLI reads `forge/skills` and `forge/rules` and writes them into the chosen agent directory (`.claude/`, `.codex/`, `.forgecode/`, or `.agents/`), recording every file it wrote in `<agent-dir>/.plain-forge/manifest.json` so `update` can later refresh and prune precisely.
234
195
 
235
196
  ## Available Skills
236
197
 
package/bin/cli.mjs CHANGED
@@ -78,8 +78,9 @@ function usage() {
78
78
  console.log(`Usage: plain-forge <command> [options]
79
79
 
80
80
  Commands:
81
- install Install plain-forge into an agent directory
82
- update Refresh every existing plain-forge install in cwd and $HOME
81
+ install Install plain-forge into an agent directory
82
+ update Refresh every existing plain-forge install in cwd and $HOME
83
+ uninstall Remove a plain-forge install using its manifest
83
84
 
84
85
  Install options:
85
86
  --agent <claude|codex|forgecode|universal> Target agent layout
@@ -90,16 +91,25 @@ Update options:
90
91
  -y, --yes Remove deprecated files without
91
92
  confirming each one
92
93
 
94
+ Uninstall options:
95
+ --agent <claude|codex|forgecode|universal|*> Which install to remove
96
+ (default: * — every agent)
97
+ --scope <project|global> Where to look (default: project)
98
+
93
99
  Examples:
94
100
  plain-forge install --agent claude --scope project
95
101
  plain-forge install --agent universal --scope global
96
102
  plain-forge update
97
103
  plain-forge update --yes
104
+ plain-forge uninstall
105
+ plain-forge uninstall --agent claude --scope global
98
106
 
99
107
  "install" fails if plain-forge is already installed at the target — use
100
108
  "update" to refresh it. Missing install flags are prompted interactively.
101
109
  "update" auto-detects installs and prunes only files plain-forge wrote
102
- (confirming each removal), leaving your own and third-party skills untouched.`);
110
+ (confirming each removal), leaving your own and third-party skills untouched.
111
+ "uninstall" deletes exactly the files recorded in the install manifest, then
112
+ the manifest itself; an install with no manifest is reported and left in place.`);
103
113
  }
104
114
 
105
115
  function parseArgs(argv) {
@@ -488,6 +498,95 @@ async function cmdUpdate(args) {
488
498
  }
489
499
  }
490
500
 
501
+ async function cmdUninstall(args) {
502
+ printBanner();
503
+
504
+ const scope = args.scope ?? "project";
505
+ if (!SCOPES.includes(scope)) {
506
+ console.error(`unknown scope "${scope}". valid: ${SCOPES.join(", ")}`);
507
+ process.exit(2);
508
+ }
509
+
510
+ // Default agent is "*" — every agent layout. A named agent narrows to one.
511
+ const agentArg = args.agent ?? "*";
512
+ let agents;
513
+ if (agentArg === "*" || agentArg === "all") {
514
+ agents = Object.keys(AGENTS);
515
+ } else if (Object.hasOwn(AGENTS, agentArg)) {
516
+ agents = [agentArg];
517
+ } else {
518
+ console.error(
519
+ `unknown agent "${agentArg}". valid: ${Object.keys(AGENTS).join(", ")}, or "*" for all`,
520
+ );
521
+ process.exit(2);
522
+ }
523
+
524
+ const root = scope === "global" ? os.homedir() : process.cwd();
525
+
526
+ let found = 0;
527
+ let removed = 0;
528
+ let hadError = false;
529
+
530
+ for (const agent of agents) {
531
+ const baseDir = path.join(root, AGENTS[agent]);
532
+ if (!fs.existsSync(baseDir)) continue;
533
+
534
+ const manifest = readManifest(baseDir);
535
+ const legacy = !manifest && hasForgeSignature(baseDir);
536
+ if (!manifest && !legacy) continue; // not a plain-forge install
537
+ found++;
538
+
539
+ // No manifest → we have no record of which files are ours, so deleting
540
+ // would risk touching the user's own content. Refuse and point at the
541
+ // directories to clean by hand.
542
+ if (!manifest) {
543
+ hadError = true;
544
+ console.error(`cannot uninstall ${agent} (${scope}) — ${baseDir}`);
545
+ console.error(
546
+ ` the install manifest (${MANIFEST_REL}) is missing, so automatic deletion is not supported.`,
547
+ );
548
+ console.error(` please remove plain-forge's files manually from:`);
549
+ for (const dir of CONTENT_DIRS) {
550
+ const p = path.join(baseDir, dir);
551
+ if (fs.existsSync(p)) console.error(` ${p}`);
552
+ }
553
+ console.error("");
554
+ continue;
555
+ }
556
+
557
+ let deleted = 0;
558
+ let failed = 0;
559
+ for (const rel of manifest.files) {
560
+ if (deleteForgeFile(baseDir, rel)) deleted++;
561
+ else failed++;
562
+ }
563
+ // Finally remove the manifest itself, then prune the now-empty .plain-forge
564
+ // directory and the agent directory if nothing else remains in it.
565
+ fs.rmSync(manifestPathFor(baseDir), { force: true });
566
+ removeEmptyDirsUpward(
567
+ path.join(baseDir, path.dirname(MANIFEST_REL)),
568
+ baseDir,
569
+ );
570
+ removeEmptyDirsUpward(baseDir, root);
571
+
572
+ console.log(`uninstalled ${agent} (${scope}) from ${baseDir}`);
573
+ console.log(
574
+ ` removed ${deleted} file(s)${failed ? `, ${failed} could not be removed` : ""} + manifest`,
575
+ );
576
+ console.log();
577
+ removed++;
578
+ }
579
+
580
+ if (found === 0) {
581
+ console.log(`no plain-forge installation found in ${root} (scope: ${scope}).`);
582
+ return;
583
+ }
584
+ if (removed > 0) {
585
+ console.log(`uninstalled ${removed} installation(s).`);
586
+ }
587
+ if (hadError) process.exit(1);
588
+ }
589
+
491
590
  function printNextSteps(agent) {
492
591
  const bold = (s) => `\x1b[1;97m${s}\x1b[0m`;
493
592
  const dim = (s) => `\x1b[2m${s}\x1b[0m`;
@@ -548,6 +647,9 @@ async function main() {
548
647
  case "update":
549
648
  await cmdUpdate(args);
550
649
  break;
650
+ case "uninstall":
651
+ await cmdUninstall(args);
652
+ break;
551
653
  default:
552
654
  console.error(`unknown command "${cmd}"`);
553
655
  usage();
@@ -557,8 +659,21 @@ async function main() {
557
659
 
558
660
  // Only run the CLI when executed directly — importing this module (e.g. from
559
661
  // the test suite) must not trigger main() or process.exit().
560
- const invokedDirectly =
561
- process.argv[1] && path.resolve(process.argv[1]) === __filename;
662
+ // `__filename` (from import.meta.url) is realpath-resolved by Node, but
663
+ // process.argv[1] is the path as invoked — under npx / a global install it's a
664
+ // symlink in node_modules/.bin or the npx cache. Resolve both through realpath
665
+ // so the comparison survives symlinked bins; otherwise main() silently never
666
+ // runs (spinner, then nothing).
667
+ function isInvokedDirectly() {
668
+ const invoked = process.argv[1];
669
+ if (!invoked) return false;
670
+ try {
671
+ return fs.realpathSync(invoked) === __filename;
672
+ } catch {
673
+ return path.resolve(invoked) === __filename;
674
+ }
675
+ }
676
+ const invokedDirectly = isInvokedDirectly();
562
677
  if (invokedDirectly) {
563
678
  main().catch((err) => {
564
679
  if (err instanceof Error && err.message === "cancelled") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plain-forge",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "description": "Conversational spec-writing tool for ***plain specification language",
5
5
  "type": "module",
6
6
  "engines": {