plain-forge 1.0.1 → 1.0.2

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 +43 -34
  2. package/bin/cli.mjs +83 -35
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -19,73 +19,82 @@ Each phase is **incremental**, not a single long questionnaire. plain-forge walk
19
19
 
20
20
  ## Getting Started
21
21
 
22
- plain-forge ships as a set of skills 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.
22
+ 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.
23
23
 
24
- ### Install with the `skills` CLI (any runtime)
24
+ ### Install with `npx plain-forge install` (recommended)
25
25
 
26
- The fastest way to add plain-forge is the `skills` CLI. The `--all` flag installs **every** plain-forge skill at once:
26
+ 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.
27
27
 
28
28
  ```bash
29
- npx skills add Codeplain-ai/plain-forge --skill '*'
29
+ npx plain-forge install
30
30
  ```
31
31
 
32
- #### Install into a specific runtime
33
-
34
- If you only use one runtime, pass `--agent` to target just that one (you can repeat the flag to pick several):
32
+ This prompts you to pick an agent and a scope using an arrow-key menu. You can also pass both flags non-interactively:
35
33
 
36
34
  ```bash
37
- # Just Claude Code
38
- npx skills add Codeplain-ai/plain-forge --skill '*' --agent claude-code
35
+ npx plain-forge install --agent claude --scope project
36
+ ```
37
+
38
+ **Agent options:**
39
+
40
+ | `--agent` | Installs into | Use when |
41
+ |-----------|---------------|----------|
42
+ | `claude` | `.claude/` | You use Claude Code |
43
+ | `codex` | `.codex/` | You use the OpenAI Codex CLI |
44
+ | `forgecode` | `.forgecode/` | You use ForgeCode |
45
+ | `universal` | `.agents/` | You want a runtime-neutral layout that any agent reading from `.agents/` can pick up |
39
46
 
40
- # Just Codex
41
- npx skills add Codeplain-ai/plain-forge --skill '*' --agent codex
47
+ **Scope options:**
42
48
 
43
- # Just OpenCode
44
- npx skills add Codeplain-ai/plain-forge --skill '*' --agent opencode
49
+ | `--scope` | Installs into | Use when |
50
+ |-----------|---------------|----------|
51
+ | `project` | `./<agent-dir>/` in the current working directory | You want plain-forge in just this project |
52
+ | `global` | `~/<agent-dir>/` in your home directory | You want plain-forge available in every project on the machine |
45
53
 
46
- # Any combination, non-interactive
47
- npx skills add Codeplain-ai/plain-forge --skill '*' --agent opencode --agent codex --agent claude-code
54
+ Each install writes three subfolders under the chosen directory:
55
+
56
+ ```
57
+ <agent-dir>/
58
+ skills/ # every plain-forge skill
59
+ rules/ # spec-writing rules (loaded as workspace instructions)
60
+ docs/ # shared reference docs
48
61
  ```
49
62
 
50
- If you'd rather use the native install flow for a specific runtime, the per-tool instructions below still work.
63
+ Re-running `npx plain-forge install` overwrites the destination silently, so it doubles as the upgrade path — `npx plain-forge@latest install` pulls the newest release every time.
51
64
 
52
- ### Install in Claude Code
65
+ ### Alternative install paths (skills only — no rules or docs)
53
66
 
