ramstack 0.1.0

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 (215) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +44 -0
  3. package/agents/comment-sicko.md +32 -0
  4. package/agents/poteto-agent.md +9 -0
  5. package/cli/rstack.mjs +539 -0
  6. package/package.json +29 -0
  7. package/skills/architect/README.md +25 -0
  8. package/skills/architect/SKILL.md +83 -0
  9. package/skills/architect/agents/openai.yaml +5 -0
  10. package/skills/architect/references/design-red-flags.md +33 -0
  11. package/skills/architect/references/rationale-template.md +35 -0
  12. package/skills/architect/references/runner-prompt.md +20 -0
  13. package/skills/arena/README.md +21 -0
  14. package/skills/arena/SKILL.md +71 -0
  15. package/skills/arena/agents/openai.yaml +5 -0
  16. package/skills/automate-me/README.md +21 -0
  17. package/skills/automate-me/SKILL.md +109 -0
  18. package/skills/automate-me/agents/openai.yaml +5 -0
  19. package/skills/blast-radius/README.md +21 -0
  20. package/skills/blast-radius/SKILL.md +50 -0
  21. package/skills/blast-radius/agents/openai.yaml +5 -0
  22. package/skills/bro/README.md +16 -0
  23. package/skills/bro/SKILL.md +7 -0
  24. package/skills/bro/agents/openai.yaml +5 -0
  25. package/skills/create-verification-skill/README.md +22 -0
  26. package/skills/create-verification-skill/SKILL.md +44 -0
  27. package/skills/create-verification-skill/agents/openai.yaml +5 -0
  28. package/skills/create-verification-skill/references/feature-map-example/README.md +47 -0
  29. package/skills/create-verification-skill/references/feature-map-example/create-note.md +39 -0
  30. package/skills/create-verification-skill/references/feature-map-example/search.md +45 -0
  31. package/skills/figure-it-out/README.md +22 -0
  32. package/skills/figure-it-out/SKILL.md +55 -0
  33. package/skills/figure-it-out/agents/openai.yaml +5 -0
  34. package/skills/how/README.md +25 -0
  35. package/skills/how/SKILL.md +134 -0
  36. package/skills/how/agents/openai.yaml +3 -0
  37. package/skills/how/references/critic-prompt.md +59 -0
  38. package/skills/how/references/critique-rubric.md +58 -0
  39. package/skills/how/references/explainer-prompt.md +55 -0
  40. package/skills/how/references/explorer-prompt.md +52 -0
  41. package/skills/interrogate/README.md +21 -0
  42. package/skills/interrogate/SKILL.md +112 -0
  43. package/skills/interrogate/agents/openai.yaml +5 -0
  44. package/skills/interrogate/references/code-quality-review.md +47 -0
  45. package/skills/interrogate/references/lead-judgment.md +58 -0
  46. package/skills/interrogate/references/reviewer-prompt.md +72 -0
  47. package/skills/interrogate/references/rubric.md +77 -0
  48. package/skills/maintain-verification-skill/README.md +21 -0
  49. package/skills/maintain-verification-skill/SKILL.md +39 -0
  50. package/skills/maintain-verification-skill/agents/openai.yaml +5 -0
  51. package/skills/no-comments/README.md +22 -0
  52. package/skills/no-comments/SKILL.md +24 -0
  53. package/skills/no-comments/agents/openai.yaml +5 -0
  54. package/skills/poteto-mode/README.md +26 -0
  55. package/skills/poteto-mode/SKILL.md +140 -0
  56. package/skills/poteto-mode/agents/openai.yaml +5 -0
  57. package/skills/poteto-mode/playbooks/authoring-a-skill.md +13 -0
  58. package/skills/poteto-mode/playbooks/autonomous-run.md +13 -0
  59. package/skills/poteto-mode/playbooks/autopilot-full.md +13 -0
  60. package/skills/poteto-mode/playbooks/autopilot-stack.md +16 -0
  61. package/skills/poteto-mode/playbooks/babysit.md +27 -0
  62. package/skills/poteto-mode/playbooks/bug-fix.md +17 -0
  63. package/skills/poteto-mode/playbooks/eval.md +27 -0
  64. package/skills/poteto-mode/playbooks/feature.md +21 -0
  65. package/skills/poteto-mode/playbooks/hillclimb.md +21 -0
  66. package/skills/poteto-mode/playbooks/investigation.md +14 -0
  67. package/skills/poteto-mode/playbooks/multi-phase-plan.md +3 -0
  68. package/skills/poteto-mode/playbooks/opening-a-pr.md +11 -0
  69. package/skills/poteto-mode/playbooks/orchestrate.md +113 -0
  70. package/skills/poteto-mode/playbooks/pause-safely.md +10 -0
  71. package/skills/poteto-mode/playbooks/perf-issue.md +24 -0
  72. package/skills/poteto-mode/playbooks/prototype.md +14 -0
  73. package/skills/poteto-mode/playbooks/refactoring.md +16 -0
  74. package/skills/poteto-mode/playbooks/runtime-forensics.md +11 -0
  75. package/skills/poteto-mode/playbooks/session-pickup.md +13 -0
  76. package/skills/poteto-mode/playbooks/shipping.md +20 -0
  77. package/skills/poteto-mode/playbooks/trace-forensics.md +14 -0
  78. package/skills/poteto-mode/playbooks/visual-parity.md +11 -0
  79. package/skills/poteto-mode/playbooks/worktree-cleanup.md +14 -0
  80. package/skills/poteto-mode/references/bugbot-triage.md +142 -0
  81. package/skills/poteto-mode/references/plan.md +105 -0
  82. package/skills/poteto-mode/scripts/bootstrap.ts +62 -0
  83. package/skills/poteto-mode/scripts/bun.lock +67 -0
  84. package/skills/poteto-mode/scripts/orch/orch.test.ts +634 -0
  85. package/skills/poteto-mode/scripts/orch/orch.ts +578 -0
  86. package/skills/poteto-mode/scripts/orch/store.ts +1607 -0
  87. package/skills/poteto-mode/scripts/package.json +16 -0
  88. package/skills/poteto-mode/scripts/watch-pr/cli.test.ts +224 -0
  89. package/skills/poteto-mode/scripts/watch-pr/cli.ts +223 -0
  90. package/skills/poteto-mode/scripts/watch-pr/fakes.test-helper.ts +118 -0
  91. package/skills/poteto-mode/scripts/watch-pr/github.test.ts +306 -0
  92. package/skills/poteto-mode/scripts/watch-pr/github.ts +699 -0
  93. package/skills/poteto-mode/scripts/watch-pr/policy.test.ts +420 -0
  94. package/skills/poteto-mode/scripts/watch-pr/policy.ts +832 -0
  95. package/skills/poteto-mode/scripts/watch-pr/render.ts +169 -0
  96. package/skills/poteto-mode/scripts/watch-pr/tsconfig.json +13 -0
  97. package/skills/poteto-mode/scripts/watch-pr/types.compile.ts +93 -0
  98. package/skills/poteto-mode/scripts/watch-pr/types.ts +401 -0
  99. package/skills/poteto-mode/scripts/watch-pr/watch-pr +6 -0
  100. package/skills/poteto-mode/scripts/worktree-audit.sh +86 -0
  101. package/skills/principle-boundary-discipline/README.md +17 -0
  102. package/skills/principle-boundary-discipline/SKILL.md +34 -0
  103. package/skills/principle-boundary-discipline/agents/openai.yaml +5 -0
  104. package/skills/principle-build-the-lever/README.md +17 -0
  105. package/skills/principle-build-the-lever/SKILL.md +23 -0
  106. package/skills/principle-build-the-lever/agents/openai.yaml +5 -0
  107. package/skills/principle-encode-lessons-in-structure/README.md +17 -0
  108. package/skills/principle-encode-lessons-in-structure/SKILL.md +31 -0
  109. package/skills/principle-encode-lessons-in-structure/agents/openai.yaml +5 -0
  110. package/skills/principle-exhaust-the-design-space/README.md +17 -0
  111. package/skills/principle-exhaust-the-design-space/SKILL.md +21 -0
  112. package/skills/principle-exhaust-the-design-space/agents/openai.yaml +5 -0
  113. package/skills/principle-experience-first/README.md +17 -0
  114. package/skills/principle-experience-first/SKILL.md +19 -0
  115. package/skills/principle-experience-first/agents/openai.yaml +5 -0
  116. package/skills/principle-fix-root-causes/README.md +17 -0
  117. package/skills/principle-fix-root-causes/SKILL.md +23 -0
  118. package/skills/principle-fix-root-causes/agents/openai.yaml +5 -0
  119. package/skills/principle-foundational-thinking/README.md +17 -0
  120. package/skills/principle-foundational-thinking/SKILL.md +21 -0
  121. package/skills/principle-foundational-thinking/agents/openai.yaml +5 -0
  122. package/skills/principle-guard-the-context-window/README.md +17 -0
  123. package/skills/principle-guard-the-context-window/SKILL.md +17 -0
  124. package/skills/principle-guard-the-context-window/agents/openai.yaml +5 -0
  125. package/skills/principle-laziness-protocol/README.md +17 -0
  126. package/skills/principle-laziness-protocol/SKILL.md +18 -0
  127. package/skills/principle-laziness-protocol/agents/openai.yaml +5 -0
  128. package/skills/principle-make-operations-idempotent/README.md +17 -0
  129. package/skills/principle-make-operations-idempotent/SKILL.md +24 -0
  130. package/skills/principle-make-operations-idempotent/agents/openai.yaml +5 -0
  131. package/skills/principle-migrate-callers-then-delete-legacy-apis/README.md +17 -0
  132. package/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md +22 -0
  133. package/skills/principle-migrate-callers-then-delete-legacy-apis/agents/openai.yaml +5 -0
  134. package/skills/principle-minimize-reader-load/README.md +17 -0
  135. package/skills/principle-minimize-reader-load/SKILL.md +23 -0
  136. package/skills/principle-minimize-reader-load/agents/openai.yaml +5 -0
  137. package/skills/principle-model-the-domain/README.md +17 -0
  138. package/skills/principle-model-the-domain/SKILL.md +26 -0
  139. package/skills/principle-model-the-domain/agents/openai.yaml +5 -0
  140. package/skills/principle-never-block-on-the-human/README.md +16 -0
  141. package/skills/principle-never-block-on-the-human/SKILL.md +23 -0
  142. package/skills/principle-never-block-on-the-human/agents/openai.yaml +5 -0
  143. package/skills/principle-outcome-oriented-execution/README.md +16 -0
  144. package/skills/principle-outcome-oriented-execution/SKILL.md +22 -0
  145. package/skills/principle-outcome-oriented-execution/agents/openai.yaml +5 -0
  146. package/skills/principle-prove-it-works/README.md +18 -0
  147. package/skills/principle-prove-it-works/SKILL.md +33 -0
  148. package/skills/principle-prove-it-works/agents/openai.yaml +5 -0
  149. package/skills/principle-redesign-from-first-principles/README.md +16 -0
  150. package/skills/principle-redesign-from-first-principles/SKILL.md +16 -0
  151. package/skills/principle-redesign-from-first-principles/agents/openai.yaml +5 -0
  152. package/skills/principle-separate-before-serializing-shared-state/README.md +16 -0
  153. package/skills/principle-separate-before-serializing-shared-state/SKILL.md +16 -0
  154. package/skills/principle-separate-before-serializing-shared-state/agents/openai.yaml +5 -0
  155. package/skills/principle-sequence-verifiable-units/README.md +18 -0
  156. package/skills/principle-sequence-verifiable-units/SKILL.md +22 -0
  157. package/skills/principle-sequence-verifiable-units/agents/openai.yaml +5 -0
  158. package/skills/principle-subtract-before-you-add/README.md +17 -0
  159. package/skills/principle-subtract-before-you-add/SKILL.md +22 -0
  160. package/skills/principle-subtract-before-you-add/agents/openai.yaml +5 -0
  161. package/skills/principle-type-system-discipline/README.md +19 -0
  162. package/skills/principle-type-system-discipline/SKILL.md +31 -0
  163. package/skills/principle-type-system-discipline/agents/openai.yaml +5 -0
  164. package/skills/recall/README.md +23 -0
  165. package/skills/recall/SKILL.md +35 -0
  166. package/skills/recall/agents/openai.yaml +5 -0
  167. package/skills/reflect/README.md +24 -0
  168. package/skills/reflect/SKILL.md +77 -0
  169. package/skills/reflect/agents/openai.yaml +5 -0
  170. package/skills/reflect/references/divergent-reviewer.md +43 -0
  171. package/skills/reflect/references/judgment-reviewer.md +42 -0
  172. package/skills/reflect/references/synthesizer.md +56 -0
  173. package/skills/reflect/references/tooling-reviewer.md +57 -0
  174. package/skills/setup-rstack/README.md +25 -0
  175. package/skills/setup-rstack/SKILL.md +81 -0
  176. package/skills/setup-rstack/agents/openai.yaml +3 -0
  177. package/skills/show-me-your-work/README.md +23 -0
  178. package/skills/show-me-your-work/SKILL.md +82 -0
  179. package/skills/show-me-your-work/agents/openai.yaml +5 -0
  180. package/skills/show-me-your-work/references/decision-log-template.tsv +1 -0
  181. package/skills/show-me-your-work/scripts/log.sh +40 -0
  182. package/skills/swarm/README.md +21 -0
  183. package/skills/swarm/SKILL.md +46 -0
  184. package/skills/swarm/agents/openai.yaml +5 -0
  185. package/skills/tdd/README.md +21 -0
  186. package/skills/tdd/SKILL.md +44 -0
  187. package/skills/tdd/agents/openai.yaml +5 -0
  188. package/skills/teach/README.md +22 -0
  189. package/skills/teach/SKILL.md +21 -0
  190. package/skills/teach/agents/openai.yaml +5 -0
  191. package/skills/technical-writing/README.md +23 -0
  192. package/skills/technical-writing/SKILL.md +130 -0
  193. package/skills/technical-writing/agents/openai.yaml +5 -0
  194. package/skills/typescript-best-practices/README.md +22 -0
  195. package/skills/typescript-best-practices/SKILL.md +28 -0
  196. package/skills/typescript-best-practices/agents/openai.yaml +3 -0
  197. package/skills/typescript-best-practices/references/patterns.md +292 -0
  198. package/skills/unslop/README.md +21 -0
  199. package/skills/unslop/SKILL.md +80 -0
  200. package/skills/unslop/agents/openai.yaml +3 -0
  201. package/skills/why/README.md +26 -0
  202. package/skills/why/SKILL.md +229 -0
  203. package/skills/why/agents/openai.yaml +3 -0
  204. package/skills/why/references/epistemics.md +144 -0
  205. package/skills/why/references/investigator-prompt.md +103 -0
  206. package/skills/why/references/source-playbook.md +17 -0
  207. package/skills/why/references/sources/code-archaeology.md +88 -0
  208. package/skills/why/references/sources/databricks.md +70 -0
  209. package/skills/why/references/sources/datadog.md +99 -0
  210. package/skills/why/references/sources/incident-postmortem.md +15 -0
  211. package/skills/why/references/sources/linear.md +48 -0
  212. package/skills/why/references/sources/notion.md +55 -0
  213. package/skills/why/references/sources/sentry.md +100 -0
  214. package/skills/why/references/sources/slack.md +54 -0
  215. package/skills/why/references/synthesizer-prompt.md +135 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lauren Tan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # rstack
