yadflow 2.0.1 → 2.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/CHANGELOG.md +8 -2
- package/README.md +27 -0
- package/bin/yad.mjs +11 -1
- package/cli/doctor.mjs +146 -0
- package/cli/epic-state.mjs +2 -1
- package/cli/errors.mjs +25 -0
- package/cli/gate.mjs +13 -7
- package/cli/lib.mjs +2 -1
- package/cli/reconcile.mjs +1 -1
- package/cli/setup.mjs +2 -2
- package/package.json +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
# [2.1.0](https://github.com/abdelrahmannasr/yadflow/compare/v2.0.1...v2.1.0) (2026-06-13)
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
### Bug Fixes
|
|
5
5
|
|
|
6
|
-
*
|
|
6
|
+
* address CodeRabbit review on the hardening PR ([7dbe9e3](https://github.com/abdelrahmannasr/yadflow/commit/7dbe9e358e731d69ecead6ecac9faa8377c37023))
|
|
7
|
+
* drop useless backtick escapes in a single-quoted doctor hint (lint) ([c0cf1a2](https://github.com/abdelrahmannasr/yadflow/commit/c0cf1a26c15fccc91df15013d6bac83892f2af25))
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* yad doctor + structured YAD-* error codes with recovery hints ([94f9e9f](https://github.com/abdelrahmannasr/yadflow/commit/94f9e9f6ff6d6d3c83ed29f1cfcc97e32678615c))
|
|
7
13
|
|
|
8
14
|
# [1.1.0](https://github.com/abdelrahmannasr/sdlc-workflow/compare/v1.0.3...v1.1.0) (2026-06-09)
|
|
9
15
|
|
package/README.md
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/yadflow)
|
|
4
4
|
[](https://github.com/abdelrahmannasr/yadflow/actions/workflows/ci.yml)
|
|
5
5
|
[](https://docs.npmjs.com/generating-provenance-statements)
|
|
6
|
+
[](https://github.com/abdelrahmannasr/yadflow/blob/main/package.json)
|
|
7
|
+
[](https://github.com/abdelrahmannasr/yadflow/blob/main/SECURITY.md)
|
|
6
8
|
[](https://abdelrahmannasr.github.io/yadflow/)
|
|
7
9
|
|
|
8
10
|
> 📖 **Start here: the [Yadflow Terminology & Workflow Structure Report](https://abdelrahmannasr.github.io/yadflow/)** —
|
|
@@ -70,12 +72,18 @@ The module ships a zero-dependency CLI, published to npm as
|
|
|
70
72
|
[`yadflow`](https://www.npmjs.com/package/yadflow). Run it
|
|
71
73
|
with `npx` from your **product hub** repo — no clone needed.
|
|
72
74
|
|
|
75
|
+
> **Platform support.** Linux and macOS are first-class — the test suite, the bash check gates, and
|
|
76
|
+
> the end-to-end harness all run on both in CI. The CLI shells out to `git` (and the bash gate
|
|
77
|
+
> scripts), so on **Windows use [WSL](https://learn.microsoft.com/windows/wsl/)**; native PowerShell
|
|
78
|
+
> is not yet supported. Requires **Node.js ≥ 18**.
|
|
79
|
+
|
|
73
80
|
| Command | What it does |
|
|
74
81
|
|---------|--------------|
|
|
75
82
|
| `npx yadflow setup` | Guided first-run wizard (the steps below). |
|
|
76
83
|
| `npx yadflow check` | Read-only report: what is **missing** / **outdated** (drifted) / **stale** (code-context) / **legacy** (pre-2.0 `sdlc-*` names) vs the bundled manifest. |
|
|
77
84
|
| `npx yadflow check --fix` | Reconcile: fill what is missing **and** update what changed — touches nothing already correct. |
|
|
78
85
|
| `npx yadflow update` | Apply drift only (alias for `check --fix --scope=changed`). Also migrates a pre-2.0 install in place: `sdlc-*` skill copies and marker-owned `sdlc-*.yml` CI files are replaced by their `yad-*` names (a same-named file *you* authored is never touched). |
|
|
86
|
+
| `npx yadflow doctor [--json]` | Environment + state health: tools on PATH and platform auth, config files parse and point at real repos, every epic ledger loads. Exit 1 on any failure; `--json` for CI and bug reports. |
|
|
79
87
|
| `yad gate open <epic> <artifact>` | Open the front-half **review PR/MR** for an artifact and mark the step `in_review`. |
|
|
80
88
|
| `yad gate sync <epic> [artifact]` | Pull the PR/MR's reviews + comment threads into the file ledger; **auto-advance** the step when approvals are satisfied, all threads are resolved, and the PR is merged. |
|
|
81
89
|
| `yad gate comments <epic> [artifact]` | Fetch the unresolved review comments to address (then reply on the PR; reviewers resolve their threads). |
|
|
@@ -143,6 +151,25 @@ provenance). See [`RELEASING.md`](RELEASING.md).
|
|
|
143
151
|
> ships the `CHANGELOG.md` in the tarball, and cuts a GitHub release. No manual `npm publish`. See
|
|
144
152
|
> [`RELEASING.md`](RELEASING.md).
|
|
145
153
|
|
|
154
|
+
### Troubleshooting (`yad doctor` + error codes)
|
|
155
|
+
|
|
156
|
+
When something is off, run `yad doctor` first — it checks the environment (git, gh/glab auth, node
|
|
157
|
+
version), the project state (`.sdlc/*.json` parse and point at real repos), and every epic ledger,
|
|
158
|
+
with a fix-it hint per finding. Failures carry stable, greppable codes, also printed by any failing
|
|
159
|
+
`yad` command:
|
|
160
|
+
|
|
161
|
+
| Code | Meaning | Fix |
|
|
162
|
+
|------|---------|-----|
|
|
163
|
+
| `YAD-ENV-001` | git is not installed or not on PATH | install git — every yad command needs it |
|
|
164
|
+
| `YAD-ENV-002` | platform CLI (gh/glab) missing or not authenticated | install it and authenticate — `gh auth login` (GitHub) or `glab auth login` (GitLab); the gate degrades to file-only without it |
|
|
165
|
+
| `YAD-ENV-003` | Node.js older than the supported range | install Node >= 18 |
|
|
166
|
+
| `YAD-STATE-001` | a ledger/config JSON file exists but does not parse | fix the file or restore from git — never delete a ledger blindly |
|
|
167
|
+
| `YAD-STATE-002` | a ledger/config file parses but has the wrong shape | fix the file or restore from git (the message names the field) |
|
|
168
|
+
| `YAD-STATE-003` | a registered repo path is missing or not a git repo | fix the path in `.sdlc/repos.json` or re-connect the repo |
|
|
169
|
+
| `YAD-CFG-001` | `hub.json` names an unknown platform | expected `github`, `gitlab`, or `null` — fix it or re-run `yad setup` |
|
|
170
|
+
|
|
171
|
+
Filing a bug? Attach `yad doctor --json` — it contains no secrets (names, paths, and check results only).
|
|
172
|
+
|
|
146
173
|
## Agent skills (all 17)
|
|
147
174
|
|
|
148
175
|
The CLI **installs and wires** the module; the skills below are the **agents you invoke by name** in your
|
package/bin/yad.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { isValidEpicId } from '../cli/epic-state.mjs';
|
|
|
9
9
|
import { runCommit } from '../cli/commit.mjs';
|
|
10
10
|
import { runOpenPr } from '../cli/openpr.mjs';
|
|
11
11
|
import { runRepo } from '../cli/repo.mjs';
|
|
12
|
+
import { runDoctor } from '../cli/doctor.mjs';
|
|
12
13
|
|
|
13
14
|
const HELP = `${c.bold('yad')} — setup, review-gate & build helpers for the SDLC Workflow module ${c.dim('v' + VERSION)}
|
|
14
15
|
|
|
@@ -18,6 +19,8 @@ ${c.bold('Setup & maintenance')}
|
|
|
18
19
|
yad check --fix Reconcile: fill what is missing, update what changed
|
|
19
20
|
yad update Apply drift only (alias for: check --fix --scope=changed);
|
|
20
21
|
also migrates pre-2.0 sdlc-* installs to the yad-* names
|
|
22
|
+
yad doctor [--json] Environment + state health: tools/auth, config files,
|
|
23
|
+
repo paths, epic ledgers (exit 1 on any failure)
|
|
21
24
|
|
|
22
25
|
${c.bold('Review gate (front half)')}
|
|
23
26
|
yad gate open <epic> <artifact> Open the review PR/MR; mark the step in_review
|
|
@@ -62,6 +65,7 @@ function parseArgs(argv) {
|
|
|
62
65
|
else if (a === '--contract-change') o.contractChange = true;
|
|
63
66
|
else if (a === '--no-push') o.noPush = true;
|
|
64
67
|
else if (a === '--dry-run') o.dryRun = true;
|
|
68
|
+
else if (a === '--json') o.json = true;
|
|
65
69
|
else if (a === '-h' || a === '--help') o.help = true;
|
|
66
70
|
else if (a === '-v' || a === '--version') o.version = true;
|
|
67
71
|
else if (a.startsWith('--scope=')) o.scope = a.slice('--scope='.length);
|
|
@@ -96,6 +100,9 @@ async function main() {
|
|
|
96
100
|
case 'update':
|
|
97
101
|
await reconcile(o.dir, { fix: true, scope: 'changed', force: o.force, today });
|
|
98
102
|
break;
|
|
103
|
+
case 'doctor':
|
|
104
|
+
await runDoctor(o.dir, { json: o.json });
|
|
105
|
+
break;
|
|
99
106
|
case 'gate': {
|
|
100
107
|
const [, action, epic, artifact] = o._;
|
|
101
108
|
// `gate ci` takes no positionals — epic/artifact come from --branch (or a sweep of all PRs).
|
|
@@ -130,7 +137,10 @@ async function main() {
|
|
|
130
137
|
|
|
131
138
|
main()
|
|
132
139
|
.catch((err) => {
|
|
133
|
-
|
|
140
|
+
const code = err?.code && /^YAD-/.test(err.code) ? ` [${err.code}]` : '';
|
|
141
|
+
log(c.red(`\nyad failed${code}: ${err?.message || err}`));
|
|
142
|
+
if (err?.hint) log(c.yellow(` → ${err.hint}`));
|
|
143
|
+
if (code) log(c.dim(' (see README "Troubleshooting" for this code, or run `yad doctor`)'));
|
|
134
144
|
process.exitCode = 1;
|
|
135
145
|
})
|
|
136
146
|
.finally(closePrompts);
|
package/cli/doctor.mjs
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// `yad doctor` — environment + state health, the complement of `yad check` (file drift).
|
|
2
|
+
// Three sections: environment (tools on PATH, auth), project state (config files parse and
|
|
3
|
+
// point at real repos), epics (each ledger loads). Pure reporting: exit 1 on any FAIL,
|
|
4
|
+
// 0 with warnings. `--json` emits the checks for CI / bug reports.
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import { c, log, ok, info, warn, fail, hand, run, has, exists, readJSON, readJSONStrict } from './lib.mjs';
|
|
8
|
+
import { VERSION, PROJECT_FILES } from './manifest.mjs';
|
|
9
|
+
import { loadLedger, epicRoot } from './epic-state.mjs';
|
|
10
|
+
import { gitHead } from './setup.mjs';
|
|
11
|
+
import { cliFor } from './platform.mjs';
|
|
12
|
+
|
|
13
|
+
const MIN_NODE = 18;
|
|
14
|
+
|
|
15
|
+
// Each check: { id, section, status: 'ok'|'warn'|'fail', message, hint? }
|
|
16
|
+
function check(checks, id, section, status, message, hint = '') {
|
|
17
|
+
checks.push({ id, section, status, message, ...(hint ? { hint } : {}) });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function envChecks(checks) {
|
|
21
|
+
const major = Number(process.versions.node.split('.')[0]);
|
|
22
|
+
if (major >= MIN_NODE) check(checks, 'node', 'environment', 'ok', `node ${process.versions.node}`);
|
|
23
|
+
else check(checks, 'node', 'environment', 'fail', `node ${process.versions.node} is below the supported range [YAD-ENV-003]`, `install Node.js >= ${MIN_NODE}`);
|
|
24
|
+
|
|
25
|
+
if (has('git')) check(checks, 'git', 'environment', 'ok', 'git present');
|
|
26
|
+
else check(checks, 'git', 'environment', 'fail', 'git not found on PATH [YAD-ENV-001]', 'install git — every yad command needs it');
|
|
27
|
+
|
|
28
|
+
for (const tool of ['npx', 'bash']) {
|
|
29
|
+
if (has(tool)) check(checks, tool, 'environment', 'ok', `${tool} present`);
|
|
30
|
+
else check(checks, tool, 'environment', 'warn', `${tool} not found on PATH`, tool === 'npx' ? 'repomix packing will be skipped' : 'the check gates are bash scripts');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function projectChecks(checks, root) {
|
|
35
|
+
const hubPath = path.join(root, PROJECT_FILES.hubConfig);
|
|
36
|
+
const regPath = path.join(root, PROJECT_FILES.reposRegistry);
|
|
37
|
+
const verPath = path.join(root, PROJECT_FILES.version);
|
|
38
|
+
if (!exists(hubPath) && !exists(regPath) && !exists(verPath)) {
|
|
39
|
+
check(checks, 'project', 'project', 'warn', 'no yad project here (.sdlc/ not initialised)', 'run `yad setup` to start one — environment checks above still apply');
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// version stamp
|
|
44
|
+
const ver = readJSON(verPath, null);
|
|
45
|
+
if (!ver) check(checks, 'cli-version', 'project', 'warn', `${PROJECT_FILES.version} missing or unreadable`, 'run `yad check --fix`');
|
|
46
|
+
else if (ver.version !== VERSION) check(checks, 'cli-version', 'project', 'warn', `project stamped v${ver.version}, CLI is v${VERSION}`, 'run `yad update` to reconcile');
|
|
47
|
+
else check(checks, 'cli-version', 'project', 'ok', `version stamp matches (v${VERSION})`);
|
|
48
|
+
|
|
49
|
+
// hub.json: parse + shape
|
|
50
|
+
let hub = null;
|
|
51
|
+
if (!exists(hubPath)) {
|
|
52
|
+
check(checks, 'hub', 'project', 'warn', `${PROJECT_FILES.hubConfig} absent — file-only gate`, 'run `yad setup` to configure a platform + roster');
|
|
53
|
+
} else {
|
|
54
|
+
let hubBroken = false;
|
|
55
|
+
try {
|
|
56
|
+
hub = readJSONStrict(hubPath, null);
|
|
57
|
+
} catch (e) {
|
|
58
|
+
hubBroken = true;
|
|
59
|
+
check(checks, 'hub', 'project', 'fail', `${PROJECT_FILES.hubConfig} does not parse [${e.code || 'YAD-STATE-001'}]`, e.hint || 'fix the JSON or restore it from git');
|
|
60
|
+
}
|
|
61
|
+
if (hubBroken) { /* reported above */ }
|
|
62
|
+
else if (typeof hub !== 'object' || Array.isArray(hub) || hub === null) check(checks, 'hub', 'project', 'fail', `${PROJECT_FILES.hubConfig} has the wrong shape [YAD-STATE-002]`, 'expected a JSON object');
|
|
63
|
+
else if (![null, undefined, 'github', 'gitlab'].includes(hub.platform)) check(checks, 'hub', 'project', 'fail', `${PROJECT_FILES.hubConfig}: unknown platform '${hub.platform}' [YAD-CFG-001]`, 'expected github, gitlab, or null');
|
|
64
|
+
// Mirror gate.mjs's roster shape check so doctor never reports "ok" on a hub the gate would reject.
|
|
65
|
+
else if (hub.roster !== undefined && !Array.isArray(hub.roster)) check(checks, 'hub', 'project', 'fail', `${PROJECT_FILES.hubConfig}: \`roster\` must be an array [YAD-STATE-002]`, 'fix the file or re-run `yad setup`');
|
|
66
|
+
else {
|
|
67
|
+
check(checks, 'hub', 'project', 'ok', `hub: ${hub.platform || 'file-only'}, ${(hub.roster || []).length} reviewer(s)`);
|
|
68
|
+
// platform CLI + auth (best-effort; auth probing is the user's own session)
|
|
69
|
+
const cli = cliFor(hub.platform);
|
|
70
|
+
if (cli) {
|
|
71
|
+
if (!has(cli)) check(checks, 'platform-cli', 'project', 'warn', `${cli} not found on PATH [YAD-ENV-002]`, `install ${cli} — the gate degrades to file-only without it`);
|
|
72
|
+
else if (!run(cli, ['auth', 'status']).ok) check(checks, 'platform-cli', 'project', 'warn', `${cli} present but not authenticated [YAD-ENV-002]`, `run \`${cli} auth login\``);
|
|
73
|
+
else check(checks, 'platform-cli', 'project', 'ok', `${cli} present and authenticated`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// repos.json: parse + every entry is a live git repo; staleness vs syncedHead
|
|
79
|
+
let registry = { repos: [] };
|
|
80
|
+
let regBroken = false;
|
|
81
|
+
try {
|
|
82
|
+
registry = readJSONStrict(regPath, { repos: [] });
|
|
83
|
+
} catch (e) {
|
|
84
|
+
regBroken = true;
|
|
85
|
+
check(checks, 'repos', 'project', 'fail', `${PROJECT_FILES.reposRegistry} does not parse [${e.code || 'YAD-STATE-001'}]`, e.hint || 'fix the JSON or restore it from git');
|
|
86
|
+
}
|
|
87
|
+
if (regBroken) { /* reported above */ }
|
|
88
|
+
else if (!Array.isArray(registry?.repos)) check(checks, 'repos', 'project', 'fail', `${PROJECT_FILES.reposRegistry} has the wrong shape [YAD-STATE-002]`, 'expected a `repos` array');
|
|
89
|
+
else {
|
|
90
|
+
for (const repo of registry.repos) {
|
|
91
|
+
// A missing/empty path must NOT fall back to the project root (which is itself a git repo and
|
|
92
|
+
// would read as "healthy") — an entry with no path is malformed.
|
|
93
|
+
if (!repo.path) { check(checks, `repo:${repo.name || '(unnamed)'}`, 'project', 'fail', `${repo.name || '(unnamed)'}: no \`path\` in repos.json [YAD-STATE-003]`, 're-connect the repo (`yad setup`)'); continue; }
|
|
94
|
+
const repoRoot = path.resolve(root, repo.path);
|
|
95
|
+
if (!exists(repoRoot)) { check(checks, `repo:${repo.name}`, 'project', 'fail', `${repo.name}: path ${repo.path} does not exist [YAD-STATE-003]`, 'fix the path in repos.json or re-connect the repo'); continue; }
|
|
96
|
+
const head = gitHead(repoRoot);
|
|
97
|
+
if (!head) { check(checks, `repo:${repo.name}`, 'project', 'fail', `${repo.name}: ${repo.path} is not a git repository (or has no commits) [YAD-STATE-003]`, 'init/clone the repo, then re-connect it'); continue; }
|
|
98
|
+
if (repo.syncedHead && head !== repo.syncedHead) check(checks, `repo:${repo.name}`, 'project', 'warn', `${repo.name}: code-context is stale (HEAD moved since last pack)`, 'run `yad repo refresh ' + repo.name + '`');
|
|
99
|
+
else check(checks, `repo:${repo.name}`, 'project', 'ok', `${repo.name}: git repo, context fresh`);
|
|
100
|
+
}
|
|
101
|
+
if (!registry.repos.length) check(checks, 'repos', 'project', 'warn', 'no code repos registered', 'run `yad setup` to connect one');
|
|
102
|
+
}
|
|
103
|
+
return { hub, registry };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function epicChecks(checks, root) {
|
|
107
|
+
const epicsDir = path.join(root, 'epics');
|
|
108
|
+
if (!exists(epicsDir)) return;
|
|
109
|
+
for (const e of fs.readdirSync(epicsDir).sort()) {
|
|
110
|
+
if (!fs.statSync(path.join(epicsDir, e)).isDirectory()) continue;
|
|
111
|
+
try {
|
|
112
|
+
const ledger = loadLedger(epicRoot(root, e));
|
|
113
|
+
if (!ledger.state) check(checks, `epic:${e}`, 'epics', 'warn', `${e}: no state.json — epic not seeded`, 'author it via yad-epic, or remove the directory');
|
|
114
|
+
else check(checks, `epic:${e}`, 'epics', 'ok', `${e}: currentStep ${ledger.state.currentStep}`);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
check(checks, `epic:${e}`, 'epics', 'fail', `${e}: ${err.message} [${err.code || 'YAD-STATE-001'}]`, err.hint || 'fix the file or restore it from git');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function runDoctor(root, { json = false } = {}) {
|
|
122
|
+
const checks = [];
|
|
123
|
+
envChecks(checks);
|
|
124
|
+
projectChecks(checks, root);
|
|
125
|
+
epicChecks(checks, root);
|
|
126
|
+
|
|
127
|
+
const failed = checks.filter((x) => x.status === 'fail');
|
|
128
|
+
const warned = checks.filter((x) => x.status === 'warn');
|
|
129
|
+
if (json) {
|
|
130
|
+
log(JSON.stringify({ version: VERSION, ok: failed.length === 0, checks }, null, 2));
|
|
131
|
+
} else {
|
|
132
|
+
log(c.bold(`\nyad doctor ${c.dim('v' + VERSION)}`));
|
|
133
|
+
let section = '';
|
|
134
|
+
for (const x of checks) {
|
|
135
|
+
if (x.section !== section) { section = x.section; log(`\n ${c.bold(section)}`); }
|
|
136
|
+
({ ok, warn, fail })[x.status](x.message);
|
|
137
|
+
if (x.hint && x.status !== 'ok') hand(x.hint);
|
|
138
|
+
}
|
|
139
|
+
log('');
|
|
140
|
+
if (failed.length) fail(`${failed.length} problem(s) found`);
|
|
141
|
+
else if (warned.length) info(`healthy with ${warned.length} warning(s)`);
|
|
142
|
+
else ok('all clear');
|
|
143
|
+
}
|
|
144
|
+
if (failed.length) process.exitCode = 1;
|
|
145
|
+
return { ok: failed.length === 0, failed: failed.length, warned: warned.length, checks };
|
|
146
|
+
}
|
package/cli/epic-state.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
import { createHash } from 'node:crypto';
|
|
6
6
|
import fs from 'node:fs';
|
|
7
7
|
import { readJSONStrict, writeJSON, fileSha } from './lib.mjs';
|
|
8
|
+
import { err } from './errors.mjs';
|
|
8
9
|
import { epicFiles } from './manifest.mjs';
|
|
9
10
|
|
|
10
11
|
const RISK_ESCALATORS = ['contract', 'auth', 'payments'];
|
|
@@ -98,7 +99,7 @@ export function artifactHash(epicDir, artifact) {
|
|
|
98
99
|
|
|
99
100
|
// Shape checks for the ledger files. Fail fast with the exact file named — a wrong-shape ledger
|
|
100
101
|
// silently treated as a default would be rewritten by the next sync, destroying the real data.
|
|
101
|
-
const badShape = (file, what) =>
|
|
102
|
+
const badShape = (file, what) => err('YAD-STATE-002', `${file}: ${what}`, 'fix the file or restore it from git');
|
|
102
103
|
function requireArray(v, file) {
|
|
103
104
|
if (!Array.isArray(v)) throw badShape(file, 'expected a JSON array');
|
|
104
105
|
return v;
|
package/cli/errors.mjs
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Structured error codes for the `yad` CLI. A YadError carries a stable code (greppable,
|
|
2
|
+
// documented in README "Troubleshooting") and a one-line recovery hint that the top-level
|
|
3
|
+
// catch in bin/yad.mjs prints after the message. Plain Errors still work everywhere; codes
|
|
4
|
+
// are reserved for the failures users actually hit and need to act on.
|
|
5
|
+
export class YadError extends Error {
|
|
6
|
+
constructor(code, message, hint = '') {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'YadError';
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.hint = hint;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// The catalog — single source for doctor, the top-level catch, and the README table.
|
|
15
|
+
export const CODES = {
|
|
16
|
+
'YAD-ENV-001': 'git is not installed or not on PATH',
|
|
17
|
+
'YAD-ENV-002': 'the platform CLI (gh/glab) is missing or not authenticated',
|
|
18
|
+
'YAD-ENV-003': 'Node.js is older than the supported range (>=18)',
|
|
19
|
+
'YAD-STATE-001': 'a ledger/config JSON file exists but does not parse',
|
|
20
|
+
'YAD-STATE-002': 'a ledger/config JSON file parses but has the wrong shape',
|
|
21
|
+
'YAD-STATE-003': 'a registered repo path is missing or not a git repository',
|
|
22
|
+
'YAD-CFG-001': 'hub.json names an unknown platform (expected github, gitlab, or null)',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const err = (code, message, hint) => new YadError(code, message, hint);
|
package/cli/gate.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
artifactPaths, upsertHubPr,
|
|
15
15
|
} from './epic-state.mjs';
|
|
16
16
|
import { readPr, mapApprovers, createPr } from './platform.mjs';
|
|
17
|
+
import { err } from './errors.mjs';
|
|
17
18
|
|
|
18
19
|
// ---- tiny frontmatter reader (key: value, and `repos: [a, b]`) ----------------------------------
|
|
19
20
|
function frontmatter(file) {
|
|
@@ -64,18 +65,23 @@ function warnUnlockedContract(epicDir, artifact) {
|
|
|
64
65
|
function loadHub(root) {
|
|
65
66
|
const hubFile = path.join(root, PROJECT_FILES.hubConfig);
|
|
66
67
|
const regFile = path.join(root, PROJECT_FILES.reposRegistry);
|
|
68
|
+
// Distinguish an ABSENT hub.json (null default → fine, file-only gate) from one that exists but
|
|
69
|
+
// holds literal `null` (malformed — must not silently downgrade to file-only).
|
|
67
70
|
const hub = readJSONStrict(hubFile, null);
|
|
71
|
+
if (hub === null && fs.existsSync(hubFile)) {
|
|
72
|
+
throw err('YAD-STATE-002', `${hubFile}: contains \`null\` — expected a config object`, 'fix the file or re-run `yad setup`');
|
|
73
|
+
}
|
|
68
74
|
if (hub !== null) {
|
|
69
|
-
if (typeof hub !== 'object' || Array.isArray(hub)) throw
|
|
75
|
+
if (typeof hub !== 'object' || Array.isArray(hub)) throw err('YAD-STATE-002', `${hubFile}: expected a JSON object`, 'fix the file or re-run `yad setup`');
|
|
70
76
|
if (![null, undefined, 'github', 'gitlab'].includes(hub.platform)) {
|
|
71
|
-
throw
|
|
77
|
+
throw err('YAD-CFG-001', `${hubFile}: unknown platform '${hub.platform}'`, 'expected github, gitlab, or null — fix the file or re-run `yad setup`');
|
|
72
78
|
}
|
|
73
79
|
if (hub.roster !== undefined && !Array.isArray(hub.roster)) {
|
|
74
|
-
throw
|
|
80
|
+
throw err('YAD-STATE-002', `${hubFile}: expected \`roster\` to be an array`, 'fix the file or re-run `yad setup`');
|
|
75
81
|
}
|
|
76
82
|
}
|
|
77
83
|
const registry = readJSONStrict(regFile, { repos: [] });
|
|
78
|
-
if (!Array.isArray(registry?.repos)) throw
|
|
84
|
+
if (!Array.isArray(registry?.repos)) throw err('YAD-STATE-002', `${regFile}: expected a \`repos\` array`, 'fix the file or re-run `yad setup`');
|
|
79
85
|
return { hub, repos: registry.repos };
|
|
80
86
|
}
|
|
81
87
|
|
|
@@ -131,7 +137,7 @@ function writeComments(epicDir, base, today, blocking) {
|
|
|
131
137
|
// Upsert machine-readable participation records into the comments ledger (the counterpart to the
|
|
132
138
|
// markdown side file) so the ledger — not just reviews/*.md — reflects platform thread state. One
|
|
133
139
|
// record per (step, commenter, round); `round` is the count of prior synced rounds for the step.
|
|
134
|
-
function recordComments(comments, { artifact, stepId, today, roster,
|
|
140
|
+
function recordComments(comments, { artifact, stepId, today, roster, blocking }) {
|
|
135
141
|
if (!blocking.length) return comments;
|
|
136
142
|
const byName = (login) => (roster.find((r) => r.login === login)?.name) || login || 'reviewer';
|
|
137
143
|
const roleOf = (login) => (roster.find((r) => r.login === login)?.role) || 'reviewer';
|
|
@@ -386,8 +392,8 @@ export async function gateStatus(root, { epic } = {}) {
|
|
|
386
392
|
}
|
|
387
393
|
}
|
|
388
394
|
|
|
389
|
-
export async function gateOpen(root, { epic, artifact
|
|
390
|
-
const { hub
|
|
395
|
+
export async function gateOpen(root, { epic, artifact } = {}) {
|
|
396
|
+
const { hub } = loadHub(root);
|
|
391
397
|
const epicDir = epicRoot(root, epic);
|
|
392
398
|
const ledger = loadLedger(epicDir);
|
|
393
399
|
if (!ledger.state) { fail(`no epic state at ${epicDir}`); process.exitCode = 1; return; }
|
package/cli/lib.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Shared helpers for the `yad` CLI. Node >=18 built-ins only — no dependencies.
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
+
import { err } from './errors.mjs';
|
|
3
4
|
import { spawnSync } from 'node:child_process';
|
|
4
5
|
import * as readline from 'node:readline/promises';
|
|
5
6
|
import { stdin as input, stdout as output } from 'node:process';
|
|
@@ -108,7 +109,7 @@ export function readJSONStrict(p, def = null) {
|
|
|
108
109
|
try {
|
|
109
110
|
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
110
111
|
} catch (e) {
|
|
111
|
-
throw
|
|
112
|
+
throw err('YAD-STATE-001', `corrupt JSON in ${p}: ${e.message}`, 'fix the file or restore it from git — never delete a ledger blindly');
|
|
112
113
|
}
|
|
113
114
|
}
|
|
114
115
|
// Atomic: serialize first, write a sibling tmp file (same dir = same filesystem),
|
package/cli/reconcile.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// manifest: missing setup, drifted files, stale code-context.
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import {
|
|
6
|
-
c, log, ok, info, warn, hand,
|
|
6
|
+
c, log, ok, info, warn, hand, readJSON, writeJSON, exists,
|
|
7
7
|
} from './lib.mjs';
|
|
8
8
|
import { VERSION, PROJECT_FILES } from './manifest.mjs';
|
|
9
9
|
import {
|
package/cli/setup.mjs
CHANGED
|
@@ -3,9 +3,9 @@ import path from 'node:path';
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import {
|
|
5
5
|
c, log, step, ok, info, warn, hand, fail, ask, askYesNo, run, has,
|
|
6
|
-
exists,
|
|
6
|
+
exists, readJSON, writeJSON,
|
|
7
7
|
} from './lib.mjs';
|
|
8
|
-
import { VERSION, IDE_FOLDER_TARGETS,
|
|
8
|
+
import { VERSION, IDE_FOLDER_TARGETS, PROJECT_FILES } from './manifest.mjs';
|
|
9
9
|
import { moduleActions, repoActions, hubActions, authorsActions } from './plan.mjs';
|
|
10
10
|
|
|
11
11
|
const ALL_IDES = [...IDE_FOLDER_TARGETS, '.opencode'];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yadflow",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Yadflow — the gated, team, multi-repo SDLC: author → review → build with a PR-driven review gate and a zero-dependency `yad` CLI (setup, gate, commit, open-pr, repo). A BMAD module + 17 yad-* skills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "AbdelRahman Nasr",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"bin/",
|
|
21
21
|
"cli/",
|
|
22
22
|
"!cli/test.mjs",
|
|
23
|
+
"!cli/test-checks.mjs",
|
|
23
24
|
"skills/",
|
|
24
25
|
"docs/index.html",
|
|
25
26
|
"README.md",
|
|
@@ -35,7 +36,10 @@
|
|
|
35
36
|
},
|
|
36
37
|
"scripts": {
|
|
37
38
|
"yad": "node bin/yad.mjs",
|
|
38
|
-
"
|
|
39
|
+
"lint": "eslint cli bin",
|
|
40
|
+
"test": "node --test cli/test.mjs cli/test-checks.mjs",
|
|
41
|
+
"test:e2e": "bash test/e2e/run.sh",
|
|
42
|
+
"coverage": "node --test --experimental-test-coverage --test-coverage-exclude='cli/test*.mjs' --test-coverage-lines=70 --test-coverage-branches=70 cli/test.mjs cli/test-checks.mjs",
|
|
39
43
|
"diagrams": "npx -y @mermaid-js/mermaid-cli -i docs/diagrams/sdlc-overview.mmd -o docs/diagrams/sdlc-overview.svg -b transparent && npx -y @mermaid-js/mermaid-cli -i docs/diagrams/review-loop.mmd -o docs/diagrams/review-loop.svg -b transparent",
|
|
40
44
|
"prepublishOnly": "npm test"
|
|
41
45
|
},
|
|
@@ -49,7 +53,9 @@
|
|
|
49
53
|
"spec-driven-development"
|
|
50
54
|
],
|
|
51
55
|
"devDependencies": {
|
|
56
|
+
"@eslint/js": "^9.39.4",
|
|
52
57
|
"@semantic-release/changelog": "^6.0.3",
|
|
58
|
+
"eslint": "^9.39.4",
|
|
53
59
|
"semantic-release": "^25.0.3"
|
|
54
60
|
}
|
|
55
61
|
}
|