@mayurdluffy/ccwf 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/README.md +110 -2
- package/bin/ccwf.js +7 -0
- package/lib/index.js +312 -0
- package/lib/installer.js +127 -0
- package/lib/machine.js +167 -0
- package/lib/merge.js +92 -0
- package/lib/modules.js +199 -0
- package/package.json +45 -4
- package/templates/agent-memory/MEMORY.md +4 -0
- package/templates/blocks/browser.md +9 -0
- package/templates/blocks/cbm.md +9 -0
- package/templates/blocks/core.md +35 -0
- package/templates/blocks/memory.md +7 -0
- package/templates/blocks/tracking.md +11 -0
- package/templates/blocks/workers.md +3 -0
- package/templates/cursor-cli.json +17 -0
- package/templates/cursor-rule.mdc +6 -0
- package/templates/docs/BACKLOG.md +4 -0
- package/templates/docs/DECISIONS.md +4 -0
- package/templates/docs/FEATURES.md +8 -0
- package/templates/docs/GLOSSARY.md +3 -0
- package/templates/docs/adr/.gitkeep +0 -0
- package/templates/docs/agents/domain.md +52 -0
- package/templates/docs/agents/issue-tracker.md +30 -0
- package/templates/docs/agents/setup.md +48 -0
- package/templates/docs/agents/triage-labels.md +15 -0
- package/templates/docs/agents/workflow.md +58 -0
- package/templates/machine.json +47 -0
- package/templates/skills/agent-browser/LICENSE +202 -0
- package/templates/skills/agent-browser/SKILL.md +51 -0
- package/templates/skills/commit/SKILL.md +77 -0
- package/templates/skills/orchestrate-workers/HC.md +10 -0
- package/templates/skills/orchestrate-workers/SKILL.md +138 -0
- package/templates/skills/orchestrate-workers/approve.sh +7 -0
- package/templates/skills/orchestrate-workers/claude-worker-settings.json +14 -0
- package/templates/skills/orchestrate-workers/end.sh +13 -0
- package/templates/skills/orchestrate-workers/grid.sh +24 -0
- package/templates/skills/orchestrate-workers/pane.sh +51 -0
- package/templates/skills/orchestrate-workers/watch.sh +42 -0
- package/templates/skills/orchestrate-workers/workers.conf +8 -0
- package/templates/skills/verify-ui/PLAYBOOK.md +81 -0
- package/templates/skills/verify-ui/SKILL.md +88 -0
- package/templates/skills/verify-ui/lib.sh +63 -0
- package/templates/skills/verify-ui/probe.sh +78 -0
- package/templates/skills/verify-ui/run.sh +113 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mayurdluffy
|
|
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
CHANGED
|
@@ -1,3 +1,111 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @mayurdluffy/ccwf
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@mayurdluffy/ccwf)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://github.com/mayurdluffy/ccwf/actions/workflows/ci.yml)
|
|
6
|
+
[](https://github.com/mayurdluffy/ccwf/actions/workflows/publish.yml)
|
|
7
|
+
|
|
8
|
+
One command that sets up the same agent system on any computer and in any repo, for **Claude Code, Codex and Cursor** (only those three; macOS and Linux).
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
cd your-repo
|
|
12
|
+
npx @mayurdluffy/cbm-cli install --agents claude,codex,cursor # code-graph MCP first (see docs/agents/setup.md after init)
|
|
13
|
+
npx @mayurdluffy/ccwf init # tools + user-level skills + project files
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`init` shows what is missing on your machine, installs it after one confirmation, then writes the project files. `--yes` skips the question: it runs vendor install scripts from the network and installs skills from the current head of third-party repos. Re-running is safe: your own text and config keys are kept. `--force` overwrites only files ccwf owns, never seeded docs.
|
|
17
|
+
|
|
18
|
+
## What you get
|
|
19
|
+
|
|
20
|
+
**On the machine (once):** the pinned user-level skills (`mattpocock/skills`, `find-skills`), `agent-browser`, `herdr` + `hc`, `gh`, `ffmpeg`, the Claude / Codex / Cursor CLIs you pick, and Codex trust for the repo. Chrome and logins are checked and explained, not installed.
|
|
21
|
+
|
|
22
|
+
**In the repo (committed):**
|
|
23
|
+
|
|
24
|
+
| Module | Writes |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `core` (always) | discipline rules in `AGENTS.md` (`CLAUDE.md` imports it), `commit` skill, Cursor rule, `docs/agents/workflow.md` and `setup.md` |
|
|
27
|
+
| `memory` | `.agent-memory/MEMORY.md` + rules |
|
|
28
|
+
| `tracking` | empty `docs/GLOSSARY.md`, `DECISIONS.md`, `FEATURES.md`, `BACKLOG.md`, `adr/` + rules; `docs/agents/{domain,issue-tracker,triage-labels}.md` |
|
|
29
|
+
| `browser` | `agent-browser` + `verify-ui` skills, `chrome-devtools` MCP, `.cursor/cli.json`, Claude MCP approval and allowed sites |
|
|
30
|
+
| `workers` | `orchestrate-workers` skill (herdr panes, `hc` reference, `workers.conf`) |
|
|
31
|
+
|
|
32
|
+
Skills live in `.agents/skills/` (Codex, Cursor); Claude Code gets symlinks in `.claude/skills/`. Files are written only for the agents you select.
|
|
33
|
+
|
|
34
|
+
The code-graph server itself is set up by [`@mayurdluffy/cbm-cli`](https://github.com/mayurdluffy/cbm-cli); once `.codebase-memory/cbm-server.sh` exists, `ccwf sync` adds the graph-first navigation rules.
|
|
35
|
+
|
|
36
|
+
## How skills get their settings
|
|
37
|
+
|
|
38
|
+
A skill that needs project settings asks you the first time it runs and saves them next to itself. `verify-ui` asks for the dev server URL and the sites the browser may visit, writes `.agents/skills/verify-ui/verify-ui.conf`, and runs `ccwf sync`, which copies the allowed sites into `.claude/settings.json` and `.codex/config.toml`.
|
|
39
|
+
|
|
40
|
+
## Commands
|
|
41
|
+
|
|
42
|
+
| Command | What it does |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `ccwf init` | machine setup + repo setup (`--agents`, `--modules`, `--project-only`, `--machine-only`, `--yes`, `--dry-run`, `--force`, `--dir`) |
|
|
45
|
+
| `ccwf add <module>` | add a module |
|
|
46
|
+
| `ccwf update` | the only command that re-renders templates from this ccwf version; edited files are skipped |
|
|
47
|
+
| `ccwf sync` | push skill settings into agent configs and add navigation rules; does not re-render templates |
|
|
48
|
+
| `ccwf doctor` | check tools, skills and files; offer to fix |
|
|
49
|
+
| `ccwf eject <skill>` | stop managing a skill you changed |
|
|
50
|
+
| `ccwf status` | installed modules, drifted files |
|
|
51
|
+
|
|
52
|
+
State is in `.ccwf.json` (choices and a hash of every file ccwf wrote). Commit it.
|
|
53
|
+
|
|
54
|
+
## What a first run looks like
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
$ npx @mayurdluffy/ccwf init
|
|
58
|
+
+ AGENTS.md
|
|
59
|
+
+ .agents/skills/commit/SKILL.md
|
|
60
|
+
...
|
|
61
|
+
repo: 40 created, 0 updated, 0 unchanged, 0 skipped
|
|
62
|
+
machine:
|
|
63
|
+
✗ mattpocock/skills skills (0/38 installed)
|
|
64
|
+
✓ gh (GitHub CLI)
|
|
65
|
+
✗ Codex trusts this repo
|
|
66
|
+
|
|
67
|
+
Will run:
|
|
68
|
+
- install 38 skills from mattpocock/skills with `npx skills add` (full command: ccwf init --machine-only --dry-run)
|
|
69
|
+
- add [projects."/path/to/repo"] trust_level = "trusted" to ~/.codex/config.toml
|
|
70
|
+
Install all? [Y/n]:
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Then follow `docs/agents/workflow.md` in your repo. Commit `.ccwf.json` and the files `init` listed.
|
|
74
|
+
|
|
75
|
+
## Troubleshooting
|
|
76
|
+
|
|
77
|
+
| You see | Do |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `skipped: not a terminal; re-run with --yes` | run `ccwf init --yes` from a terminal script, or install by hand from `docs/agents/setup.md` |
|
|
80
|
+
| `warning: <file>: modified locally, left as is` | your edit was kept; `ccwf update --force` replaces only files ccwf owns |
|
|
81
|
+
| `... already defined by hand, skipped` | you wrote that config table yourself; ccwf leaves it alone |
|
|
82
|
+
| `next: codebase-memory is not set up here` | run `npx @mayurdluffy/cbm-cli install --agents claude,codex,cursor`, then `ccwf sync` |
|
|
83
|
+
| anything else | `ccwf doctor` checks tools, skills and files and offers to fix them |
|
|
84
|
+
|
|
85
|
+
## Read next (in your repo after `init`)
|
|
86
|
+
|
|
87
|
+
- `docs/agents/workflow.md` — grill-with-docs → to-spec → to-tickets → orchestrate-workers.
|
|
88
|
+
- `docs/agents/setup.md` — every tool and skill, and how to install each by hand.
|
|
89
|
+
|
|
90
|
+
## Maintaining the templates
|
|
91
|
+
|
|
92
|
+
`node scripts/harvest.js <repo> --terms yourproduct` diffs a working repo's skills against `templates/skills/`, flags project-specific lines, and with `--apply` copies the clean ones. Not published to npm.
|
|
93
|
+
|
|
94
|
+
## Releasing
|
|
95
|
+
|
|
96
|
+
Push a tag `vX.Y.Z`; `.github/workflows/publish.yml` publishes using the `NPM_ACCESS_TOKEN` repository secret.
|
|
97
|
+
|
|
98
|
+
## Contributing
|
|
99
|
+
|
|
100
|
+
Issues and PRs are welcome; see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
MIT. `templates/skills/agent-browser/SKILL.md` is the skill stub from [vercel-labs/agent-browser](https://github.com/vercel-labs/agent-browser) (Apache-2.0, license text shipped next to it, copied from the version installed with the CLI).
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
<p align="center">
|
|
109
|
+
<a href="https://github.com/mayurdluffy/ccwf">GitHub</a> •
|
|
110
|
+
<a href="https://www.npmjs.com/package/@mayurdluffy/ccwf">npm</a>
|
|
111
|
+
</p>
|
package/bin/ccwf.js
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const os = require("os");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const readline = require("readline/promises");
|
|
6
|
+
const { Installer, sha } = require("./installer");
|
|
7
|
+
const { MODULES, ORDER, AGENTS, makeParams, sync } = require("./modules");
|
|
8
|
+
const machine = require("./machine");
|
|
9
|
+
const pkg = require("../package.json");
|
|
10
|
+
|
|
11
|
+
const MANIFEST = ".ccwf.json";
|
|
12
|
+
const STATE_MARK = { created: "+", updated: "~", unchanged: "=", skipped: "!" };
|
|
13
|
+
|
|
14
|
+
const HELP = `ccwf ${pkg.version} — agent setup for Claude Code, Codex and Cursor (macOS, Linux)
|
|
15
|
+
|
|
16
|
+
Usage: ccwf <command> [options]
|
|
17
|
+
|
|
18
|
+
init set up this computer (tools, user-level skills) and the current repo
|
|
19
|
+
add <module> add one module to an initialised repo
|
|
20
|
+
update the only command that re-renders templates from this ccwf version (keeps files you edited)
|
|
21
|
+
sync push skill settings and add navigation rules; does not re-render templates
|
|
22
|
+
doctor check tools, skills and files; offer to fix what is missing
|
|
23
|
+
eject <skill> stop managing a skill you changed
|
|
24
|
+
status show installed modules and drifted files
|
|
25
|
+
|
|
26
|
+
Modules: ${ORDER.join(", ")}
|
|
27
|
+
${ORDER.map((m) => ` ${m.padEnd(9)} ${MODULES[m].desc}`).join("\n")}
|
|
28
|
+
|
|
29
|
+
Options:
|
|
30
|
+
--agents a,b claude,codex,cursor (default: all)
|
|
31
|
+
--modules a,b|all default: all
|
|
32
|
+
--project-only skip machine setup
|
|
33
|
+
--machine-only skip the repo
|
|
34
|
+
--yes, -y no prompts (install everything missing)
|
|
35
|
+
--dry-run show what would change, write and install nothing
|
|
36
|
+
--force overwrite only files ccwf owns, never seeded docs
|
|
37
|
+
--dir PATH target repo (default: current directory)
|
|
38
|
+
-h, --help -v, --version`;
|
|
39
|
+
|
|
40
|
+
const OPTIONS = new Set(["agents", "modules", "dir", "project-only", "machine-only", "yes", "dry-run", "force", "help", "version"]);
|
|
41
|
+
const VALUED = new Set(["agents", "modules", "dir"]);
|
|
42
|
+
const SHORT = { y: "yes", h: "help", v: "version" };
|
|
43
|
+
|
|
44
|
+
function parseArgs(argv) {
|
|
45
|
+
const o = { _: [] };
|
|
46
|
+
for (let i = 0; i < argv.length; i++) {
|
|
47
|
+
const a = argv[i];
|
|
48
|
+
if (a.startsWith("--")) {
|
|
49
|
+
const [k, inline] = a.slice(2).split("=");
|
|
50
|
+
if (!OPTIONS.has(k)) throw new Error(`unknown option: --${k}`);
|
|
51
|
+
if (VALUED.has(k)) o[k] = inline ?? argv[++i];
|
|
52
|
+
else o[k] = true;
|
|
53
|
+
} else if (a.startsWith("-") && a !== "-") {
|
|
54
|
+
const k = a.length === 2 ? SHORT[a[1]] : undefined;
|
|
55
|
+
if (!k) throw new Error(`unknown option: ${a}`);
|
|
56
|
+
o[k] = true;
|
|
57
|
+
} else o._.push(a);
|
|
58
|
+
}
|
|
59
|
+
return o;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function list(v, all, what) {
|
|
63
|
+
const items = v === "all" ? all : String(v).split(",").map((s) => s.trim()).filter(Boolean);
|
|
64
|
+
const bad = items.filter((i) => !all.includes(i));
|
|
65
|
+
if (bad.length) throw new Error(`unknown ${what}: ${bad.join(", ")} (valid: ${all.join(", ")})`);
|
|
66
|
+
return items;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function loadManifest(root) {
|
|
70
|
+
try {
|
|
71
|
+
return JSON.parse(fs.readFileSync(path.join(root, MANIFEST), "utf8"));
|
|
72
|
+
} catch (err) {
|
|
73
|
+
if (err.code === "ENOENT") return null;
|
|
74
|
+
throw new Error(`${MANIFEST} is not valid JSON: ${err.message}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function saveManifest(root, data, version = pkg.version) {
|
|
79
|
+
fs.writeFileSync(path.join(root, MANIFEST), JSON.stringify({ ...data, ccwf: version }, null, 2) + "\n");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function confirm(question, def = true) {
|
|
83
|
+
if (!process.stdin.isTTY) return false;
|
|
84
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
85
|
+
try {
|
|
86
|
+
const a = (await rl.question(`${question} [${def ? "Y/n" : "y/N"}]: `)).trim().toLowerCase();
|
|
87
|
+
return a === "" ? def : a === "y" || a === "yes";
|
|
88
|
+
} finally {
|
|
89
|
+
rl.close();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// One line per path: a file touched by several modules reports its strongest state.
|
|
94
|
+
function collapse(log) {
|
|
95
|
+
const rank = { unchanged: 0, skipped: 1, updated: 2, created: 3 };
|
|
96
|
+
const by = new Map();
|
|
97
|
+
for (const { rel, state } of log) if (!by.has(rel) || rank[state] > rank[by.get(rel)]) by.set(rel, state);
|
|
98
|
+
return [...by].map(([rel, state]) => ({ rel, state }));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function report(ins, dryRun) {
|
|
102
|
+
const log = collapse(ins.log);
|
|
103
|
+
for (const { rel, state } of log) if (state !== "unchanged") console.log(` ${STATE_MARK[state]} ${rel}`);
|
|
104
|
+
const n = (s) => log.filter((l) => l.state === s).length;
|
|
105
|
+
console.log(`${dryRun ? "would change" : "repo"}: ${n("created")} created, ${n("updated")} updated, ${n("unchanged")} unchanged, ${n("skipped")} skipped`);
|
|
106
|
+
for (const w of ins.warnings) console.log(` warning: ${w}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// A server entry is the "codebase-memory" key under mcpServers (top level, or under
|
|
110
|
+
// projects in ~/.claude.json), or a Codex mcp_servers table. The name elsewhere is not one.
|
|
111
|
+
const CODEX_CBM_TABLE = /^\[mcp_servers\.(?:"codebase-memory"|'codebase-memory'|codebase-memory)(?:\.env)?\][ \t]*$/;
|
|
112
|
+
|
|
113
|
+
function hasCbmServer(obj) {
|
|
114
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj)) return false;
|
|
115
|
+
const box = obj.mcpServers;
|
|
116
|
+
return !!box && typeof box === "object" && !Array.isArray(box) && "codebase-memory" in box;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function jsonHasCbmServer(text, projects) {
|
|
120
|
+
const data = JSON.parse(text);
|
|
121
|
+
if (hasCbmServer(data)) return true;
|
|
122
|
+
if (!projects || !data || typeof data !== "object" || Array.isArray(data)) return false;
|
|
123
|
+
const map = data.projects;
|
|
124
|
+
if (!map || typeof map !== "object" || Array.isArray(map)) return false;
|
|
125
|
+
return Object.values(map).some((proj) => hasCbmServer(proj));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function tomlHasCbmTable(text) {
|
|
129
|
+
return text.split("\n").some((line) => CODEX_CBM_TABLE.test(line));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function globalCbmFindings() {
|
|
133
|
+
const home = os.homedir();
|
|
134
|
+
const files = [
|
|
135
|
+
[".claude.json", true],
|
|
136
|
+
[".claude/mcp.json", false],
|
|
137
|
+
[".codex/config.toml", false],
|
|
138
|
+
[".cursor/mcp.json", false],
|
|
139
|
+
];
|
|
140
|
+
const found = [];
|
|
141
|
+
for (const [rel, projects] of files) {
|
|
142
|
+
const file = path.join(home, rel);
|
|
143
|
+
let text;
|
|
144
|
+
try {
|
|
145
|
+
text = fs.readFileSync(file, "utf8");
|
|
146
|
+
} catch {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
const hit = rel.endsWith(".toml") ? tomlHasCbmTable(text) : jsonHasCbmServer(text, projects);
|
|
151
|
+
if (hit) found.push({ file, kind: "server" });
|
|
152
|
+
} catch {
|
|
153
|
+
found.push({ file, kind: "unparsed" });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return found;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---- project ----
|
|
160
|
+
|
|
161
|
+
function applyProject({ root, agents, modules, ejected, files, dryRun, force }) {
|
|
162
|
+
const ins = new Installer({ root, dryRun, force, files, ejected });
|
|
163
|
+
const ctx = { ins, agents, params: makeParams({ agents }) };
|
|
164
|
+
for (const m of ORDER.filter((x) => modules.includes(x))) MODULES[m].run(ctx);
|
|
165
|
+
const synced = sync(ctx, modules);
|
|
166
|
+
return { ins, synced };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function projectNextSteps(synced) {
|
|
170
|
+
if (!synced.cbm) {
|
|
171
|
+
console.log(" next: codebase-memory is not set up here. Run `npx @mayurdluffy/cbm-cli install --agents claude,codex,cursor`, then `npx @mayurdluffy/ccwf sync` (see docs/agents/setup.md)");
|
|
172
|
+
}
|
|
173
|
+
for (const { file, kind } of globalCbmFindings()) {
|
|
174
|
+
const shown = file.replace(os.homedir(), "~");
|
|
175
|
+
if (kind === "server") console.log(` warning: ${shown} registers a global codebase-memory server; run \`npx @mayurdluffy/cbm-cli clean-global\``);
|
|
176
|
+
else console.log(` note: ${shown} could not be checked`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ---- machine ----
|
|
181
|
+
|
|
182
|
+
async function machineSetup({ agents, modules, root, opts, fixOnly = false }) {
|
|
183
|
+
const items = machine.plan({ agents, modules, repoRoot: root });
|
|
184
|
+
const missing = items.filter((i) => i.status === "missing");
|
|
185
|
+
console.log("machine:");
|
|
186
|
+
for (const i of items) console.log(` ${i.status === "ok" ? "✓" : "✗"} ${i.label}`);
|
|
187
|
+
if (missing.length === 0) return console.log(" everything is installed");
|
|
188
|
+
|
|
189
|
+
for (const i of missing.filter((m) => m.kind === "manual")) console.log(` by hand: ${i.hint}`);
|
|
190
|
+
const todo = missing.filter((i) => i.kind !== "manual");
|
|
191
|
+
if (todo.length === 0) return;
|
|
192
|
+
|
|
193
|
+
const dry = opts["dry-run"];
|
|
194
|
+
console.log(dry ? "\nWould run:" : "\nWill run:");
|
|
195
|
+
for (const i of todo) console.log(` - ${i.kind === "run" ? (dry ? i.cmd : i.summary || i.cmd) : i.hint}`);
|
|
196
|
+
if (dry) return;
|
|
197
|
+
const go = opts.yes || (await confirm("Install all?"));
|
|
198
|
+
if (!go) {
|
|
199
|
+
console.log(!process.stdin.isTTY && !opts.yes ? " skipped: not a terminal; re-run with --yes to install" : " skipped");
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
for (const r of machine.run(todo)) console.log(` ${r.ok ? "installed" : "FAILED"}: ${r.id}${r.error ? ` (${r.error})` : ""}`);
|
|
203
|
+
console.log(`\nLog in once (cannot be scripted): ${machine.loginHints(agents).join(" · ")}`);
|
|
204
|
+
if (!fixOnly) console.log("Restart your coding agents so they see new skills.");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ---- commands ----
|
|
208
|
+
|
|
209
|
+
async function install(cmd, opts, root) {
|
|
210
|
+
const prev = loadManifest(root);
|
|
211
|
+
let agents;
|
|
212
|
+
let modules;
|
|
213
|
+
const ejected = prev?.ejected || [];
|
|
214
|
+
if (cmd === "update") {
|
|
215
|
+
if (!prev) throw new Error(`no ${MANIFEST} here; run \`ccwf init\` first`);
|
|
216
|
+
({ agents, modules } = prev);
|
|
217
|
+
} else {
|
|
218
|
+
agents = list(opts.agents ?? (prev?.agents || AGENTS).join(","), AGENTS, "agent");
|
|
219
|
+
modules = list(opts.modules ?? "all", ORDER, "module");
|
|
220
|
+
// Re-init keeps modules already installed, the same way add does.
|
|
221
|
+
if (prev && (cmd === "add" || cmd === "init")) modules = [...new Set([...(prev.modules || []), ...modules])];
|
|
222
|
+
if (!modules.includes("core")) modules.unshift("core");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let synced = { cbm: true };
|
|
226
|
+
if (!opts["machine-only"]) {
|
|
227
|
+
if (!fs.existsSync(path.join(root, ".git"))) console.log(" note: not a git repo root; ccwf works anyway, but `commit`/`orchestrate-workers` expect git");
|
|
228
|
+
const r = applyProject({ root, agents, modules, ejected, files: prev?.files, dryRun: opts["dry-run"], force: opts.force });
|
|
229
|
+
synced = r.synced;
|
|
230
|
+
report(r.ins, opts["dry-run"]);
|
|
231
|
+
if (!opts["dry-run"]) saveManifest(root, { agents, modules, ejected, files: r.ins.files });
|
|
232
|
+
}
|
|
233
|
+
if (!opts["project-only"] && cmd === "init") await machineSetup({ agents, modules, root, opts });
|
|
234
|
+
if (!opts["machine-only"]) projectNextSteps(synced);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function runSync(opts, root) {
|
|
238
|
+
const prev = loadManifest(root);
|
|
239
|
+
if (!prev) throw new Error(`no ${MANIFEST} here; run \`ccwf init\` first`);
|
|
240
|
+
// Settings and the navigation block only. Re-rendering templates is update's job.
|
|
241
|
+
const ins = new Installer({ root, dryRun: opts["dry-run"], force: false, files: prev.files, ejected: prev.ejected || [] });
|
|
242
|
+
const agents = prev.agents;
|
|
243
|
+
const synced = sync({ ins, agents, params: makeParams({ agents }) }, prev.modules);
|
|
244
|
+
report(ins, opts["dry-run"]);
|
|
245
|
+
if (!opts["dry-run"]) saveManifest(root, { ...prev, files: ins.files }, prev.ccwf);
|
|
246
|
+
console.log(`allowed sites: ${synced.domains}${synced.cbm ? " · codebase-memory rules: on" : ""}`);
|
|
247
|
+
projectNextSteps(synced);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function eject(opts, root) {
|
|
251
|
+
const name = opts._[1];
|
|
252
|
+
const prev = loadManifest(root);
|
|
253
|
+
if (!prev) throw new Error(`no ${MANIFEST} here; run \`ccwf init\` first`);
|
|
254
|
+
const dir = `.agents/skills/${name}`;
|
|
255
|
+
if (!name || !fs.existsSync(path.join(root, dir))) throw new Error(`usage: ccwf eject <skill> (no ${dir})`);
|
|
256
|
+
const ejected = [...new Set([...(prev.ejected || []), dir])];
|
|
257
|
+
const files = Object.fromEntries(Object.entries(prev.files).filter(([rel]) => !rel.startsWith(dir + "/")));
|
|
258
|
+
saveManifest(root, { ...prev, ejected, files });
|
|
259
|
+
console.log(`ejected ${name}: ccwf no longer touches ${dir}/`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function status(root) {
|
|
263
|
+
const m = loadManifest(root);
|
|
264
|
+
if (!m) return console.log(`not initialised (no ${MANIFEST}); run \`ccwf init\``);
|
|
265
|
+
console.log(`ccwf ${m.ccwf} (installed) / ${pkg.version} (this package)`);
|
|
266
|
+
console.log(`agents: ${m.agents.join(", ")}\nmodules: ${m.modules.join(", ")}`);
|
|
267
|
+
if (m.ejected?.length) console.log(`ejected: ${m.ejected.join(", ")}`);
|
|
268
|
+
let drift = 0;
|
|
269
|
+
for (const [rel, hash] of Object.entries(m.files)) {
|
|
270
|
+
let cur = null;
|
|
271
|
+
try {
|
|
272
|
+
cur = fs.readFileSync(path.join(root, rel), "utf8");
|
|
273
|
+
} catch {}
|
|
274
|
+
const state = cur === null ? "missing" : sha(cur) === hash ? null : "modified";
|
|
275
|
+
if (state) {
|
|
276
|
+
drift++;
|
|
277
|
+
console.log(` ${state}: ${rel}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
console.log(drift ? `${drift} file(s) differ from what ccwf wrote` : "all ccwf-owned files match");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function doctor(opts, root) {
|
|
284
|
+
const m = loadManifest(root);
|
|
285
|
+
if (m) status(root);
|
|
286
|
+
else console.log(`(no ${MANIFEST} in ${root}: checking the machine only)`);
|
|
287
|
+
console.log("");
|
|
288
|
+
const agents = m?.agents || AGENTS;
|
|
289
|
+
const modules = m?.modules || ORDER;
|
|
290
|
+
await machineSetup({ agents, modules, root: m ? root : null, opts, fixOnly: true });
|
|
291
|
+
if (m) projectNextSteps(applyProject({ root, agents, modules, ejected: m.ejected || [], files: m.files, dryRun: true, force: false }).synced);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function main(argv = process.argv.slice(2)) {
|
|
295
|
+
const opts = parseArgs(argv);
|
|
296
|
+
const cmd = opts._[0];
|
|
297
|
+
if (opts.version) return console.log(pkg.version);
|
|
298
|
+
if (opts.help || !cmd || cmd === "help") return console.log(HELP);
|
|
299
|
+
const root = path.resolve(opts.dir || process.cwd());
|
|
300
|
+
if (cmd === "status") return status(root);
|
|
301
|
+
if (cmd === "doctor") return doctor(opts, root);
|
|
302
|
+
if (cmd === "sync") return runSync(opts, root);
|
|
303
|
+
if (cmd === "eject") return eject(opts, root);
|
|
304
|
+
if (cmd === "init" || cmd === "update") return install(cmd, opts, root);
|
|
305
|
+
if (cmd === "add") {
|
|
306
|
+
if (!opts._[1]) throw new Error("usage: ccwf add <module>");
|
|
307
|
+
return install("add", { ...opts, modules: opts._.slice(1).join(",") }, root);
|
|
308
|
+
}
|
|
309
|
+
throw new Error(`unknown command: ${cmd}\n\n${HELP}`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
module.exports = { main, parseArgs };
|
package/lib/installer.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const crypto = require("crypto");
|
|
5
|
+
const { upsertBlock, mergeJson, hasTableOutsideBlock, missingMarker } = require("./merge");
|
|
6
|
+
|
|
7
|
+
const sha = (s) => crypto.createHash("sha256").update(s).digest("hex");
|
|
8
|
+
|
|
9
|
+
// Applies file operations under `root`, records what it did, and never clobbers user edits.
|
|
10
|
+
class Installer {
|
|
11
|
+
constructor({ root, dryRun = false, force = false, files = {}, ejected = [] }) {
|
|
12
|
+
this.root = root;
|
|
13
|
+
this.ejected = ejected; // path prefixes the user took over (ccwf eject)
|
|
14
|
+
this.dryRun = dryRun;
|
|
15
|
+
this.force = force;
|
|
16
|
+
this.files = { ...files }; // rel -> sha256 of the content ccwf last wrote
|
|
17
|
+
this.log = []; // { rel, state: created|updated|unchanged|skipped }
|
|
18
|
+
this.warnings = [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
abs(rel) {
|
|
22
|
+
return path.join(this.root, rel);
|
|
23
|
+
}
|
|
24
|
+
read(rel) {
|
|
25
|
+
try {
|
|
26
|
+
return fs.readFileSync(this.abs(rel), "utf8");
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
note(rel, state) {
|
|
32
|
+
this.log.push({ rel, state });
|
|
33
|
+
}
|
|
34
|
+
warn(msg) {
|
|
35
|
+
this.warnings.push(msg);
|
|
36
|
+
}
|
|
37
|
+
write(rel, content, mode) {
|
|
38
|
+
if (this.dryRun) return;
|
|
39
|
+
fs.mkdirSync(path.dirname(this.abs(rel)), { recursive: true });
|
|
40
|
+
fs.writeFileSync(this.abs(rel), content, mode ? { mode } : undefined);
|
|
41
|
+
if (mode) fs.chmodSync(this.abs(rel), mode);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// A file ccwf owns: replaced on update only while it still matches what ccwf wrote.
|
|
45
|
+
owned(rel, content, mode) {
|
|
46
|
+
if (this.ejected.some((p) => rel === p || rel.startsWith(p + "/"))) return this.note(rel, "unchanged");
|
|
47
|
+
const cur = this.read(rel);
|
|
48
|
+
if (cur === content) {
|
|
49
|
+
this.files[rel] = sha(content);
|
|
50
|
+
return this.note(rel, "unchanged");
|
|
51
|
+
}
|
|
52
|
+
if (cur !== null && !this.force && this.files[rel] !== sha(cur)) {
|
|
53
|
+
this.warn(`${rel}: modified locally, left as is (use --force to overwrite)`);
|
|
54
|
+
return this.note(rel, "skipped");
|
|
55
|
+
}
|
|
56
|
+
this.write(rel, content, mode);
|
|
57
|
+
this.files[rel] = sha(content);
|
|
58
|
+
this.note(rel, cur === null ? "created" : "updated");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// A file the user will edit: written once, never touched again. --force does not apply.
|
|
62
|
+
seed(rel, content, mode) {
|
|
63
|
+
if (this.read(rel) !== null) return this.note(rel, "unchanged");
|
|
64
|
+
this.write(rel, content, mode);
|
|
65
|
+
this.note(rel, "created");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Managed section inside a shared text file (AGENTS.md, .gitignore, config.toml).
|
|
69
|
+
block(rel, id, body, style = "md") {
|
|
70
|
+
const raw = this.read(rel);
|
|
71
|
+
const cur = raw ?? "";
|
|
72
|
+
const missing = missingMarker(cur, id, style);
|
|
73
|
+
if (missing) {
|
|
74
|
+
this.warn(`${rel}: managed block ${id} has no ${missing} marker, skipped`);
|
|
75
|
+
return this.note(rel, "skipped");
|
|
76
|
+
}
|
|
77
|
+
const next = upsertBlock(cur, id, body, style);
|
|
78
|
+
if (next === cur) return this.note(rel, "unchanged");
|
|
79
|
+
this.write(rel, next);
|
|
80
|
+
this.note(rel, raw === null ? "created" : "updated");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Codex TOML: a managed block, unless the user already defines the same table by hand.
|
|
84
|
+
tomlBlock(rel, id, body, tables) {
|
|
85
|
+
const cur = this.read(rel) ?? "";
|
|
86
|
+
const clash = tables.find((t) => hasTableOutsideBlock(cur, id, t));
|
|
87
|
+
if (clash) {
|
|
88
|
+
this.warn(`${rel}: [${clash}] already defined by hand, skipped ${id}`);
|
|
89
|
+
return this.note(rel, "skipped");
|
|
90
|
+
}
|
|
91
|
+
this.block(rel, id, body, "hash");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Deep-merge keys into a JSON file; invalid JSON aborts without touching the file.
|
|
95
|
+
json(rel, patch, union = []) {
|
|
96
|
+
const cur = this.read(rel) ?? "";
|
|
97
|
+
const next = mergeJson(cur, patch, rel, union);
|
|
98
|
+
if (next === cur) return this.note(rel, "unchanged");
|
|
99
|
+
const existed = cur !== "";
|
|
100
|
+
this.write(rel, next);
|
|
101
|
+
this.note(rel, existed ? "updated" : "created");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Relative symlink. A failure is an error for this skill; the files are not copied.
|
|
105
|
+
link(rel, target) {
|
|
106
|
+
let cur = null;
|
|
107
|
+
try {
|
|
108
|
+
cur = fs.readlinkSync(this.abs(rel));
|
|
109
|
+
} catch {}
|
|
110
|
+
if (cur === target) return this.note(rel, "unchanged");
|
|
111
|
+
if (fs.existsSync(this.abs(rel)) || cur !== null) {
|
|
112
|
+
this.warn(`${rel}: already exists and is not the expected link, left as is`);
|
|
113
|
+
return this.note(rel, "skipped");
|
|
114
|
+
}
|
|
115
|
+
if (!this.dryRun) {
|
|
116
|
+
fs.mkdirSync(path.dirname(this.abs(rel)), { recursive: true });
|
|
117
|
+
try {
|
|
118
|
+
fs.symlinkSync(target, this.abs(rel), "dir");
|
|
119
|
+
} catch (err) {
|
|
120
|
+
throw new Error(`${rel}: could not create symlink (${err.code || err.message})`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
this.note(rel, "created");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = { Installer, sha };
|