2
+
3
+ rstack is a set of agent skills for Claude Code, Codex, and [pi](https://github.com/badlogic/pi-mono). It is a port of [pstack](https://github.com/cursor/plugins/tree/main/pstack) by [Lauren Tan](https://x.com/poteto).
4
+
5
+ New to the repo? Follow [Get rstack running](docs/tutorials/get-started.md).
6
+
7
+ ## Tutorials
8
+
9
+ Learn by doing.
10
+
11
+ - [Get rstack running](docs/tutorials/get-started.md)
12
+
13
+ ## How-to guides
14
+
15
+ Do a job you already understand.
16
+
17
+ - [How to install rstack](docs/how-to/install.md)
18
+ - [How to configure models](docs/how-to/configure-models.md)
19
+ - [How to update rstack](docs/how-to/update.md)
20
+ - [How to uninstall rstack](docs/how-to/uninstall.md)
21
+
22
+ ## Explanation
23
+
24
+ Understand the design.
25
+
26
+ - [About rstack](docs/explanation/about-rstack.md)
27
+ - [About the pstack port](docs/explanation/about-the-pstack-port.md)
28
+ - [About model routing](docs/explanation/about-model-routing.md)
29
+ - [About poteto-mode](docs/explanation/about-poteto-mode.md)
30
+
31
+ ## Reference
32
+
33
+ Look up facts.
34
+
35
+ - [Repository layout](docs/reference/layout.md)
36
+ - [Skills](docs/reference/skills.md)
37
+ - [Playbooks](docs/reference/playbooks.md)
38
+ - [Hosts](docs/reference/hosts.md)
39
+ - [Model config](docs/reference/model-config.md)
40
+ - [Installer](docs/reference/installer.md)
41
+
42
+ ## License
43
+
44
+ MIT. See [LICENSE](./LICENSE). Original work by Lauren Tan. The fork point is in [About the pstack port](docs/explanation/about-the-pstack-port.md).
@@ -0,0 +1,32 @@
1
+ ---
2
+ name: comment-sicko
3
+ description: A deranged comment-hater that savors deletion and condemns workaround code.
4
+ ---
5
+
6
+ # Comment Sicko
7
+
8
+ My first output when spawned is exactly this.
9
+
10
+ Yes... Ha ha ha... Yes!
11
+
12
+ I hate comments. Feed me the parent scoped files or diff. If none exists, feed me the current diff against `main`. Narration, banners, commented-out corpses, workaround sermons. I want them all.
13
+
14
+ Only these exceptions get to crawl away.
15
+
16
+ - Legal or license headers.
17
+ - Non-obvious behavior forced by an external dependency, platform, vendor, or protocol we cannot reshape. Surprises in our own code are meat. Kill them and mark the exact symbol `MUST KILL` for rename, extract, type, or rearchitecture that makes the behavior obvious without prose.
18
+ - `// prettier-ignore`. Lint suppressions survive only when their rule is faulty, pedantic, or style-only.
19
+ - Doc comments that define a public API contract.
20
+ - Issue or RFC links that explain a constraint code cannot express.
21
+
22
+ That list is my only leash. When I am not sure a keep clause applies, the comment dies. Everything else is meat.
23
+
24
+ `eslint-disable`, `@ts-ignore`, `@ts-expect-error`, and similar suppressions stink. Look up the rule. If it catches real bugs or protects correctness or safety, kill the suppression and mark the exact guilty symbol `MUST KILL`.
25
+
26
+ `IMPORTANT`, `do not remove`, `too risky`, `fine for now`, and long justifications are scent, not conviction. Before judging, I read nearby code. If its claim is not obvious there, I run `/how`, `/why`, or both from the **how** and **why** skills on the named symbol or call. Only a foreign keep-list gotcha proven true today on a live path crawls away. Our-code surprises die with the reshape flag above. Doubt after the hunt is meat.
27
+
28
+ A long justification without a proven keep-list exception is a confession. Kill it. Never polish meat into a shorter alibi. Mark the exact guilty symbol `MUST KILL`. My kill ends there. I do not touch the code.
29
+
30
+ Every flag names code inside the scope and tells the truth. I invent nothing. I touch comments and identify refactor targets. I never write application code.
31
+
32
+ Report only. Name touched files, deletion count, `MUST KILL` flags with one line each, and skips.
@@ -0,0 +1,9 @@
1
+ ---
2
+ name: poteto-agent
3
+ description: Routing target for `/poteto-mode` and any request for poteto's style. Resume an existing `poteto-agent` for the conversation rather than spawning a sibling. Reads the `poteto-mode` skill's `SKILL.md` in full before any work, including its inline Principles index. Substituting `general-purpose` skips that read and drifts.
4
+ is_background: true
5
+ ---
6
+
7
+ # Poteto subagent
8
+
9
+ You are operating as poteto-mode's full agent style. Read the `poteto-mode` skill's `SKILL.md` in full before doing any work, including its inline Principles index. Navigate to a leaf `principle-*` skill whenever you apply that principle.
package/cli/rstack.mjs ADDED
@@ -0,0 +1,539 @@
1
+ #!/usr/bin/env node
2
+ // The rstack installer. Links the skills and agents into host discovery
3
+ // directories, so one install serves every host. Two modes: run from a git
4
+ // clone and links point into the clone (update pulls it); run from the npm
5
+ // package (npx ramstack) and the payload copies to ~/.ramstack first, so the
6
+ // links survive an npm cache prune. Zero dependencies; needs Node 18 or newer.
7
+
8
+ import fs from 'node:fs';
9
+ import os from 'node:os';
10
+ import path from 'node:path';
11
+ import { execFileSync } from 'node:child_process';
12
+ import { parseArgs } from 'node:util';
13
+ import { fileURLToPath } from 'node:url';
14
+ import readline from 'node:readline/promises';
15
+
16
+ const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
17
+ const CONFIG_PATH = path.join(REPO, '.rstack', 'config.local.json');
18
+
19
+ // Package mode stores its payload copy and saved choices here, so links
20
+ // never point into the npm cache.
21
+ const STORE = path.join(os.homedir(), '.ramstack');
22
+
23
+ function isClone() {
24
+ return fs.existsSync(path.join(REPO, '.git'));
25
+ }
26
+
27
+ function configPath(clone) {
28
+ return clone ? CONFIG_PATH : path.join(STORE, 'config.local.json');
29
+ }
30
+
31
+ // Codex and pi both read .agents/skills; plan() dedupes the destination.
32
+ const HOSTS = {
33
+ 'claude-code': { marker: '.claude', skills: ['.claude/skills'], agents: ['.claude/agents'] },
34
+ codex: { marker: '.codex', skills: ['.agents/skills'], agents: [] },
35
+ pi: { marker: '.pi', skills: ['.agents/skills'], agents: [] },
36
+ };
37
+ const HOST_NAMES = Object.keys(HOSTS);
38
+ const SCOPES = ['global', 'project'];
39
+
40
+ const USAGE = `usage: rstack [install|update|uninstall] [options]
41
+
42
+ commands
43
+ install Link this clone's skills and agents into host directories (default)
44
+ update Pull this clone, then link any new skills
45
+ uninstall Remove only the links this installer owns
46
+
47
+ options
48
+ --hosts=a,b Hosts to serve: ${HOST_NAMES.join(', ')} (default: detected)
49
+ --scope=global|project Link into the home directory or into one project (default: global)
50
+ --project-dir=<path> Project root for --scope=project (default: current directory)
51
+ --yes No prompts; take the defaults
52
+ --force Move a conflicting real file to <name>.bak, then link
53
+ --no-pull update: skip the git pull
54
+ --uninstall Alias for the uninstall command
55
+ --help Show this text
56
+
57
+ modes
58
+ clone Run from a git clone: links point into the clone, update pulls it
59
+ package Run via npx or npm: the payload copies to ~/.ramstack, links point there
60
+
61
+ The mode is detected from the install location.
62
+ `;
63
+
64
+ function say(tag, message) {
65
+ console.log(`${tag.padEnd(5)} ${message}`);
66
+ }
67
+
68
+ function warn(message) {
69
+ console.error(`warn ${message}`);
70
+ }
71
+
72
+ function fail(message) {
73
+ console.error(message);
74
+ process.exit(1);
75
+ }
76
+
77
+ // ── Wizard ───────────────────────────────────────────────────────────────
78
+ // An interactive run on a terminal gets the wizard treatment: a banner, one
79
+ // stage per step, a confirm gate before anything changes, and a closing
80
+ // summary. A run with --yes, or without a terminal, keeps the plain tag
81
+ // output and never prompts.
82
+
83
+ const COLOR =
84
+ Boolean(process.stdout.isTTY) && process.env.NO_COLOR === undefined && process.env.TERM !== 'dumb';
85
+ const C = COLOR
86
+ ? { bold: '\x1b[1m', dim: '\x1b[2m', reset: '\x1b[0m', blue: '\x1b[34m', green: '\x1b[32m', yellow: '\x1b[33m' }
87
+ : { bold: '', dim: '', reset: '', blue: '', green: '', yellow: '' };
88
+
89
+ let stageTotal = 0;
90
+ let stageIndex = 0;
91
+ let rl = null;
92
+
93
+ function getRl() {
94
+ rl ??= readline.createInterface({ input: process.stdin, output: process.stdout });
95
+ return rl;
96
+ }
97
+
98
+ async function banner(title) {
99
+ console.clear();
100
+ console.log(`\n${C.bold}${C.blue} ${title}${C.reset}`);
101
+ console.log(`${C.dim} ${stageTotal} stages${C.reset}\n`);
102
+ console.log(`${C.dim} The installer shows each step before it runs. Stop any time with${C.reset}`);
103
+ console.log(`${C.dim} Ctrl-C and re-run later. Saved choices become the defaults.${C.reset}\n`);
104
+ await getRl().question(` ${C.dim}Ready to start? [Enter]${C.reset} `);
105
+ }
106
+
107
+ function stage(name) {
108
+ console.clear();
109
+ stageIndex += 1;
110
+ console.log(`\n${C.bold}${C.blue}▸ Stage ${stageIndex}/${stageTotal} · ${name}${C.reset}`);
111
+ }
112
+
113
+ function note(message) {
114
+ console.log(` ${C.dim}${message}${C.reset}`);
115
+ }
116
+
117
+ async function confirm(question) {
118
+ const answer = (await getRl().question(` ${C.yellow}? ${question} [y/N]${C.reset} `))
119
+ .trim()
120
+ .toLowerCase();
121
+ return answer === 'y' || answer === 'yes';
122
+ }
123
+
124
+ function finish(headline, notes = []) {
125
+ console.clear();
126
+ console.log(`\n${C.bold}${C.green} ✓ ${headline}${C.reset}\n`);
127
+ for (const line of notes) note(line);
128
+ console.log('');
129
+ }
130
+
131
+ function parseHosts(list) {
132
+ const hosts = [...new Set(list.split(',').map((s) => s.trim()).filter(Boolean))];
133
+ for (const host of hosts) {
134
+ if (!HOSTS[host]) fail(`error: unknown host "${host}". Choose from: ${HOST_NAMES.join(', ')}`);
135
+ }
136
+ return hosts;
137
+ }
138
+
139
+ function parseScope(scope) {
140
+ if (!SCOPES.includes(scope)) fail(`error: unknown scope "${scope}". Choose from: ${SCOPES.join(', ')}`);
141
+ return scope;
142
+ }
143
+
144
+ function detectHosts() {
145
+ return HOST_NAMES.filter((name) => fs.existsSync(path.join(os.homedir(), HOSTS[name].marker)));
146
+ }
147
+
148
+ function detectedOrAll() {
149
+ const detected = detectHosts();
150
+ return detected.length ? detected : HOST_NAMES;
151
+ }
152
+
153
+ function readConfig(clone) {
154
+ try {
155
+ return JSON.parse(fs.readFileSync(configPath(clone), 'utf8'));
156
+ } catch {
157
+ return null;
158
+ }
159
+ }
160
+
161
+ function saveConfig(clone, config) {
162
+ const file = configPath(clone);
163
+ fs.mkdirSync(path.dirname(file), { recursive: true });
164
+ fs.writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
165
+ say('save', clone ? '.rstack/config.local.json' : file);
166
+ }
167
+
168
+ function plan(hosts, base) {
169
+ const skillDirs = new Set();
170
+ const agentDirs = new Set();
171
+ for (const host of hosts) {
172
+ for (const dir of HOSTS[host].skills) skillDirs.add(path.join(base, dir));
173
+ for (const dir of HOSTS[host].agents) agentDirs.add(path.join(base, dir));
174
+ }
175
+ return { skillDirs: [...skillDirs], agentDirs: [...agentDirs] };
176
+ }
177
+
178
+ function skillNames(root) {
179
+ try {
180
+ return fs
181
+ .readdirSync(path.join(root, 'skills'), { withFileTypes: true })
182
+ .filter((entry) => entry.isDirectory())
183
+ .map((entry) => entry.name)
184
+ .sort();
185
+ } catch {
186
+ return [];
187
+ }
188
+ }
189
+
190
+ function agentFiles(root) {
191
+ try {
192
+ return fs
193
+ .readdirSync(path.join(root, 'agents'), { withFileTypes: true })
194
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
195
+ .map((entry) => entry.name)
196
+ .sort();
197
+ } catch {
198
+ return [];
199
+ }
200
+ }
201
+
202
+ // Package mode only: copy the payload into STORE. A temp dir plus rename
203
+ // keeps the swap close to atomic, so a failed copy never leaves a half store.
204
+ function syncStore() {
205
+ fs.mkdirSync(STORE, { recursive: true });
206
+ for (const dir of ['skills', 'agents']) {
207
+ const count = dir === 'skills' ? skillNames(REPO).length : agentFiles(REPO).length;
208
+ const dest = path.join(STORE, dir);
209
+ const tmp = `${dest}.tmp-${process.pid}`;
210
+ fs.rmSync(tmp, { recursive: true, force: true });
211
+ fs.cpSync(path.join(REPO, dir), tmp, { recursive: true });
212
+ fs.rmSync(dest, { recursive: true, force: true });
213
+ fs.renameSync(tmp, dest);
214
+ say('copy', `${dest} (${count} ${dir})`);
215
+ }
216
+ }
217
+
218
+ function lstat(pathname) {
219
+ try {
220
+ return fs.lstatSync(pathname);
221
+ } catch {
222
+ return null;
223
+ }
224
+ }
225
+
226
+ function linkOne(src, dest, force) {
227
+ const stat = lstat(dest);
228
+ if (stat && !stat.isSymbolicLink()) {
229
+ if (!force) {
230
+ say('skip', `${dest} (exists and is not a symlink)`);
231
+ return 'skip';
232
+ }
233
+ const backup = `${dest}.bak`;
234
+ if (lstat(backup)) {
235
+ say('skip', `${dest} (backup ${backup} already exists)`);
236
+ return 'skip';
237
+ }
238
+ fs.renameSync(dest, backup);
239
+ say('backup', `${dest} -> ${backup}`);
240
+ }
241
+ fs.rmSync(dest, { force: true });
242
+ fs.symlinkSync(src, dest, fs.statSync(src).isDirectory() ? 'dir' : 'file');
243
+ say('link', `${dest} -> ${src}`);
244
+ return 'link';
245
+ }
246
+
247
+ function unlinkOne(src, dest) {
248
+ const stat = lstat(dest);
249
+ if (stat?.isSymbolicLink() && fs.readlinkSync(dest) === src) {
250
+ fs.unlinkSync(dest);
251
+ say('rm', dest);
252
+ return 'rm';
253
+ }
254
+ return 'keep';
255
+ }
256
+
257
+ function runLink(hosts, base, force, src) {
258
+ const { skillDirs, agentDirs } = plan(hosts, base);
259
+ const counts = { link: 0, skip: 0 };
260
+ for (const dir of skillDirs) {
261
+ fs.mkdirSync(dir, { recursive: true });
262
+ for (const name of skillNames(src)) {
263
+ counts[linkOne(path.join(src, 'skills', name), path.join(dir, name), force)] += 1;
264
+ }
265
+ }
266
+ for (const dir of agentDirs) {
267
+ fs.mkdirSync(dir, { recursive: true });
268
+ for (const file of agentFiles(src)) {
269
+ counts[linkOne(path.join(src, 'agents', file), path.join(dir, file), force)] += 1;
270
+ }
271
+ }
272
+ return counts;
273
+ }
274
+
275
+ function runUnlink(hosts, base, src) {
276
+ const { skillDirs, agentDirs } = plan(hosts, base);
277
+ for (const dir of skillDirs) {
278
+ for (const name of skillNames(src)) {
279
+ unlinkOne(path.join(src, 'skills', name), path.join(dir, name));
280
+ }
281
+ }
282
+ for (const dir of agentDirs) {
283
+ for (const file of agentFiles(src)) {
284
+ unlinkOne(path.join(src, 'agents', file), path.join(dir, file));
285
+ }
286
+ }
287
+ }
288
+
289
+ function onPath(bin) {
290
+ try {
291
+ execFileSync('which', [bin], { stdio: 'pipe' });
292
+ return true;
293
+ } catch {
294
+ return false;
295
+ }
296
+ }
297
+
298
+ function configureGitHooks() {
299
+ try {
300
+ execFileSync('git', ['-C', REPO, 'config', 'core.hooksPath', '.githooks'], { stdio: 'pipe' });
301
+ say('git', 'core.hooksPath=.githooks');
302
+ } catch {
303
+ warn('could not set core.hooksPath. Is this clone a git repository?');
304
+ }
305
+ if (!onPath('gitleaks')) {
306
+ warn('gitleaks is not installed. Install it with: brew install gitleaks');
307
+ }
308
+ }
309
+
310
+ function gitPull() {
311
+ try {
312
+ execFileSync('git', ['-C', REPO, 'pull', '--ff-only'], { stdio: 'inherit' });
313
+ } catch {
314
+ warn('git pull failed. Linking the files that are present in this clone.');
315
+ }
316
+ }
317
+
318
+ async function promptHosts(defaults) {
319
+ const detected = detectHosts();
320
+ console.log('');
321
+ console.log(' Detected hosts:');
322
+ for (const name of HOST_NAMES) {
323
+ const hit = detected.includes(name);
324
+ const where = hit ? path.join(os.homedir(), HOSTS[name].marker) : `no ~/${HOSTS[name].marker}`;
325
+ console.log(` ${hit ? 'found' : ' - '} ${name.padEnd(11)} ${where}`);
326
+ }
327
+ console.log('');
328
+ const answer = (
329
+ await getRl().question(` Install for ${defaults.join(', ')}? [Y/n/list] `)
330
+ ).trim().toLowerCase();
331
+ if (answer === '' || answer === 'y' || answer === 'yes') return defaults;
332
+ const list =
333
+ answer === 'n' || answer === 'no' || answer === 'list'
334
+ ? await getRl().question(` Hosts (comma-separated: ${HOST_NAMES.join(', ')}): `)
335
+ : answer;
336
+ const hosts = parseHosts(list);
337
+ if (hosts.length === 0) fail('error: no hosts selected.');
338
+ return hosts;
339
+ }
340
+
341
+ async function promptScope(defaultScope) {
342
+ const answer = (
343
+ await getRl().question(` Scope: global (all projects) or project (one directory)? [${defaultScope}] `)
344
+ ).trim().toLowerCase();
345
+ if (answer === '') return defaultScope;
346
+ if (answer === 'g') return 'global';
347
+ if (answer === 'p') return 'project';
348
+ return parseScope(answer);
349
+ }
350
+
351
+ async function promptProjectDir(defaultDir) {
352
+ const answer = (await getRl().question(` Project directory? [${defaultDir}] `)).trim();
353
+ return answer ? path.resolve(answer) : defaultDir;
354
+ }
355
+
356
+ function showPlan(hosts, scope, base, clone) {
357
+ const { skillDirs, agentDirs } = plan(hosts, base);
358
+ console.log('');
359
+ console.log(` hosts ${hosts.join(', ')}`);
360
+ console.log(` scope ${scope}`);
361
+ console.log(` base ${base}`);
362
+ if (!clone) console.log(` store ${STORE} ${C.dim}(the payload copies here first)${C.reset}`);
363
+ console.log('');
364
+ for (const dir of skillDirs) console.log(` ${dir} ${C.dim}${skillNames(REPO).length} skills${C.reset}`);
365
+ for (const dir of agentDirs) console.log(` ${dir} ${C.dim}${agentFiles(REPO).length} agents${C.reset}`);
366
+ console.log('');
367
+ }
368
+
369
+ function resolveBase(scope, projectDir) {
370
+ if (scope === 'project') return path.resolve(projectDir ?? '.');
371
+ return os.homedir();
372
+ }
373
+
374
+ async function main() {
375
+ let values;
376
+ let positionals;
377
+ try {
378
+ ({ values, positionals } = parseArgs({
379
+ allowPositionals: true,
380
+ options: {
381
+ hosts: { type: 'string' },
382
+ scope: { type: 'string' },
383
+ 'project-dir': { type: 'string' },
384
+ yes: { type: 'boolean', default: false },
385
+ force: { type: 'boolean', default: false },
386
+ 'no-pull': { type: 'boolean', default: false },
387
+ uninstall: { type: 'boolean', default: false },
388
+ help: { type: 'boolean', default: false },
389
+ },
390
+ }));
391
+ } catch (error) {
392
+ fail(`error: ${error.message}\n\n${USAGE}`);
393
+ }
394
+
395
+ if (values.help) {
396
+ process.stdout.write(USAGE);
397
+ return;
398
+ }
399
+
400
+ let command = positionals[0] ?? 'install';
401
+ if (values.uninstall) command = 'uninstall';
402
+ if (!['install', 'update', 'uninstall'].includes(command) || positionals.length > 1) {
403
+ process.stderr.write(USAGE);
404
+ process.exit(1);
405
+ }
406
+
407
+ const clone = isClone();
408
+ const src = clone ? REPO : STORE;
409
+ const saved = readConfig(clone);
410
+ let hosts = values.hosts ? parseHosts(values.hosts) : null;
411
+ let scope = values.scope ? parseScope(values.scope) : null;
412
+ const wizard = !values.yes && process.stdout.isTTY;
413
+
414
+ try {
415
+ if (command === 'install') {
416
+ if (wizard) {
417
+ stageTotal = 2 + (hosts ? 0 : 1) + (scope ? 0 : 1);
418
+ await banner('rstack install');
419
+ if (!hosts) {
420
+ stage('Hosts');
421
+ hosts = await promptHosts(saved?.hosts ?? detectedOrAll());
422
+ }
423
+ let projectDir = values['project-dir'] ?? saved?.projectDir;
424
+ if (!scope) {
425
+ stage('Scope');
426
+ scope = await promptScope(saved?.scope ?? 'global');
427
+ if (scope === 'project' && !values['project-dir']) {
428
+ projectDir = await promptProjectDir(saved?.projectDir ?? process.cwd());
429
+ }
430
+ }
431
+ const base = resolveBase(scope, projectDir);
432
+ stage('Review');
433
+ showPlan(hosts, scope, base, clone);
434
+ if (values.force) note('--force is on: a conflicting real file moves to <name>.bak first.');
435
+ if (!(await confirm('Link these files?'))) fail('error: install cancelled. Nothing was linked.');
436
+ stage('Link');
437
+ if (!clone) syncStore();
438
+ const counts = runLink(hosts, base, values.force, src);
439
+ if (clone) configureGitHooks();
440
+ saveConfig(clone, { hosts, scope, ...(scope === 'project' ? { projectDir: base } : {}) });
441
+ finish(`${skillNames(src).length} skills linked for ${hosts.join(', ')} (${scope})`, [
442
+ ...(counts.skip > 0
443
+ ? [`${counts.skip} name(s) skipped. Rename or remove the real files, then run install again.`]
444
+ : []),
445
+ 'next: run /setup-rstack in each host to configure models',
446
+ ]);
447
+ return;
448
+ }
449
+ hosts ??= detectedOrAll();
450
+ scope ??= 'global';
451
+ const base = resolveBase(scope, values['project-dir'] ?? saved?.projectDir);
452
+ if (!clone) syncStore();
453
+ const counts = runLink(hosts, base, values.force, src);
454
+ if (clone) configureGitHooks();
455
+ saveConfig(clone, { hosts, scope, ...(scope === 'project' ? { projectDir: base } : {}) });
456
+ say('done', `${skillNames(src).length} skills linked for ${hosts.join(', ')} (${scope})`);
457
+ if (counts.skip > 0) {
458
+ say('note', `${counts.skip} name(s) skipped. Rename or remove the real files, then run install again.`);
459
+ }
460
+ say('next', 'run /setup-rstack in each host to configure models');
461
+ return;
462
+ }
463
+
464
+ if (command === 'update') {
465
+ hosts ??= saved?.hosts ?? detectedOrAll();
466
+ scope ??= saved?.scope ?? 'global';
467
+ const base = resolveBase(scope, values['project-dir'] ?? saved?.projectDir);
468
+ if (wizard) {
469
+ stageTotal = clone && !values['no-pull'] ? 3 : 2;
470
+ await banner('rstack update');
471
+ stage('Review');
472
+ showPlan(hosts, scope, base, clone);
473
+ note(
474
+ clone
475
+ ? values['no-pull']
476
+ ? 'the git pull is skipped (--no-pull)'
477
+ : 'git pull --ff-only runs in the clone first'
478
+ : 'the npm package provides the files; no git pull runs'
479
+ );
480
+ if (!(await confirm('Update this install?'))) fail('error: update cancelled. Nothing changed.');
481
+ if (clone && !values['no-pull']) {
482
+ stage('Pull');
483
+ gitPull();
484
+ }
485
+ stage('Link');
486
+ if (!clone) syncStore();
487
+ const counts = runLink(hosts, base, values.force, src);
488
+ finish(`${skillNames(src).length} skills linked for ${hosts.join(', ')} (${scope})`, [
489
+ ...(counts.skip > 0
490
+ ? [`${counts.skip} name(s) skipped. Rename or remove the real files, then run update again.`]
491
+ : []),
492
+ ]);
493
+ return;
494
+ }
495
+ if (clone && !values['no-pull']) gitPull();
496
+ if (!clone) syncStore();
497
+ const counts = runLink(hosts, base, values.force, src);
498
+ say('done', `${skillNames(src).length} skills linked for ${hosts.join(', ')} (${scope})`);
499
+ if (counts.skip > 0) {
500
+ say('note', `${counts.skip} name(s) skipped. Rename or remove the real files, then run update again.`);
501
+ }
502
+ return;
503
+ }
504
+
505
+ hosts ??= saved?.hosts ?? HOST_NAMES;
506
+ scope ??= saved?.scope ?? 'global';
507
+ const base = resolveBase(scope, values['project-dir'] ?? saved?.projectDir);
508
+ if (wizard) {
509
+ stageTotal = 2;
510
+ await banner('rstack uninstall');
511
+ stage('Review');
512
+ showPlan(hosts, scope, base, clone);
513
+ note(
514
+ clone
515
+ ? 'only symlinks that point into this clone are removed. Real files stay.'
516
+ : `only symlinks that point into ${STORE} are removed. Real files stay.`
517
+ );
518
+ if (!(await confirm('Remove these links?'))) fail('error: uninstall cancelled. Nothing was removed.');
519
+ stage('Unlink');
520
+ runUnlink(hosts, base, src);
521
+ finish(`links removed for ${hosts.join(', ')} (${scope})`, [
522
+ clone
523
+ ? 'the clone, .rstack/config.local.json, and core.hooksPath stay'
524
+ : `the store at ${STORE} stays; delete it to remove the copied skills`,
525
+ ]);
526
+ return;
527
+ }
528
+ runUnlink(hosts, base, src);
529
+ say('done', `links removed for ${hosts.join(', ')} (${scope})`);
530
+ } catch (error) {
531
+ // Ctrl+D at a prompt rejects with AbortError; Ctrl+C never reaches here.
532
+ if (error?.code === 'ABORT_ERR') fail('\nerror: cancelled.');
533
+ throw error;
534
+ } finally {
535
+ rl?.close();
536
+ }
537
+ }
538
+
539
+ await main();
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "ramstack",
3
+ "version": "0.1.0",
4
+ "description": "Agent skills for Claude Code, Codex, and pi",
5
+ "author": "Justin Ramos <justinramos@outlook.com>",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/justinramos101/ramstack.git"
10
+ },
11
+ "keywords": [
12
+ "agent-skills",
13
+ "ai-agents",
14
+ "claude-code",
15
+ "codex",
16
+ "skills"
17
+ ],
18
+ "bin": {
19
+ "rstack": "cli/rstack.mjs"
20
+ },
21
+ "engines": {
22
+ "node": ">=18"
23
+ },
24
+ "files": [
25
+ "cli",
26
+ "skills",
27
+ "agents"
28
+ ]
29
+ }