plain-forge 1.0.12 → 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 +32 -23
  2. package/bin/cli.mjs +105 -3
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -95,6 +95,27 @@ 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
+ #### Removing an install
99
+
100
+ ```bash
101
+ npx plain-forge uninstall
102
+ ```
103
+
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.
105
+
106
+ By default it removes **every** agent layout in the **project** scope (the current folder). Narrow it with flags:
107
+
108
+ ```bash
109
+ npx plain-forge uninstall --agent claude --scope global
110
+ ```
111
+
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`.
118
+
98
119
  ## Usage
99
120
 
100
121
  ### Prerequisites
@@ -154,36 +175,24 @@ Hit a bug in the rendered app, a failing test, or behavior that doesn't match wh
154
175
 
155
176
  ## Repository Structure
156
177
 
157
- 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.
158
179
 
159
180
  ```
160
- forge/ # canonical, runtime-neutral content
181
+ forge/ # canonical content, copied verbatim on install
161
182
  skills/ # all skills used during spec writing
162
- rules/ # workspace rules for spec validation
163
-
164
- runtimes/ # per-runtime adapters
165
- claude/
166
- build.ts # generates .claude/ + .claude-plugin/ from forge/
167
- templates/ # Claude-specific files: settings.json, hook script, plugin manifests
168
- codex/
169
- build.ts # generates .codex-plugin/ and .agents/plugins/ (manifest points at forge/skills/)
170
- templates/ # Codex-specific files: plugin.json, marketplace catalog
171
- opencode/
172
- build.ts # generates .opencode/ from forge/
173
- templates/ # OpenCode-specific files: package.json, .gitignore
183
+ rules/ # spec-writing rules (installed as workspace instructions)
174
184
 
175
185
  bin/
176
- forge-build.ts # orchestrator: runs every runtimes/*/build.ts
177
- lib.ts # shared symlink/copy helpers
178
-
179
- # Generated outputs (committed, do not edit by hand):
180
- .claude/ # Claude Code plugin layout
181
- .claude-plugin/ # Claude Code plugin manifests
182
- .codex-plugin/ # Codex plugin manifest (its "skills" field points at forge/skills/)
183
- .agents/plugins/ # Codex marketplace catalog
184
- .opencode/ # OpenCode plugin layout
186
+ cli.mjs # the `plain-forge` CLI — `install` and `update` commands
187
+
188
+ test/
189
+ cli.test.mjs # tests for the install / update CLI
190
+
191
+ package.json # ships only `bin/cli.mjs` and `forge/` to npm
185
192
  ```
186
193
 
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.
195
+
187
196
  ## Available Skills
188
197
 
189
198
  ### Core Workflow
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plain-forge",
3
- "version": "1.0.12",
3
+ "version": "1.0.13",
4
4
  "description": "Conversational spec-writing tool for ***plain specification language",
5
5
  "type": "module",
6
6
  "engines": {