54
- Requires the [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed and configured. Inside any Claude Code session, run the following **three commands**, one after the other (copy and paste each one separately):
67
+ 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`.
55
68
 
56
- **1.** Register this repository as a plugin marketplace:
69
+ #### `npx skills` CLI
57
70
 
58
- ```text
59
- /plugin marketplace add Codeplain-ai/plain-forge
71
+ ```bash
72
+ npx skills add Codeplain-ai/plain-forge --skill '*' --agent claude-code
60
73
  ```
61
74
 
62
- **2.** Install the `plain-forge` plugin from it:
75
+ Replace `--agent claude-code` with `codex` or `opencode` to target a different runtime, or repeat the flag for several at once.
63
76
 
64
- ```text
65
- /plugin install plain-forge@plain-forge
66
- ```
77
+ #### Claude Code native plugin flow
67
78
 
68
- **3.** Reload plugins so Claude Code picks up the newly installed skills:
79
+ 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:
69
80
 
70
81
  ```text
82
+ /plugin marketplace add Codeplain-ai/plain-forge
83
+ /plugin install plain-forge@plain-forge
71
84
  /reload-plugins
72
85
  ```
73
86
 
74
- Without the reload, the plain-forge skills won't be visible in the current session even though the install succeeded. Once all three commands have run, all plain-forge skills become available.
87
+ Without the reload the skills won't appear in the current session.
75
88
 
76
- ### Install in Codex
89
+ #### Codex native plugin flow
77
90
 
78
- Requires the [OpenAI Codex CLI](https://developers.openai.com/codex/cli/reference) installed and signed in. Installation is **two steps**, but only the first one is a shell command:
79
-
80
- **1.** From your shell, register this repository as a Codex marketplace:
91
+ Requires the [OpenAI Codex CLI](https://developers.openai.com/codex/cli/reference). From your shell:
81
92
 
82
93
  ```bash
83
94
  codex plugin marketplace add Codeplain-ai/plain-forge
84
95
  ```
85
96
 
86
- **2.** Inside Codex, open the plugin directory, pick the `plain-forge` marketplace, and install the plugin from there. (The Codex CLI does not currently expose a `codex plugin install` equivalent — installation has to be triggered from the in-app plugin directory.)
87
-
88
- Once the plugin is installed, all plain-forge skills become available in your Codex sessions.
97
+ 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.)
89
98
 
90
99
  ## Usage
91
100
 
package/bin/cli.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
- import readline from "node:readline/promises";
5
+ import readline from "node:readline";
6
6
  import { fileURLToPath } from "node:url";
7
7
 
8
8
  const __filename = fileURLToPath(import.meta.url);
@@ -23,49 +23,96 @@ function usage() {
23
23
  Options:
24
24
  --agent <claude|codex|forgecode|universal> Target agent layout
25
25
  --scope <project|global> Install into cwd or $HOME
26
- --skill <name> Install only the named skill (repeatable; default: all)
27
26
  -h, --help Show this help
28
27
 
29
28
  Examples:
30
29
  plain-forge install --agent claude --scope project
31
30
  plain-forge install --agent universal --scope global
32
- plain-forge install --agent claude --skill add-functional-spec --skill add-concept
33
31
 
34
32
  Missing flags are prompted interactively.`);
35
33
  }
36
34
 
37
35
  function parseArgs(argv) {
38
- const out = { _: [], skills: [] };
36
+ const out = { _: [] };
39
37
  for (let i = 0; i < argv.length; i++) {
40
38
  const a = argv[i];
41
39
  if (a === "--agent") out.agent = argv[++i];
42
40
  else if (a === "--scope") out.scope = argv[++i];
43
- else if (a === "--skill") out.skills.push(argv[++i]);
44
41
  else if (a === "-h" || a === "--help") out.help = true;
45
42
  else out._.push(a);
46
43
  }
47
44
  return out;
48
45
  }
49
46
 
50
- async function promptChoice(question, choices) {
51
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
52
- try {
53
- while (true) {
54
- const ans = (await rl.question(`${question} [${choices.join("/")}]: `)).trim().toLowerCase();
55
- if (choices.includes(ans)) return ans;
56
- console.log(` please answer one of: ${choices.join(", ")}`);
57
- }
58
- } finally {
59
- rl.close();
47
+ function promptChoice(question, choices) {
48
+ const input = process.stdin;
49
+ const output = process.stdout;
50
+ if (!input.isTTY) {
51
+ return Promise.reject(
52
+ new Error(
53
+ `cannot prompt for "${question}" — stdin is not a TTY. Pass the value as a flag instead.`,
54
+ ),
55
+ );
60
56
  }
57
+
58
+ return new Promise((resolve, reject) => {
59
+ let index = 0;
60
+ let rendered = 0;
61
+
62
+ const render = () => {
63
+ if (rendered > 0) {
64
+ readline.moveCursor(output, 0, -rendered);
65
+ readline.clearScreenDown(output);
66
+ }
67
+ output.write(`? ${question} (use arrow keys, enter to select)\n`);
68
+ for (let i = 0; i < choices.length; i++) {
69
+ const pointer = i === index ? "\x1b[36m>\x1b[0m" : " ";
70
+ const label = i === index ? `\x1b[36m${choices[i]}\x1b[0m` : choices[i];
71
+ output.write(` ${pointer} ${label}\n`);
72
+ }
73
+ rendered = choices.length + 1;
74
+ };
75
+
76
+ const cleanup = () => {
77
+ input.removeListener("keypress", onKey);
78
+ input.setRawMode(false);
79
+ input.pause();
80
+ output.write("\x1b[?25h"); // show cursor
81
+ };
82
+
83
+ const onKey = (_str, key) => {
84
+ if (!key) return;
85
+ if (key.name === "up" || (key.ctrl && key.name === "p")) {
86
+ index = (index - 1 + choices.length) % choices.length;
87
+ render();
88
+ } else if (key.name === "down" || (key.ctrl && key.name === "n")) {
89
+ index = (index + 1) % choices.length;
90
+ render();
91
+ } else if (key.name === "return") {
92
+ cleanup();
93
+ output.write(` \x1b[32m${choices[index]}\x1b[0m\n`);
94
+ resolve(choices[index]);
95
+ } else if (key.name === "escape" || (key.ctrl && key.name === "c")) {
96
+ cleanup();
97
+ output.write("\n");
98
+ reject(new Error("cancelled"));
99
+ }
100
+ };
101
+
102
+ readline.emitKeypressEvents(input);
103
+ input.setRawMode(true);
104
+ input.resume();
105
+ output.write("\x1b[?25l"); // hide cursor
106
+ input.on("keypress", onKey);
107
+ render();
108
+ });
61
109
  }
62
110
 
63
- function copyTree(srcDir, destDir, filterNames) {
111
+ function copyTree(srcDir, destDir) {
64
112
  if (!fs.existsSync(srcDir)) return 0;
65
113
  fs.mkdirSync(destDir, { recursive: true });
66
114
  let count = 0;
67
115
  for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
68
- if (filterNames && !filterNames.has(entry.name)) continue;
69
116
  const src = path.join(srcDir, entry.name);
70
117
  const dest = path.join(destDir, entry.name);
71
118
  if (entry.isDirectory()) {
@@ -82,7 +129,9 @@ async function cmdInstall(args) {
82
129
  let agent = args.agent;
83
130
  if (!agent) agent = await promptChoice("Which agent?", Object.keys(AGENTS));
84
131
  if (!Object.hasOwn(AGENTS, agent)) {
85
- console.error(`unknown agent "${agent}". valid: ${Object.keys(AGENTS).join(", ")}`);
132
+ console.error(
133
+ `unknown agent "${agent}". valid: ${Object.keys(AGENTS).join(", ")}`,
134
+ );
86
135
  process.exit(2);
87
136
  }
88
137
 
@@ -96,22 +145,18 @@ async function cmdInstall(args) {
96
145
  const root = scope === "global" ? os.homedir() : process.cwd();
97
146
  const baseDir = path.join(root, AGENTS[agent]);
98
147
 
99
- const explicit = args.skills.filter((s) => s !== "*");
100
- const skillFilter = explicit.length ? new Set(explicit) : null;
101
-
102
- const skillsSrc = path.join(forgeDir, "skills");
103
- if (skillFilter) {
104
- for (const name of skillFilter) {
105
- if (!fs.existsSync(path.join(skillsSrc, name))) {
106
- console.error(`skill "${name}" not found under ${skillsSrc}`);
107
- process.exit(2);
108
- }
109
- }
110
- }
111
-
112
- const skillsCount = copyTree(skillsSrc, path.join(baseDir, "skills"), skillFilter);
113
- const rulesCount = copyTree(path.join(forgeDir, "rules"), path.join(baseDir, "rules"), null);
114
- const docsCount = copyTree(path.join(forgeDir, "docs"), path.join(baseDir, "docs"), null);
148
+ const skillsCount = copyTree(
149
+ path.join(forgeDir, "skills"),
150
+ path.join(baseDir, "skills"),
151
+ );
152
+ const rulesCount = copyTree(
153
+ path.join(forgeDir, "rules"),
154
+ path.join(baseDir, "rules"),
155
+ );
156
+ const docsCount = copyTree(
157
+ path.join(forgeDir, "docs"),
158
+ path.join(baseDir, "docs"),
159
+ );
115
160
 
116
161
  console.log(`installed into ${baseDir}`);
117
162
  console.log(` skills: ${skillsCount}`);
@@ -138,6 +183,9 @@ async function main() {
138
183
  }
139
184
 
140
185
  main().catch((err) => {
141
- console.error(err instanceof Error ? err.stack ?? err.message : err);
186
+ if (err instanceof Error && err.message === "cancelled") {
187
+ process.exit(130);
188
+ }
189
+ console.error(err instanceof Error ? (err.stack ?? err.message) : err);
142
190
  process.exit(1);
143
191
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plain-forge",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Conversational spec-writing tool for ***plain specification language",
5
5
  "type": "module",
6
6
  "engines": {