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.
- package/README.md +21 -60
- package/bin/cli.mjs +120 -5
- 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`
|
|
45
|
+
### Install with `npx plain-forge install`
|
|
46
46
|
|
|
47
|
-
The
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
+
npx plain-forge uninstall --agent claude --scope global
|
|
128
110
|
```
|
|
129
111
|
|
|
130
|
-
|
|
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
|
|
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,
|
|
181
|
+
forge/ # canonical content, copied verbatim on install
|
|
195
182
|
skills/ # all skills used during spec writing
|
|
196
|
-
rules/ #
|
|
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
|
-
|
|
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
|
-
|
|
188
|
+
test/
|
|
189
|
+
cli.test.mjs # tests for the install / update CLI
|
|
222
190
|
|
|
223
|
-
|
|
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
|
-
|
|
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
|
|
82
|
-
update
|
|
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
|
-
|
|
561
|
-
|
|
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") {
|