yadflow 2.7.0 → 2.9.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 +2 -8
- package/README.md +8 -4
- package/bin/yad.mjs +15 -1
- package/cli/platform.mjs +1 -1
- package/cli/roster.mjs +164 -0
- package/cli/setup.mjs +128 -2
- package/package.json +3 -4
- package/skills/sdlc/module-help.csv +1 -1
- package/skills/yad-connect-repos/SKILL.md +4 -0
- package/skills/yad-connect-repos/references/hub-config.md +3 -1
- package/skills/yad-docs/references/data-mapping.md +22 -0
- package/skills/yad-docs/templates/app/index.html +1 -1
- package/skills/yad-docs/templates/app/public/yadflow-icon.png +0 -0
- package/skills/yad-docs/templates/app/src/App.tsx +0 -8
- package/skills/yad-docs/templates/app/src/components/Canvas/SystemComponent.tsx +2 -2
- package/skills/yad-docs/templates/app/src/components/DetailPanel/RightPanel.tsx +14 -13
- package/skills/yad-docs/templates/app/src/components/Navigation/TopNavBar.tsx +13 -29
- package/skills/yad-docs/templates/app/src/components/Reference/RulesLegendPanel.tsx +16 -11
- package/skills/yad-docs/templates/app/src/components/Sidebar/PathSelector.tsx +6 -3
- package/skills/yad-docs/templates/app/src/components/shared/Tooltip.tsx +3 -2
- package/skills/yad-docs-overview/SKILL.md +13 -11
- package/skills/yad-docs-overview/references/pipeline-model.md +1 -1
- package/skills/yad-hub-bridge/references/login-roster.md +1 -0
- package/docs/index.html +0 -1323
- package/skills/yad-docs/templates/app/src/components/Auth/LoginPage.tsx +0 -101
- package/skills/yad-docs/templates/app/src/store/useAuthStore.ts +0 -42
package/CHANGELOG.md
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
# [2.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
### Bug Fixes
|
|
5
|
-
|
|
6
|
-
* drop unused today param from runDocs (lint) ([1c255f1](https://github.com/abdelrahmannasr/yadflow/commit/1c255f1c848b9571502e29b142a07366612d638c))
|
|
7
|
-
* publish per-epic docs sites in CI + check shell-version staleness ([9862646](https://github.com/abdelrahmannasr/yadflow/commit/9862646fbe910de8ad8248a5f1bb586a5604b18f))
|
|
1
|
+
# [2.9.0](https://github.com/abdelrahmannasr/yadflow/compare/v2.8.0...v2.9.0) (2026-06-15)
|
|
8
2
|
|
|
9
3
|
|
|
10
4
|
### Features
|
|
11
5
|
|
|
12
|
-
*
|
|
6
|
+
* **docs:** enhance interactive docs — clearer diagram, brand icon, dimmed stubs ([#65](https://github.com/abdelrahmannasr/yadflow/issues/65)) ([969a20d](https://github.com/abdelrahmannasr/yadflow/commit/969a20dc1e9778e1ffd0962e557f3e2c28dfd6ef))
|
|
13
7
|
|
|
14
8
|
# [2.2.0](https://github.com/abdelrahmannasr/yadflow/compare/v2.1.0...v2.2.0) (2026-06-14)
|
|
15
9
|
|
package/README.md
CHANGED
|
@@ -90,6 +90,7 @@ with `npx` from your **product hub** repo — no clone needed.
|
|
|
90
90
|
| `npx yadflow check --fix` | Reconcile: fill what is missing **and** update what changed — touches nothing already correct. |
|
|
91
91
|
| `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). |
|
|
92
92
|
| `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. |
|
|
93
|
+
| `yad roster list` / `yad roster add <login>` | Manage the reviewer roster + per-repo roles **any time** (not just at setup). `add` upserts a member then walks each connected repo asking for their role; `grant`/`revoke <name> <repo> <role>` and `remove <login>` round it out. A `domain-owner` grant keeps `repos.json` `domain_owners` in sync. |
|
|
93
94
|
| `yad gate open <epic> <artifact>` | Open the front-half **review PR/MR** for an artifact and mark the step `in_review`. |
|
|
94
95
|
| `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. |
|
|
95
96
|
| `yad gate comments <epic> [artifact]` | Fetch the unresolved review comments to address (then reply on the PR; reviewers resolve their threads). |
|
|
@@ -136,6 +137,7 @@ simultaneous advancements can be lost; the next event or scheduled sweep re-sync
|
|
|
136
137
|
2. **Install the module** — copy all 29 `yad-*` skills into the IDE skill dirs you pick
|
|
137
138
|
(`.claude/`, `.agents/`, `.zencoder/`, `.opencode/`) and register `_bmad/sdlc/`.
|
|
138
139
|
3. **Hub platform & roster** — detect GitHub/GitLab from the remote; record reviewers → `.sdlc/hub.json`.
|
|
140
|
+
Edit the roster any time afterwards with `yad roster` (no need to re-run the whole wizard).
|
|
139
141
|
4. **Connect a design tool** — record the design tool (Figma / pencil / none) → `.sdlc/design.json` so
|
|
140
142
|
the UI step can materialize the design; the MCP itself is confirmed later by `yad-connect-design`.
|
|
141
143
|
5. **Connect a testing tool** — record the testing tool (Playwright / cypress / pytest / none) →
|
|
@@ -234,7 +236,8 @@ directly. Each skill stops at a gate and never auto-advances unless a step has *
|
|
|
234
236
|
contract lock. `generate` / `refresh` / `deploy`.
|
|
235
237
|
- **`yad-docs-overview`** — Generates the project **SDLC-overview site** (`docs/sdlc-site/`) — every
|
|
236
238
|
stage from setup → ship as flow paths / system components / stakeholder roles, reusing the same shell —
|
|
237
|
-
superseding the hand-maintained `docs/index.html
|
|
239
|
+
superseding the hand-maintained `docs/index.html` (folded into the site as `public/report.html`, linked
|
|
240
|
+
from the nav).
|
|
238
241
|
- **`yad-docs-sync`** — Keeps the sites fresh: detects staleness (a content hash of the authored
|
|
239
242
|
artifacts + the connected repos' HEAD shas vs each site's build manifest), regenerates + redeploys, and
|
|
240
243
|
can wire a CI job that rebuilds on push. Generalizes the rule that feature work must hand-update the
|
|
@@ -387,9 +390,10 @@ detailed sections below expand every phase. Invoke a skill by name in your agent
|
|
|
387
390
|
`testing.json`, lets `yad-test-cases` implement automation), `yad-connect-learning action: connect`
|
|
388
391
|
(DeepTutor-first → `learning.json`, powers the cross-cutting learning layer).
|
|
389
392
|
7. **(Optional) Put the hub on a platform** so the front-half review runs through real PRs:
|
|
390
|
-
`yad-connect-repos action: detect-hub`, then `
|
|
391
|
-
name +
|
|
392
|
-
|
|
393
|
+
`yad-connect-repos action: detect-hub`, then `yad roster add <login>` once per reviewer (login →
|
|
394
|
+
SDLC name + per-repo roles — the `add` walk asks for each connected repo's role; `yad roster grant`
|
|
395
|
+
sets one directly), and `yad-pr-template repo:hub action: wire` / `yad-review-comments repo:hub
|
|
396
|
+
action: wire` / `yad-checks repo:hub action: wire`. With no hub platform the front gate runs file-only.
|
|
393
397
|
8. **Conventions:** commits and PR/MR titles follow Conventional Commits (lowercase after the type), the
|
|
394
398
|
human author owns each commit with an optional per-commit `Co-Authored-By` AI trailer — see
|
|
395
399
|
[`CONTRIBUTING.md`](CONTRIBUTING.md).
|
package/bin/yad.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { runCommit } from '../cli/commit.mjs';
|
|
|
10
10
|
import { runOpenPr } from '../cli/openpr.mjs';
|
|
11
11
|
import { runShip } from '../cli/ship.mjs';
|
|
12
12
|
import { runRepo } from '../cli/repo.mjs';
|
|
13
|
+
import { runRoster } from '../cli/roster.mjs';
|
|
13
14
|
import { runDocs } from '../cli/docs.mjs';
|
|
14
15
|
import { runDoctor } from '../cli/doctor.mjs';
|
|
15
16
|
|
|
@@ -24,6 +25,14 @@ ${c.bold('Setup & maintenance')}
|
|
|
24
25
|
yad doctor [--json] Environment + state health: tools/auth, config files,
|
|
25
26
|
repo paths, epic ledgers (exit 1 on any failure)
|
|
26
27
|
|
|
28
|
+
${c.bold('Reviewer roster')}
|
|
29
|
+
yad roster list Show every member + their roles per scope (hub + each repo)
|
|
30
|
+
yad roster add <login> Add/edit a member, then walk the connected repos for their roles
|
|
31
|
+
(--name, --email, --roles "hub=owner,reviewer backend=domain-owner")
|
|
32
|
+
yad roster grant <name> <repo> <role...> Grant role(s) for a connected repo (domain-owner|reviewer|owner)
|
|
33
|
+
yad roster revoke <name> <repo> <role...> Remove role(s) for a repo
|
|
34
|
+
yad roster remove <login> Delete a member from the roster
|
|
35
|
+
|
|
27
36
|
${c.bold('Review gate (front half)')}
|
|
28
37
|
yad gate open <epic> <artifact> Open the review PR/MR; mark the step in_review
|
|
29
38
|
yad gate sync <epic> [artifact] Pull PR state -> ledger; advance on approved+resolved+merged
|
|
@@ -66,7 +75,7 @@ ${c.bold('Options')}
|
|
|
66
75
|
-h, --help Show this help
|
|
67
76
|
-v, --version Print version`;
|
|
68
77
|
|
|
69
|
-
const VALUE_FLAGS = new Set(['--dir', '--type', '--message', '--task', '--ai', '--risk', '--repo', '--platform', '--base', '--title', '--scope', '--branch', '--pr', '--epic']);
|
|
78
|
+
const VALUE_FLAGS = new Set(['--dir', '--type', '--message', '--task', '--ai', '--risk', '--repo', '--platform', '--base', '--title', '--scope', '--branch', '--pr', '--epic', '--name', '--email', '--roles']);
|
|
70
79
|
|
|
71
80
|
function parseArgs(argv) {
|
|
72
81
|
const o = { _: [], dir: process.cwd(), fix: false, force: false, scope: 'all' };
|
|
@@ -147,6 +156,11 @@ async function main() {
|
|
|
147
156
|
await runRepo(o.dir, { action: action || 'list', name, today });
|
|
148
157
|
break;
|
|
149
158
|
}
|
|
159
|
+
case 'roster': {
|
|
160
|
+
const [, action, ...rest] = o._;
|
|
161
|
+
await runRoster(o.dir, { action: action || 'list', args: rest, name: o.name, email: o.email, roles: o.roles, today });
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
150
164
|
case 'docs': {
|
|
151
165
|
const [, action] = o._;
|
|
152
166
|
if (o.epic && !isValidEpicId(o.epic)) { log(c.red(`invalid epic id: ${o.epic} (expected EP-<slug>, [a-z0-9-] only)`)); process.exitCode = 1; break; }
|
package/cli/platform.mjs
CHANGED
|
@@ -89,7 +89,7 @@ export function validateLogin(platform, login) {
|
|
|
89
89
|
const r = run('glab', ['api', `users?username=${encodeURIComponent(login)}`]);
|
|
90
90
|
if (!r.ok) return { ok: false, exists: false, checked: true };
|
|
91
91
|
let exists = false;
|
|
92
|
-
try { exists = Array.isArray(JSON.parse(r.stdout)) && JSON.parse(r.stdout).length > 0; } catch { exists
|
|
92
|
+
try { exists = Array.isArray(JSON.parse(r.stdout)) && JSON.parse(r.stdout).length > 0; } catch { /* malformed JSON -> exists stays false */ }
|
|
93
93
|
return { ok: exists, exists, checked: true };
|
|
94
94
|
}
|
|
95
95
|
|
package/cli/roster.mjs
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// `yad roster` — manage the reviewer roster (.sdlc/hub.json) and per-repo roles at any time.
|
|
2
|
+
// The roster maps a platform login -> SDLC name + a per-scope roles map; it is the only thing that lets
|
|
3
|
+
// the review gate attribute approvals and route per-repo domain-owner reviewers. This is the standalone
|
|
4
|
+
// counterpart of the `yad setup` roster step and the `yad-connect-repos action: roster` skill action.
|
|
5
|
+
// Repo-driven: `add`/`edit` walks the connected repos from repos.json so roles are assigned against what
|
|
6
|
+
// is actually connected, not against repo names the user has to remember. Granting/revoking a
|
|
7
|
+
// `domain-owner` keeps repos.json `domain_owners` in sync so the gate never drifts from the roster.
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { c, log, ok, info, warn, hand, fail, ask, askYesNo, readJSON, writeJSON } from './lib.mjs';
|
|
10
|
+
import { PROJECT_FILES } from './manifest.mjs';
|
|
11
|
+
import { rolesForScope } from './platform.mjs';
|
|
12
|
+
import { parseRolesSpec, upsertRosterEntry, addRepoRoles, removeRepoRole, setRepoDomainOwners, reconcileRepoRoles } from './setup.mjs';
|
|
13
|
+
|
|
14
|
+
const ROLES = ['owner', 'reviewer', 'domain-owner'];
|
|
15
|
+
|
|
16
|
+
const loadHub = (root) => readJSON(path.join(root, PROJECT_FILES.hubConfig), null);
|
|
17
|
+
const loadRepos = (root) => readJSON(path.join(root, PROJECT_FILES.reposRegistry), { repos: [] }).repos || [];
|
|
18
|
+
const ownersOf = (repo) => (Array.isArray(repo.domain_owners) ? repo.domain_owners : (repo.domain_owner ? [repo.domain_owner] : []));
|
|
19
|
+
|
|
20
|
+
// `list` — every member with their per-scope roles, plus a drift check between hub.json roles and
|
|
21
|
+
// repos.json domain_owners (the two should agree; the sync on grant/revoke keeps them aligned).
|
|
22
|
+
function rosterList(root) {
|
|
23
|
+
const hub = loadHub(root);
|
|
24
|
+
if (!hub || !Array.isArray(hub.roster) || !hub.roster.length) {
|
|
25
|
+
warn('no roster yet (.sdlc/hub.json) — add one with `yad roster add <login>` or `yad setup`');
|
|
26
|
+
return { members: 0 };
|
|
27
|
+
}
|
|
28
|
+
const repos = loadRepos(root);
|
|
29
|
+
log(c.bold('\nreviewer roster'));
|
|
30
|
+
for (const e of hub.roster) {
|
|
31
|
+
const flag = e.unverified ? c.yellow(' (unverified)') : '';
|
|
32
|
+
log(` ${c.bold(e.name || e.login)} ${c.dim(`@${e.login}`)}${e.email ? c.dim(` <${e.email}>`) : ''}${flag}`);
|
|
33
|
+
const hubRoles = rolesForScope(e, 'hub');
|
|
34
|
+
log(` hub: ${hubRoles.length ? hubRoles.join(', ') : c.dim('—')}`);
|
|
35
|
+
for (const r of repos) {
|
|
36
|
+
const rr = rolesForScope(e, r.name);
|
|
37
|
+
if (rr.length) log(` ${r.name}: ${rr.join(', ')}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const drift = [];
|
|
41
|
+
for (const r of repos) {
|
|
42
|
+
const owners = ownersOf(r);
|
|
43
|
+
for (const nm of owners) {
|
|
44
|
+
const e = hub.roster.find((x) => x.name === nm);
|
|
45
|
+
if (!e || !rolesForScope(e, r.name).includes('domain-owner')) {
|
|
46
|
+
drift.push(`${nm} owns ${r.name} in repos.json but has no domain-owner role in hub.json`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
for (const e of hub.roster) {
|
|
50
|
+
if (rolesForScope(e, r.name).includes('domain-owner') && !owners.includes(e.name)) {
|
|
51
|
+
drift.push(`${e.name} has domain-owner for ${r.name} in hub.json but is missing from repos.json domain_owners`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (drift.length) { log(''); for (const d of drift) warn(d); }
|
|
56
|
+
return { members: hub.roster.length, repos: repos.length, drift: drift.length };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// The repo-driven walk: for each connected repo show the member's current role and offer to set it.
|
|
60
|
+
async function repoWalk(root, entry) {
|
|
61
|
+
const repos = loadRepos(root);
|
|
62
|
+
if (!repos.length) { info('no connected repos to assign roles for (.sdlc/repos.json) — connect one with `yad setup`'); return; }
|
|
63
|
+
for (const r of repos) {
|
|
64
|
+
const cur = rolesForScope(entry, r.name);
|
|
65
|
+
if (!(await askYesNo(` set ${entry.name}'s role on ${r.name}? (current: ${cur.length ? cur.join(', ') : 'none'})`, false))) continue;
|
|
66
|
+
const input = await ask(' roles (domain-owner/reviewer/owner, space-separated; blank = clear)', cur.join(' '));
|
|
67
|
+
const want = input.split(/\s+/).map((x) => x.trim()).filter(Boolean);
|
|
68
|
+
const invalid = want.filter((x) => !ROLES.includes(x));
|
|
69
|
+
if (invalid.length) warn(`ignoring unknown role(s): ${invalid.join(', ')} (allowed: ${ROLES.join(', ')})`);
|
|
70
|
+
reconcileRepoRoles(root, entry.name, r.name, cur, want.filter((x) => ROLES.includes(x)));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Mirror any domain-owner scopes from a non-interactive `--roles` upsert into repos.json.
|
|
75
|
+
function syncDomainOwners(root, entry) {
|
|
76
|
+
for (const [scope, list] of Object.entries(entry.roles || {})) {
|
|
77
|
+
if (scope !== 'hub' && Array.isArray(list) && list.includes('domain-owner')) setRepoDomainOwners(root, scope, entry.name, { add: true });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// `add`/`edit <login>` — upsert by login (from flags or interactive prompts), then either apply a
|
|
82
|
+
// `--roles` spec directly (scriptable) or run the repo-driven walk (interactive default).
|
|
83
|
+
async function rosterAdd(root, login, { name, email, roles } = {}) {
|
|
84
|
+
if (!login) { fail('usage: yad roster add <login> [--name N] [--email E] [--roles "hub=owner,reviewer backend=domain-owner"]'); process.exitCode = 1; return {}; }
|
|
85
|
+
const hub = loadHub(root);
|
|
86
|
+
const platform = hub ? hub.platform : null;
|
|
87
|
+
const existing = hub && Array.isArray(hub.roster) ? hub.roster.find((e) => e.login === login) : null;
|
|
88
|
+
const scripted = !!roles;
|
|
89
|
+
let nm = name;
|
|
90
|
+
let em = email;
|
|
91
|
+
let rolesMap;
|
|
92
|
+
if (scripted) {
|
|
93
|
+
rolesMap = parseRolesSpec(roles);
|
|
94
|
+
} else {
|
|
95
|
+
nm = nm || await ask(' yad name', (existing && existing.name) || login);
|
|
96
|
+
em = em || await ask(' commit email (blank to skip)', (existing && existing.email) || '');
|
|
97
|
+
const def = rolesForScope(existing, 'hub').join(' ') || 'reviewer';
|
|
98
|
+
const hubRoles = (await ask(' hub roles (owner/reviewer, space-separated)', def)).split(/\s+/).filter(Boolean);
|
|
99
|
+
rolesMap = hubRoles.length ? { hub: hubRoles } : {};
|
|
100
|
+
}
|
|
101
|
+
const { entry, created } = upsertRosterEntry(root, { login, name: nm, email: em || undefined, roles: rolesMap, platform });
|
|
102
|
+
if (!entry) return {};
|
|
103
|
+
ok(`${created ? 'added' : 'updated'} ${entry.name} (@${login})`);
|
|
104
|
+
if (scripted) syncDomainOwners(root, entry);
|
|
105
|
+
else await repoWalk(root, entry);
|
|
106
|
+
return { entry: entry.name, created };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// `grant <name> <repo> <role...>` — scriptable per-repo grant (member must already be in the roster).
|
|
110
|
+
function rosterGrant(root, [name, repo, ...roles]) {
|
|
111
|
+
if (!name || !repo || !roles.length) { fail('usage: yad roster grant <name> <repo> <role...>'); process.exitCode = 1; return {}; }
|
|
112
|
+
const invalid = roles.filter((r) => !ROLES.includes(r));
|
|
113
|
+
if (invalid.length) { fail(`unknown role(s): ${invalid.join(', ')} (allowed: ${ROLES.join(', ')})`); process.exitCode = 1; return {}; }
|
|
114
|
+
const hub = loadHub(root);
|
|
115
|
+
if (!hub || !Array.isArray(hub.roster) || !hub.roster.some((e) => e.name === name)) {
|
|
116
|
+
fail(`'${name}' is not in the roster — add them first with \`yad roster add <login>\``); process.exitCode = 1; return {};
|
|
117
|
+
}
|
|
118
|
+
if (!loadRepos(root).some((r) => r.name === repo)) warn(`repo '${repo}' is not registered — the role is recorded and applies once it is connected`);
|
|
119
|
+
addRepoRoles(root, repo, Object.fromEntries(roles.map((r) => [r, [name]])));
|
|
120
|
+
if (roles.includes('domain-owner')) setRepoDomainOwners(root, repo, name, { add: true });
|
|
121
|
+
ok(`granted ${name} ${roles.join(', ')} on ${repo}`);
|
|
122
|
+
return { name, repo, roles };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// `revoke <name> <repo> <role...>` — scriptable per-repo revoke.
|
|
126
|
+
function rosterRevoke(root, [name, repo, ...roles]) {
|
|
127
|
+
if (!name || !repo || !roles.length) { fail('usage: yad roster revoke <name> <repo> <role...>'); process.exitCode = 1; return {}; }
|
|
128
|
+
removeRepoRole(root, name, repo, roles);
|
|
129
|
+
if (roles.includes('domain-owner')) setRepoDomainOwners(root, repo, name, { add: false });
|
|
130
|
+
ok(`revoked ${name} ${roles.join(', ')} on ${repo}`);
|
|
131
|
+
return { name, repo, roles };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// `remove <login>` — delete a member; warn (do not cascade) if still a domain owner in repos.json.
|
|
135
|
+
function rosterRemove(root, login) {
|
|
136
|
+
if (!login) { fail('usage: yad roster remove <login>'); process.exitCode = 1; return {}; }
|
|
137
|
+
const hubPath = path.join(root, PROJECT_FILES.hubConfig);
|
|
138
|
+
const hub = readJSON(hubPath, null);
|
|
139
|
+
if (!hub || !Array.isArray(hub.roster)) { warn('no roster to remove from (.sdlc/hub.json)'); return { removed: 0 }; }
|
|
140
|
+
const idx = hub.roster.findIndex((e) => e.login === login);
|
|
141
|
+
if (idx < 0) { warn(`no roster member with login '${login}'`); return { removed: 0 }; }
|
|
142
|
+
const [removed] = hub.roster.splice(idx, 1);
|
|
143
|
+
writeJSON(hubPath, hub);
|
|
144
|
+
ok(`removed ${removed.name} (@${login})`);
|
|
145
|
+
const refs = loadRepos(root).filter((r) => ownersOf(r).includes(removed.name)).map((r) => r.name);
|
|
146
|
+
if (refs.length) hand(`'${removed.name}' is still a domain owner in repos.json for: ${refs.join(', ')} — revoke with \`yad roster revoke ${removed.name} <repo> domain-owner\``);
|
|
147
|
+
return { removed: 1 };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function runRoster(root, { action = 'list', args = [], name, email, roles } = {}) {
|
|
151
|
+
switch (action) {
|
|
152
|
+
case 'list': return rosterList(root);
|
|
153
|
+
case 'add':
|
|
154
|
+
case 'edit': return rosterAdd(root, args[0], { name, email, roles });
|
|
155
|
+
case 'grant': return rosterGrant(root, args);
|
|
156
|
+
case 'revoke': return rosterRevoke(root, args);
|
|
157
|
+
case 'remove':
|
|
158
|
+
case 'rm': return rosterRemove(root, args[0]);
|
|
159
|
+
default:
|
|
160
|
+
fail(`unknown roster action: ${action} (list | add | edit | grant | revoke | remove)`);
|
|
161
|
+
process.exitCode = 1;
|
|
162
|
+
return {};
|
|
163
|
+
}
|
|
164
|
+
}
|
package/cli/setup.mjs
CHANGED
|
@@ -11,13 +11,29 @@ import {
|
|
|
11
11
|
moduleActions, repoActions, hubActions, authorsActions,
|
|
12
12
|
legacyModuleActions, legacyRepoActions, legacyHubActions,
|
|
13
13
|
} from './plan.mjs';
|
|
14
|
-
import { validateLogin } from './platform.mjs';
|
|
14
|
+
import { validateLogin, rolesForScope } from './platform.mjs';
|
|
15
15
|
|
|
16
16
|
// Parse a comma/space separated list into a clean, deduped array of trimmed tokens.
|
|
17
|
-
function parseList(s) {
|
|
17
|
+
export function parseList(s) {
|
|
18
18
|
return [...new Set((s || '').split(/[,\s]+/).map((x) => x.trim()).filter(Boolean))];
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// Parse a per-scope roles spec — `"hub=owner,reviewer backend=domain-owner"` — into the roster's
|
|
22
|
+
// per-scope map `{ hub: ['owner','reviewer'], backend: ['domain-owner'] }`. Tokens are whitespace
|
|
23
|
+
// separated; each is `scope=role[,role...]`. Malformed tokens (no `=`, empty scope/roles) are skipped.
|
|
24
|
+
export function parseRolesSpec(s) {
|
|
25
|
+
const out = {};
|
|
26
|
+
for (const tok of (s || '').split(/\s+/).map((x) => x.trim()).filter(Boolean)) {
|
|
27
|
+
const eq = tok.indexOf('=');
|
|
28
|
+
if (eq < 0) continue;
|
|
29
|
+
const scope = tok.slice(0, eq).trim();
|
|
30
|
+
const roles = parseList(tok.slice(eq + 1));
|
|
31
|
+
if (!scope || !roles.length) continue;
|
|
32
|
+
out[scope] = [...new Set([...(out[scope] || []), ...roles])];
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
21
37
|
const ALL_IDES = [...IDE_FOLDER_TARGETS, '.opencode'];
|
|
22
38
|
|
|
23
39
|
export function detectPlatform(remoteUrl = '') {
|
|
@@ -68,6 +84,98 @@ export function addRepoRoles(root, repo, grants = {}) {
|
|
|
68
84
|
if (touched) writeJSON(hubPath, hub);
|
|
69
85
|
}
|
|
70
86
|
|
|
87
|
+
// Normalize a roster entry's roles in place to the per-scope map, migrating the two legacy shapes
|
|
88
|
+
// (a flat array, or a single `role` string) into `roles.hub` so nothing is lost. Mirrors the
|
|
89
|
+
// migration addRepoRoles does; pulled out so upsert/remove share it.
|
|
90
|
+
function normalizeRoles(entry) {
|
|
91
|
+
if (!entry.roles || typeof entry.roles !== 'object' || Array.isArray(entry.roles)) {
|
|
92
|
+
const hub = Array.isArray(entry.roles) ? entry.roles : (entry.role ? [entry.role] : []);
|
|
93
|
+
entry.roles = hub.length ? { hub } : {};
|
|
94
|
+
delete entry.role;
|
|
95
|
+
}
|
|
96
|
+
return entry.roles;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Upsert one roster member into hub.json, keyed by `login`. Deep-merges the per-scope `roles` map so
|
|
100
|
+
// scopes the caller did not name are preserved; sets `name`/`email` when given; validates the login
|
|
101
|
+
// against the hub (warn-only — a miss flags `unverified`, `checked:false` skips silently). Creates the
|
|
102
|
+
// hub.json shell if absent. Returns { entry, created }.
|
|
103
|
+
export function upsertRosterEntry(root, { login, name, email, roles = {}, platform } = {}) {
|
|
104
|
+
if (!login) { warn('roster upsert needs a login — skipped'); return { entry: null, created: false }; }
|
|
105
|
+
const hubPath = path.join(root, PROJECT_FILES.hubConfig);
|
|
106
|
+
const hub = readJSON(hubPath, null) || { platform: platform && platform !== 'none' ? platform : null, bridge_enabled: false, bridge: false, default_branch: 'main', roster: [] };
|
|
107
|
+
if (!Array.isArray(hub.roster)) hub.roster = [];
|
|
108
|
+
let entry = hub.roster.find((e) => e.login === login);
|
|
109
|
+
const created = !entry;
|
|
110
|
+
if (!entry) { entry = { login, name: name || login, roles: {} }; hub.roster.push(entry); }
|
|
111
|
+
if (name) entry.name = name;
|
|
112
|
+
if (email) entry.email = email;
|
|
113
|
+
normalizeRoles(entry);
|
|
114
|
+
for (const [scope, list] of Object.entries(roles || {})) {
|
|
115
|
+
const cur = Array.isArray(entry.roles[scope]) ? entry.roles[scope] : [];
|
|
116
|
+
entry.roles[scope] = [...new Set([...cur, ...list])];
|
|
117
|
+
}
|
|
118
|
+
if (email && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) warn(`'${email}' does not look like an email address`);
|
|
119
|
+
const plat = platform || hub.platform;
|
|
120
|
+
if (plat && plat !== 'none') {
|
|
121
|
+
const v = validateLogin(plat, login);
|
|
122
|
+
if (v.checked && !v.exists) { warn(`'${login}' not found on ${plat} — saved as unverified`); entry.unverified = true; }
|
|
123
|
+
else if (v.checked && v.exists) { ok(`verified ${login} on ${plat}`); delete entry.unverified; }
|
|
124
|
+
}
|
|
125
|
+
writeJSON(hubPath, hub);
|
|
126
|
+
return { entry, created };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Inverse of addRepoRoles: drop the named role(s) from a member's `roles[<repo>]` scope, removing the
|
|
130
|
+
// scope key when it empties. Member is found by yad `name` (matching addRepoRoles). Idempotent.
|
|
131
|
+
export function removeRepoRole(root, name, repo, roles = []) {
|
|
132
|
+
const hubPath = path.join(root, PROJECT_FILES.hubConfig);
|
|
133
|
+
const hub = readJSON(hubPath, null);
|
|
134
|
+
if (!hub || !Array.isArray(hub.roster)) return;
|
|
135
|
+
const entry = hub.roster.find((e) => e.name === name);
|
|
136
|
+
if (!entry) { warn(`'${name}' is not in the roster — nothing to revoke for ${repo}`); return; }
|
|
137
|
+
normalizeRoles(entry);
|
|
138
|
+
const cur = Array.isArray(entry.roles[repo]) ? entry.roles[repo] : [];
|
|
139
|
+
const next = cur.filter((r) => !roles.includes(r));
|
|
140
|
+
if (next.length === cur.length) return; // nothing removed
|
|
141
|
+
if (next.length) entry.roles[repo] = next; else delete entry.roles[repo];
|
|
142
|
+
writeJSON(hubPath, hub);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Keep repos.json `domain_owners` in sync when a domain-owner role is granted/revoked via the roster,
|
|
146
|
+
// so the gate's per-repo reviewer-routing and the derivation fallback never drift from hub.json. Adds
|
|
147
|
+
// or removes the yad `name` and mirrors `domain_owner = domain_owners[0]`. No-op + warn if the repo is
|
|
148
|
+
// not registered. Returns true when the registry was written.
|
|
149
|
+
export function setRepoDomainOwners(root, repo, name, { add = true } = {}) {
|
|
150
|
+
const regPath = path.join(root, PROJECT_FILES.reposRegistry);
|
|
151
|
+
const registry = readJSON(regPath, { repos: [] });
|
|
152
|
+
const entry = (registry.repos || []).find((r) => r.name === repo);
|
|
153
|
+
if (!entry) { warn(`repo '${repo}' is not registered (.sdlc/repos.json) — domain_owners not synced`); return false; }
|
|
154
|
+
const owners = Array.isArray(entry.domain_owners) ? [...entry.domain_owners] : (entry.domain_owner ? [entry.domain_owner] : []);
|
|
155
|
+
const has = owners.includes(name);
|
|
156
|
+
let next;
|
|
157
|
+
if (add) { if (has) return false; next = [...owners, name]; }
|
|
158
|
+
else { if (!has) return false; next = owners.filter((o) => o !== name); }
|
|
159
|
+
entry.domain_owners = next;
|
|
160
|
+
entry.domain_owner = next[0] || '';
|
|
161
|
+
writeJSON(regPath, registry);
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Reconcile one repo's roles for a member to exactly `want`: grant what is new, revoke what is gone,
|
|
166
|
+
// and mirror domain-owner changes into repos.json. `current` is the member's existing roles for the
|
|
167
|
+
// repo. Shared by the `yad roster` walk and the `yad setup` per-repo role step. Idempotent.
|
|
168
|
+
export function reconcileRepoRoles(root, name, repo, current = [], want = []) {
|
|
169
|
+
const toAdd = want.filter((r) => !current.includes(r));
|
|
170
|
+
const toRemove = current.filter((r) => !want.includes(r));
|
|
171
|
+
if (!toAdd.length && !toRemove.length) { info(` ${repo}: unchanged`); return; }
|
|
172
|
+
if (toAdd.length) addRepoRoles(root, repo, Object.fromEntries(toAdd.map((r) => [r, [name]])));
|
|
173
|
+
if (toRemove.length) removeRepoRole(root, name, repo, toRemove);
|
|
174
|
+
if (toAdd.includes('domain-owner')) setRepoDomainOwners(root, repo, name, { add: true });
|
|
175
|
+
if (toRemove.includes('domain-owner')) setRepoDomainOwners(root, repo, name, { add: false });
|
|
176
|
+
ok(` ${repo}: ${want.length ? want.join(', ') : 'cleared'}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
71
179
|
export function registerRepo(root, registry, { name, rpath, platform, domain_owner = '', domain_owners = null, default_branch = 'main', today = null }) {
|
|
72
180
|
if (!insideRoot(root, rpath)) {
|
|
73
181
|
warn(`${rpath} resolves outside the project root — skipped`);
|
|
@@ -370,6 +478,24 @@ export async function runSetup(root, opts = {}) {
|
|
|
370
478
|
}
|
|
371
479
|
}
|
|
372
480
|
|
|
481
|
+
// 7b. Assign/update roles for ALREADY-connected repos. The connect loop above only prompts for the
|
|
482
|
+
// repos you add now; this closes the gap so a member's domain-owner/reviewer/owner role on a repo
|
|
483
|
+
// connected in an earlier run can be set without reconnecting. Mirrors `yad roster` (repo-driven).
|
|
484
|
+
const hub7 = readJSON(hubPath, null);
|
|
485
|
+
if (registry.repos.length && hub7 && Array.isArray(hub7.roster) && hub7.roster.length
|
|
486
|
+
&& await askYesNo('Assign/update roles for connected repos?', false)) {
|
|
487
|
+
for (const member of hub7.roster) {
|
|
488
|
+
if (!(await askYesNo(` edit ${member.name}'s repo roles?`, false))) continue;
|
|
489
|
+
for (const repo of registry.repos) {
|
|
490
|
+
const cur = rolesForScope(member, repo.name);
|
|
491
|
+
if (!(await askYesNo(` set ${member.name}'s role on ${repo.name}? (current: ${cur.length ? cur.join(', ') : 'none'})`, false))) continue;
|
|
492
|
+
const input = await ask(' roles (domain-owner/reviewer/owner, space-separated; blank = clear)', cur.join(' '));
|
|
493
|
+
const want = parseList(input).filter((x) => ['owner', 'reviewer', 'domain-owner'].includes(x));
|
|
494
|
+
reconcileRepoRoles(root, member.name, repo.name, cur, want);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
373
499
|
// 8. Wire each connected repo + the hub itself
|
|
374
500
|
step(8, total, 'Wire connected repos + the hub (CI gates, PR template, comment scaffold, gate-sync)');
|
|
375
501
|
if (registry.repos.length === 0) info('no repos to wire');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yadflow",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.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, ship, repo). A BMAD module + 29 yad-* skills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "AbdelRahman Nasr",
|
|
@@ -22,7 +22,6 @@
|
|
|
22
22
|
"!cli/test.mjs",
|
|
23
23
|
"!cli/test-checks.mjs",
|
|
24
24
|
"skills/",
|
|
25
|
-
"docs/index.html",
|
|
26
25
|
"README.md",
|
|
27
26
|
"LICENSE",
|
|
28
27
|
"CHANGELOG.md"
|
|
@@ -55,9 +54,9 @@
|
|
|
55
54
|
"deeptutor"
|
|
56
55
|
],
|
|
57
56
|
"devDependencies": {
|
|
58
|
-
"@eslint/js": "^
|
|
57
|
+
"@eslint/js": "^10.0.1",
|
|
59
58
|
"@semantic-release/changelog": "^6.0.3",
|
|
60
|
-
"eslint": "^
|
|
59
|
+
"eslint": "^10.5.0",
|
|
61
60
|
"semantic-release": "^25.0.3"
|
|
62
61
|
}
|
|
63
62
|
}
|
|
@@ -26,5 +26,5 @@ SDLC Workflow,yad-run,Run (Automation),RN,"Phase 4 orchestrator: drive a story's
|
|
|
26
26
|
SDLC Workflow,yad-status,SDLC Status,SS,"Read-only: print the full front-state chain, per-step dials, contract lock, story repo tags, and pending approvals at the active gate. For stories in the build half, also print each back-half step's automation dial and status, the trust record (runs / % approved-unchanged / earned vs gathering evidence), and the system-wide kill-switch state.",,{epic: EP-<slug>},1-front,,,false,,
|
|
27
27
|
SDLC Workflow,yad-connect-docs,Connect Docs Target,DX,"Setup/maintenance: connect a docs/Pages publishing target so the interactive-docs steps can deploy the generated site (not just commit its source). Auto-detects the platform from .sdlc/hub.json (github->github-pages, gitlab->gitlab-pages, null->build-only), resolves the Vite base path, and records the target in .sdlc/docs.json (local-user auth, no stored tokens). Degrades to build-only when gh/glab is absent. Idempotent and refreshable; one connection per project. Not a gated state — never touches epic state or approvals.",,{action: connect|refresh|list|disconnect} {target: github-pages|gitlab-pages|none} {scope: hub|<repo>|dedicated} {base_path: <override>},0-setup,,yad-docs,false,.sdlc/,docs.json
|
|
28
28
|
SDLC Workflow,yad-docs,Author Docs Site,DS,"Generate the per-epic interactive documentation SPA (animated flow canvas + role-based stakeholder doc pages) from the epic's approved artifacts (epic, architecture, locked contract, ui-design, stories, code-context, test-cases), themed by the connected design system, into epics/EP-<slug>/docs-site/. Copies the vendored React/Vite/Tailwind shell verbatim and generates src/data/*.ts deterministically; drives the yad docs build/deploy CLI to publish to Pages (or build-only). An OUTPUT ENRICHMENT — never a gate; never mutates state.json, approvals, or the contract lock.",,{epic: EP-<slug>} {action: generate|refresh|deploy} {login_gate: true|false},,yad-review-gate,,false,epics/EP-<slug>/docs-site/,docs-site/ docs-build.json
|
|
29
|
-
SDLC Workflow,yad-docs-overview,Docs Overview Site,DO,"Generate the project SDLC-overview interactive site (docs/sdlc-site/) — every stage from setup to ship modeled as flow paths, system components, and stakeholder roles — reusing the same vendored shell, themed with yadflow's brand palette. Reads config.yaml + module-help.csv + the overview diagram as the pipeline source.
|
|
29
|
+
SDLC Workflow,yad-docs-overview,Docs Overview Site,DO,"Generate the project SDLC-overview interactive site (docs/sdlc-site/) — every stage from setup to ship modeled as flow paths, system components, and stakeholder roles — reusing the same vendored shell, themed with yadflow's brand palette. Reads config.yaml + module-help.csv + the overview diagram as the pipeline source. Folds the hand-maintained docs/index.html report into the site as report.html (linked from the nav). Not a gate — a project-level enrichment that regenerates whenever the skill set / pipeline changes.",,{action: generate|deploy},,,,false,docs/sdlc-site/,sdlc-site/ .docs-build.json
|
|
30
30
|
SDLC Workflow,yad-docs-sync,Docs Sync,DY,"Maintenance/CI: keep the generated doc sites fresh. Detect staleness (a content hash of the approved artifacts + the connected repos' HEAD shas + the doc-shell version vs each site's build manifest), report which sites drifted and why, regenerate + redeploy the stale ones, and wire a CI job that rebuilds on push (carrying [skip ci] + a concurrency group to prevent deploy loops). Generalizes the rule that feature work must hand-update docs/index.html + diagrams + skill counts. Refresh is always a human/CI decision; never a gate.",,{action: check|refresh|wire} {epic: EP-<slug>},,,,false,epics/EP-<slug>/.sdlc/,docs-build.json yad-docs.yml
|
|
@@ -133,6 +133,10 @@ write only `{project-root}/.sdlc/hub.json` (`config.yaml` `hub.config`) — neve
|
|
|
133
133
|
`roles[<repo>]` (and still **derived** as a fallback when a roster `name` equals a repo's
|
|
134
134
|
`domain_owner`/`domain_owners` in `repos.json` — see `references/hub-config.md`). An unmapped login
|
|
135
135
|
degrades to a plain `reviewer`, never auto-promoted to owner/domain-owner.
|
|
136
|
+
**The deterministic half is the `yad roster` CLI command** — runnable any time, not just at setup:
|
|
137
|
+
`yad roster list`; `yad roster add <login>` (upsert, then a repo-driven walk that asks for each
|
|
138
|
+
connected repo's role); `yad roster grant|revoke <name> <repo> <role>`; `yad roster remove <login>`.
|
|
139
|
+
A `domain-owner` grant/revoke keeps `repos.json` `domain_owners` in sync so the gate never drifts.
|
|
136
140
|
|
|
137
141
|
If the hub has no remote (`platform: null`) or the bridge is disabled, the front-half gate runs
|
|
138
142
|
file-only with no error — the bridge is purely additive.
|
|
@@ -33,7 +33,9 @@ login to an SDLC name + role. It is a single object for the hub itself — the s
|
|
|
33
33
|
|
|
34
34
|
The roster is how a platform identity (a GitHub/GitLab **login**) becomes an SDLC **name + role(s)** in
|
|
35
35
|
the file ledger (`approvals.json` / `comments.json`). Roles are the same three the gate uses:
|
|
36
|
-
`owner | reviewer | domain-owner`.
|
|
36
|
+
`owner | reviewer | domain-owner`. Populate and edit it any time with the **`yad roster`** CLI command
|
|
37
|
+
(`list` / `add` / `grant` / `revoke` / `remove`) — `add` walks the connected repos asking for each
|
|
38
|
+
one's role, and a `domain-owner` grant keeps `repos.json` `domain_owners` in sync.
|
|
37
39
|
|
|
38
40
|
- **`login`** — the platform username whose PR review / approval is being mapped.
|
|
39
41
|
- **`name`** — the SDLC name written into the ledger (the same names used across `approvals.json`,
|
|
@@ -15,6 +15,28 @@ shell renders whatever these export, as long as it satisfies `src/data/types.ts`
|
|
|
15
15
|
| `docSections.ts` | `DOC_SECTIONS: DocSectionConfig[]` | `epic.md`, `architecture.md`, `contract.md`, `ui-design.md`, `test-cases.md` | the ordered doc-section registry (`{ id, title, icon, iconColor, component }`); each section id is referenced from `roles.ts`. |
|
|
16
16
|
| `referenceData.ts` | the reference tables/payloads the doc-section components render | `contract.md` CONTRACT-SURFACE (authoritative) + `architecture.md` + `test-cases.md` | API reference rows, the status machine, the DB schema, feature flags, error codes, the test plan — the structured data behind the doc sections. |
|
|
17
17
|
|
|
18
|
+
## Canvas layout (`components.ts` `position`)
|
|
19
|
+
|
|
20
|
+
`position` is `{ x, y }` in 0–100 (percent of the canvas). Lay the components out as a **hub-and-spoke
|
|
21
|
+
organized around a central hub into four surrounding zones** so the spokes fan out without crossing,
|
|
22
|
+
rather than scattering nodes:
|
|
23
|
+
|
|
24
|
+
- **Center** — the product hub (the brain).
|
|
25
|
+
- **Top band** — the file ledger the hub owns (state / approvals / contract-lock), spread across one row.
|
|
26
|
+
- **Left** — the code side: each connector with its external target just beyond it (`repos-json → code-repos`).
|
|
27
|
+
- **Right** — the connected tools as a single aligned column: each connector on the inner edge with its
|
|
28
|
+
external tool on the same row just outside it (`design-json → Design Tool`, etc.).
|
|
29
|
+
- **Bottom band** — publish / platform / evidence (docs / platform / trust-log).
|
|
30
|
+
|
|
31
|
+
Layout constraints (the nodes are fixed-size cards, ~116×146px, so spacing is what prevents overlap):
|
|
32
|
+
- Only **~4 rows** fit vertically — keep row centers **≥23% apart**; same-row neighbours **≥18% apart** in x.
|
|
33
|
+
- Keep all nodes inside ~6–94% on each axis so no card clips the canvas edge (tool column ≤ ~88% x,
|
|
34
|
+
bottom band ≤ ~80% y).
|
|
35
|
+
- Keep `label`s short (e.g. `Git Platform`, not `Platform (GitHub/GitLab)`) — a long label widens the
|
|
36
|
+
card and breaks the spacing.
|
|
37
|
+
- The layout is **deterministic**: assign zones by role (ledger / code / tools / platform+evidence) and
|
|
38
|
+
order within a zone by stable id, so an unchanged architecture regenerates byte-identically.
|
|
39
|
+
|
|
18
40
|
## Section sources (the doc sections + their artifact)
|
|
19
41
|
|
|
20
42
|
| Doc section(s) | Artifact source |
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
|
-
<link rel="icon" type="image/
|
|
5
|
+
<link rel="icon" type="image/png" href="/yadflow-icon.png" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>Booking Flow Visualizer</title>
|
|
8
8
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
Binary file
|
|
@@ -12,10 +12,8 @@ import { SystemLogsTerminal } from './components/Logs/SystemLogsTerminal';
|
|
|
12
12
|
import { SubPathDetailPage } from './pages/SubPathDetailPage';
|
|
13
13
|
import { RoleSelectPage } from './pages/RoleSelectPage';
|
|
14
14
|
import { StakeholderDocPage } from './pages/StakeholderDocPage';
|
|
15
|
-
import { LoginPage } from './components/Auth/LoginPage';
|
|
16
15
|
import { usePlayback } from './hooks/usePlayback';
|
|
17
16
|
import { useFlowStore } from './store/useFlowStore';
|
|
18
|
-
import { useAuthStore } from './store/useAuthStore';
|
|
19
17
|
|
|
20
18
|
function Dashboard() {
|
|
21
19
|
usePlayback();
|
|
@@ -74,12 +72,6 @@ function Dashboard() {
|
|
|
74
72
|
}
|
|
75
73
|
|
|
76
74
|
function App() {
|
|
77
|
-
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
|
|
78
|
-
|
|
79
|
-
if (!isAuthenticated) {
|
|
80
|
-
return <LoginPage />;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
75
|
return (
|
|
84
76
|
<div className="flex flex-col h-screen w-screen overflow-hidden" style={{ background: 'var(--color-bg-primary)' }}>
|
|
85
77
|
<TopNavBar />
|
|
@@ -52,8 +52,8 @@ export const SystemComponent: React.FC<SystemComponentProps> = React.memo(
|
|
|
52
52
|
: 'rgba(47, 41, 56, 0.4)',
|
|
53
53
|
backdropFilter: 'blur(12px)',
|
|
54
54
|
...glowStyle,
|
|
55
|
-
minWidth: '
|
|
56
|
-
minHeight: '
|
|
55
|
+
minWidth: '116px',
|
|
56
|
+
minHeight: '120px',
|
|
57
57
|
}}
|
|
58
58
|
animate={
|
|
59
59
|
isReceiving
|
|
@@ -6,6 +6,7 @@ import { TriggerEventCard } from './TriggerEventCard';
|
|
|
6
6
|
import { RequestPayloadPreview } from './RequestPayloadPreview';
|
|
7
7
|
import { HandlerLogicSnippet } from './HandlerLogicSnippet';
|
|
8
8
|
import { Icon } from '../shared/Icon';
|
|
9
|
+
import { Tooltip } from '../shared/Tooltip';
|
|
9
10
|
|
|
10
11
|
export function RightPanel() {
|
|
11
12
|
const getCurrentStep = useFlowStore((s) => s.getCurrentStep);
|
|
@@ -68,19 +69,19 @@ export function RightPanel() {
|
|
|
68
69
|
<Icon name="open_in_new" size={18} />
|
|
69
70
|
View Full Path Details
|
|
70
71
|
</button>
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
</
|
|
72
|
+
<Tooltip content="Coming soon" className="w-full">
|
|
73
|
+
<button
|
|
74
|
+
disabled
|
|
75
|
+
className="w-full py-2.5 rounded-lg text-slate-300 text-sm font-medium border flex items-center justify-center gap-2 opacity-50 cursor-not-allowed"
|
|
76
|
+
style={{
|
|
77
|
+
background: 'rgba(255,255,255,0.05)',
|
|
78
|
+
borderColor: 'rgba(255,255,255,0.05)',
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
<Icon name="bug_report" size={18} />
|
|
82
|
+
Debug Step
|
|
83
|
+
</button>
|
|
84
|
+
</Tooltip>
|
|
84
85
|
</div>
|
|
85
86
|
)}
|
|
86
87
|
</aside>
|