spectoflow 0.13.0 → 0.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +53 -15
- package/bin/spectoflow.js +48 -19
- package/package.json +1 -1
- package/templates/dashboard/public/app.js +153 -47
- package/templates/dashboard/public/designs.js +30 -0
- package/templates/dashboard/public/fonts/ibm-plex-sans-400.woff2 +0 -0
- package/templates/dashboard/public/fonts/ibm-plex-sans-500.woff2 +0 -0
- package/templates/dashboard/public/fonts/ibm-plex-sans-600.woff2 +0 -0
- package/templates/dashboard/public/fonts/jetbrains-mono-400.woff2 +0 -0
- package/templates/dashboard/public/fonts/jetbrains-mono-500.woff2 +0 -0
- package/templates/dashboard/public/fonts/jetbrains-mono-700.woff2 +0 -0
- package/templates/dashboard/public/fonts/sora-400.woff2 +0 -0
- package/templates/dashboard/public/fonts/sora-600.woff2 +0 -0
- package/templates/dashboard/public/fonts/sora-700.woff2 +0 -0
- package/templates/dashboard/public/fonts/space-grotesk-400.woff2 +0 -0
- package/templates/dashboard/public/fonts/space-grotesk-500.woff2 +0 -0
- package/templates/dashboard/public/fonts/space-grotesk-700.woff2 +0 -0
- package/templates/dashboard/public/icons.js +33 -2
- package/templates/dashboard/public/index.html +63 -30
- package/templates/dashboard/public/styles.css +293 -26
- package/templates/dashboard/server.js +15 -2
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -23,8 +23,27 @@ ceremonial command to start.
|
|
|
23
23
|
## Install
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
npm
|
|
27
|
-
|
|
26
|
+
# npm (recommended)
|
|
27
|
+
npm install -g spectoflow
|
|
28
|
+
spectoflow init /path/to/project
|
|
29
|
+
|
|
30
|
+
# or use it straight from a clone, no global install
|
|
31
|
+
git clone https://github.com/georgesmomo/spectoflow
|
|
32
|
+
node spectoflow/bin/spectoflow.js init /path/to/project
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Every command works both ways — `spectoflow <cmd>` when installed globally, or `node
|
|
36
|
+
/path/to/spectoflow/bin/spectoflow.js <cmd>` from a clone.
|
|
37
|
+
|
|
38
|
+
### CLI
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
spectoflow init [dir] [--agent=claude,codex] scaffold a project (auto-detects installed agents)
|
|
42
|
+
spectoflow update [--dry-run] refresh framework files to this kit version
|
|
43
|
+
spectoflow dashboard [--port=NNNN] run the local control plane (default 4319)
|
|
44
|
+
spectoflow status progress + whether the dashboard is running
|
|
45
|
+
spectoflow --version (-v) print the version
|
|
46
|
+
spectoflow --help (-h) show help
|
|
28
47
|
```
|
|
29
48
|
|
|
30
49
|
`init` scaffolds:
|
|
@@ -90,20 +109,34 @@ agent never clobber each other.
|
|
|
90
109
|
## Dashboard (real-time)
|
|
91
110
|
|
|
92
111
|
```bash
|
|
93
|
-
|
|
112
|
+
spectoflow dashboard # → http://localhost:4319 (or --port=NNNN)
|
|
113
|
+
# manual (no global install): node .spectoflow/dashboard/server.js
|
|
94
114
|
```
|
|
95
115
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
116
|
+
`spectoflow dashboard` is the simple way — it prints the URL and won't double-start (it detects a
|
|
117
|
+
dashboard already running on the port). `spectoflow status` tells you whether one is up. Running the
|
|
118
|
+
`server.js` directly still works for a manual/embedded setup. Zero dependencies, updates live via SSE +
|
|
119
|
+
file watching. Eight tabs behind a dense icon-tab header (brand logo · subtitle · a slim
|
|
120
|
+
global-progress meter · a sync dot · a **Run** quick-action · a **settings** gear): **Board** (the
|
|
121
|
+
control-room Overview — KPI cards, a status donut, a **scope-vs-delivered area curve**, a
|
|
99
122
|
workflow-at-a-glance strip, per-phase progress bars, filter chips + search — plus the phase board),
|
|
100
|
-
**Requests** (tasks awaiting you — `to_validate`/`to_analyze`), **
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
123
|
+
**Requests** (tasks awaiting you — `to_validate`/`to_analyze`), **Attention** (points the agent raised
|
|
124
|
+
via a `::spectoflow attention msg=…` sentinel or that you noted — edit/resolve/delete, or **validate →
|
|
125
|
+
task**), **Backlog** (a flat sortable/filterable, paginated table of every task, defaulting to open
|
|
126
|
+
work), **Workflow** (the pipeline as step cards — click one to enable/disable it, which edits
|
|
127
|
+
`workflow.md`), **Agents & Skills** (enriched cards that open a full-body markdown drawer), **Chat** (a
|
|
128
|
+
full-height group-chat panel), and **Info** (a project-at-a-glance summary). URLs are real routes
|
|
129
|
+
(`/board`, `/backlog/T-012`, …). Charts are zero-dep, hand-rolled SVG in `dashboard/public/charts.js`
|
|
130
|
+
(donut/area/bars/ring, animated, `prefers-reduced-motion`-aware), and every aggregate is computed
|
|
131
|
+
client-side.
|
|
132
|
+
|
|
133
|
+
**Designs & theme.** The dashboard ships **switchable designs** (Settings → *Dashboard design*):
|
|
134
|
+
**Control Room** (violet), **Obsidian Ops** (near-black lime/cyan, mono), and **Neon Command**
|
|
135
|
+
(glassmorphism aurora). Each works in light and dark (the moon toggle). A design is a `data-design`
|
|
136
|
+
skin — a scoped CSS token block plus a one-line entry in `dashboard/public/designs.js`, so adding one
|
|
137
|
+
is trivial. Fonts are **self-hosted** (`dashboard/public/fonts/*.woff2`), keeping the dashboard fully
|
|
138
|
+
offline and dependency-free. Your choice persists per viewer (localStorage) and as the project
|
|
139
|
+
default (`config.design`).
|
|
107
140
|
|
|
108
141
|
A floating **💬 chat widget** (bottom-right, redesigned) and the **Chat** tab render the same
|
|
109
142
|
`runtime.messages` log via a shared `renderChatLog()`, so they never drift. A running agent identifies
|
|
@@ -125,9 +158,14 @@ INVEST, Playwright E2E, Conventional Commits, and more — not generic one-liner
|
|
|
125
158
|
|
|
126
159
|
## Language
|
|
127
160
|
|
|
128
|
-
`.spectoflow/config.json` → `language` (default `en`, incl. code comments). Switchable
|
|
161
|
+
`.spectoflow/config.json` → `language` (default `en`, incl. code comments). Switchable from the CLI
|
|
162
|
+
(`config.json`) or the dashboard's **settings** gear (mode + language).
|
|
129
163
|
|
|
130
164
|
## Studied, not copied
|
|
131
165
|
|
|
132
166
|
Structure informed by spec-kit, OpenSpec (markdown + per-agent adapters), and BMAD (agent-personas).
|
|
133
|
-
spectoflow keeps the good ideas, removes the ceremony, and adds a real-time control plane.
|
|
167
|
+
spectoflow keeps the good ideas, removes the ceremony, and adds a real-time control plane.
|
|
168
|
+
|
|
169
|
+
## License & author
|
|
170
|
+
|
|
171
|
+
MIT © 2026 [Georges MOMO](https://github.com/georgesmomo).
|
package/bin/spectoflow.js
CHANGED
|
@@ -16,6 +16,11 @@ const VERSION = require('../package.json').version;
|
|
|
16
16
|
const argv = process.argv.slice(2);
|
|
17
17
|
const cmd = argv[0] || 'help';
|
|
18
18
|
|
|
19
|
+
// Tiny ANSI colouriser — no dependency; disabled when not a TTY or NO_COLOR is set.
|
|
20
|
+
const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
21
|
+
const paint = (code) => (s) => (useColor ? `\x1b[${code}m${s}\x1b[0m` : String(s));
|
|
22
|
+
const c = { g: paint('32'), cy: paint('36'), b: paint('34'), y: paint('33'), dim: paint('2'), bold: paint('1'), amber: paint('38;5;179') };
|
|
23
|
+
|
|
19
24
|
// ---- dashboard port + running-state probe ------------------------------------
|
|
20
25
|
// Precedence: --port=NNNN > SPECTOFLOW_PORT env > 4319 (matches templates/dashboard/server.js).
|
|
21
26
|
function resolvePort(args) {
|
|
@@ -156,18 +161,27 @@ function update() {
|
|
|
156
161
|
const r = require('../lib/update').runUpdate({ projectRoot: root, templatesDir: TPL, version: VERSION, dryRun });
|
|
157
162
|
|
|
158
163
|
const from = r.fromVersion || 'unknown';
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
console.log(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
164
|
+
const changed = r.refreshed.length + r.created.length + r.adopted.length + r.newSidecar.length;
|
|
165
|
+
const row = (sym, label, list, painter, note) => {
|
|
166
|
+
if (!list.length) return;
|
|
167
|
+
const n = c.dim(String(list.length).padStart(2));
|
|
168
|
+
const detail = note ? c.dim(note) : c.dim(list.slice(0, 6).join(', ') + (list.length > 6 ? ` +${list.length - 6} more` : ''));
|
|
169
|
+
console.log(` ${sym} ${painter(label.padEnd(9))} ${n} ${detail}`);
|
|
170
|
+
};
|
|
171
|
+
console.log('');
|
|
172
|
+
console.log(` ${c.bold('spectoflow update')} ${c.dim(from)} ${c.amber('→')} ${c.bold(r.toVersion)}${dryRun ? c.dim(' (dry-run)') : ''}`);
|
|
173
|
+
console.log('');
|
|
174
|
+
row(c.g('✓'), 'refreshed', r.refreshed, c.g);
|
|
175
|
+
row(c.cy('+'), 'created', r.created, c.cy);
|
|
176
|
+
row(c.b('~'), 'adopted', r.adopted, c.b);
|
|
177
|
+
row(c.y('!'), '.new', r.newSidecar, c.y, 'you edited these — new version saved as *.new, merge by hand');
|
|
178
|
+
if (r.unchanged.length) console.log(` ${c.dim('·')} ${c.dim('unchanged'.padEnd(9))} ${c.dim(String(r.unchanged.length).padStart(2))}`);
|
|
179
|
+
console.log(` ${c.dim('=')} ${c.dim('preserved'.padEnd(9))} ${c.dim('config.json · workflow.md · specs/ · plans/ · your custom agents & skills')}`);
|
|
180
|
+
console.log('');
|
|
181
|
+
if (dryRun) console.log(` ${c.dim('(dry-run — nothing was written)')}`);
|
|
182
|
+
else console.log(` ${changed ? c.g('✓ Done') : c.dim('Already up to date')}${changed ? c.dim(` · ${changed} file(s) changed`) : ''}`);
|
|
183
|
+
if (r.newSidecar.length && !dryRun) console.log(` ${c.y('→')} ${c.dim(`${r.newSidecar.length} *.new file(s) to review and merge`)}`);
|
|
184
|
+
console.log('');
|
|
171
185
|
}
|
|
172
186
|
|
|
173
187
|
// THE launch command — prints the URL clearly and won't crash on EADDRINUSE: it probes first
|
|
@@ -204,11 +218,26 @@ async function status() {
|
|
|
204
218
|
console.log(`dashboard: ${running ? `running → http://localhost:${port}` : 'not running'}`);
|
|
205
219
|
}
|
|
206
220
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
221
|
+
function version() { console.log(`spectoflow v${VERSION}`); }
|
|
222
|
+
|
|
223
|
+
const help = () => console.log(`${c.bold('spectoflow')} ${c.amber('v' + VERSION)} — agent-agnostic spec-driven development
|
|
224
|
+
|
|
225
|
+
${c.dim('Usage:')} spectoflow <command> [options]
|
|
226
|
+
|
|
227
|
+
${c.bold('Commands:')}
|
|
228
|
+
${c.g('init')} [dir] [--agent=claude,codex] scaffold a project (auto-detects installed agents)
|
|
229
|
+
${c.g('update')} [--dry-run] refresh framework files to this kit version
|
|
230
|
+
${c.g('dashboard')} [--port=NNNN] run the local control plane (default 4319, or $SPECTOFLOW_PORT)
|
|
231
|
+
${c.g('status')} print progress + whether the dashboard is running
|
|
232
|
+
|
|
233
|
+
${c.bold('Options:')}
|
|
234
|
+
-v, --version print the version
|
|
235
|
+
-h, --help show this help
|
|
236
|
+
|
|
237
|
+
${c.dim('Docs:')} https://github.com/georgesmomo/spectoflow`);
|
|
212
238
|
|
|
213
|
-
const fns = { init, update, dashboard, status, help };
|
|
214
|
-
|
|
239
|
+
const fns = { init, update, dashboard, status, help, version };
|
|
240
|
+
if (['-v', '-V', '--version', 'version'].includes(cmd)) version();
|
|
241
|
+
else if (['-h', '--help'].includes(cmd)) help();
|
|
242
|
+
else if (fns[cmd]) fns[cmd]();
|
|
243
|
+
else help();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spectoflow",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.2",
|
|
4
4
|
"description": "Agent-agnostic spec-driven development framework + real-time local control plane. Markdown artifacts, intent router, workflow-by-scope.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"spec-driven-development",
|
|
@@ -6,6 +6,8 @@ let backlogFilter = { status: 'open', q: '' }; // backlog defaults to open (not-
|
|
|
6
6
|
let backlogSort = { col: 'id', dir: 'asc' }; // backlog sort state — client-side only
|
|
7
7
|
let backlogPage = 1; const BACKLOG_PAGE = 25; // backlog pagination — client-side only
|
|
8
8
|
let attnFilter = 'open'; // attention tab filter — client-side only
|
|
9
|
+
// apply the persisted design skin as early as possible (before the first paint of app-driven DOM)
|
|
10
|
+
(function(){ try{ const d=localStorage.getItem('spf-design'); if(d) document.documentElement.setAttribute('data-design',d); }catch{} })();
|
|
9
11
|
|
|
10
12
|
const $ = (s,r=document)=>r.querySelector(s);
|
|
11
13
|
const $$ = (s,r=document)=>[...r.querySelectorAll(s)];
|
|
@@ -115,9 +117,7 @@ function flash(){ const s=$('#sync'); s.classList.add('saving'); $('#syncLabel')
|
|
|
115
117
|
function render(){
|
|
116
118
|
const c = P.config||{};
|
|
117
119
|
$('#projectName').textContent = c.projectType || 'project';
|
|
118
|
-
|
|
119
|
-
$('#langChip').textContent = c.language||'en';
|
|
120
|
-
$('#modeChip').textContent = c.mode||'semi';
|
|
120
|
+
const bv=$('#brandVer'); if(bv){ if(P.version){ bv.textContent='v'+P.version; bv.hidden=false; } else { bv.hidden=true; } }
|
|
121
121
|
$('#brandSub').textContent = (c.mode||'semi') + ' · ' + (c.language||'en');
|
|
122
122
|
// agent select — widget (#runAgent) and Chat tab (#tabRunAgent) each get their own populated
|
|
123
123
|
// <select>, since an id can't be shared by two elements; same option list, same source of truth.
|
|
@@ -133,7 +133,7 @@ function render(){
|
|
|
133
133
|
if(meter) meter.title=`Global progress: ${s.pct}% (${s.done}/${s.total} tasks)`;
|
|
134
134
|
renderOverview(); renderBoard(); renderBacklog(); renderWorkflow(); renderTeam();
|
|
135
135
|
renderChatLog($('#chatLog')); renderChatLog($('#chatTabLog'));
|
|
136
|
-
renderSidebar(); renderRequests(); renderAttention(); renderInfo();
|
|
136
|
+
renderSidebar(); renderRequests(); renderAttention(); renderInfo(); renderSettings();
|
|
137
137
|
applyActiveTab(); // re-apply the current tab so an SSE-driven re-render never resets to Board
|
|
138
138
|
}
|
|
139
139
|
|
|
@@ -202,8 +202,9 @@ function countUp(node){
|
|
|
202
202
|
requestAnimationFrame(tick);
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
function kpiCard(label,visual,sub){
|
|
205
|
+
function kpiCard(label,visual,sub,accent){
|
|
206
206
|
const c=el('div','kpi');
|
|
207
|
+
if(accent){ c.style.borderLeftColor=accent; c.classList.add('has-accent'); }
|
|
207
208
|
c.append(el('div','kpi-label',label));
|
|
208
209
|
const body=el('div','kpi-body'); body.append(visual); c.append(body);
|
|
209
210
|
if(sub) c.append(el('div','kpi-sub',sub));
|
|
@@ -228,15 +229,15 @@ function renderOverview(){
|
|
|
228
229
|
|
|
229
230
|
// KPI row
|
|
230
231
|
const kpis=el('div','kpi-row');
|
|
231
|
-
kpis.append(kpiCard('Global progress', ring(s.pct,72), `${s.done}/${s.total} tasks
|
|
232
|
-
kpis.append(kpiCard('In progress', numBlock(s.byStatus.in_progress||0,'var(--s-in_progress)'), 'tasks'));
|
|
233
|
-
kpis.append(kpiCard('To validate', numBlock(s.byStatus.to_validate||0,'var(--s-to_validate)'), 'awaiting review'));
|
|
232
|
+
kpis.append(kpiCard('Global progress', ring(s.pct,72), `${s.done}/${s.total} tasks`, cssv('--s-done')));
|
|
233
|
+
kpis.append(kpiCard('In progress', numBlock(s.byStatus.in_progress||0,'var(--s-in_progress)'), 'tasks', cssv('--s-in_progress')));
|
|
234
|
+
kpis.append(kpiCard('To validate', numBlock(s.byStatus.to_validate||0,'var(--s-to_validate)'), 'awaiting review', cssv('--s-to_validate')));
|
|
234
235
|
const r=s.running||{};
|
|
235
236
|
let runVal='—', runSub='no runs yet';
|
|
236
237
|
if(r.agents>0){ runVal=`${r.agents} running`; runSub='agents active'; }
|
|
237
238
|
else if(r.orchestration&&r.orchestration.status){ runVal=r.orchestration.status; runSub='orchestration'; }
|
|
238
239
|
else if(r.lastRun){ runVal=r.lastRun.status||'—'; runSub='last: '+(r.lastRun.tool||'—'); }
|
|
239
|
-
kpis.append(kpiCard('Running', numBlock(runVal, r.agents>0?'var(--signal)':'var(--muted)', true), runSub));
|
|
240
|
+
kpis.append(kpiCard('Running', numBlock(runVal, r.agents>0?'var(--signal)':'var(--muted)', true), runSub, cssv('--signal')));
|
|
240
241
|
box.append(kpis);
|
|
241
242
|
|
|
242
243
|
// Status donut + legend
|
|
@@ -469,40 +470,104 @@ function renderTask(t){
|
|
|
469
470
|
return c;
|
|
470
471
|
}
|
|
471
472
|
|
|
473
|
+
// Plain-language explanation of what each workflow step does, resolved from the step name — so the
|
|
474
|
+
// detail zone teaches the user what the pipeline is, not just that a step exists.
|
|
475
|
+
function wfDesc(s){
|
|
476
|
+
const n=String(s.name||'').toLowerCase();
|
|
477
|
+
if(/brainstorm|idea|intake/.test(n)) return 'Explore the request before committing to it — clarify the goal, constraints and success criteria, and shape a raw idea into something worth building.';
|
|
478
|
+
if(/analy/.test(n)) return 'Break the idea down: study the existing code and requirements, surface risks and ambiguities, and pin down clear, testable acceptance criteria.';
|
|
479
|
+
if(/spec/.test(n)) return 'Write the specification — the versioned, plain-markdown source of truth for what to build and why, before any code is written.';
|
|
480
|
+
if(/plan/.test(n)) return 'Turn the spec into an ordered plan of small, checkbox tasks — each one testable and executable on its own by a developer or an agent.';
|
|
481
|
+
if(/develop|implement|\bcode\b/.test(n)) return 'Implement the plan task by task, writing the code (and the tests that pin it down) to satisfy each checkbox.';
|
|
482
|
+
if(/integration/.test(n)) return 'Check that the pieces work together — modules, services and data paths across component boundaries, not just in isolation.';
|
|
483
|
+
if(/end.?to.?end|e2e/.test(n)) return 'Exercise the whole product the way a real user would, through the actual UI and end-to-end flows.';
|
|
484
|
+
if(/unit/.test(n)||/\btest/.test(n)) return 'Verify each unit in isolation — fast, focused tests that lock in behaviour and catch regressions early.';
|
|
485
|
+
if(/review/.test(n)) return 'A final quality pass — correctness, security and standards — before the work is considered done.';
|
|
486
|
+
if(/deploy|ship|release|publish/.test(n)) return 'Release the delivered work — ship it to its target environment.';
|
|
487
|
+
return 'A step in the delivery pipeline.';
|
|
488
|
+
}
|
|
489
|
+
let wfPopStep=null; // name of the step whose click-popover is open (null = closed)
|
|
490
|
+
|
|
491
|
+
// Workflow — a horizontal pipeline of representative icons with directional arrows. Clicking a step
|
|
492
|
+
// opens a floating popover (tooltip-style, anchored to the step) with what it does, its
|
|
493
|
+
// capability/agent/skill, and the enable/disable button. On narrow screens the pipeline reflows into
|
|
494
|
+
// a centered wrap (no horizontal scroll).
|
|
472
495
|
function renderWorkflow(){
|
|
473
496
|
const box=$('#wfDiagram'); box.innerHTML='';
|
|
474
497
|
const steps=P.workflow||[];
|
|
475
|
-
if(!steps.length){ box.append(el('div','empty','No workflow defined.')); return; }
|
|
498
|
+
if(!steps.length){ box.append(el('div','empty','No workflow defined.')); closeWfPop(); return; }
|
|
476
499
|
const enabledCount=steps.filter(s=>s.enabled).length;
|
|
477
500
|
const legend=el('div','wf-legend');
|
|
478
|
-
legend.append(el('span','wf-legend-
|
|
501
|
+
legend.append(el('span','wf-legend-dot'));
|
|
502
|
+
legend.append(el('span','wf-legend-txt',`${enabledCount} of ${steps.length} steps enabled — click a step to see what it does`));
|
|
479
503
|
box.append(legend);
|
|
480
|
-
const
|
|
504
|
+
const pipe=el('div','wf-pipeline');
|
|
481
505
|
steps.forEach((s,i)=>{
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
if(s.cap
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
toggle.append(el('span','wf-toggle-dot'));
|
|
497
|
-
toggle.append(el('span','wf-toggle-label',s.enabled?'enabled':'disabled'));
|
|
498
|
-
node.append(toggle);
|
|
499
|
-
const act=()=>toggleStep(s.name);
|
|
500
|
-
node.addEventListener('click',act);
|
|
501
|
-
node.addEventListener('keydown',e=>{ if(e.key==='Enter'||e.key===' '){ e.preventDefault(); act(); } });
|
|
502
|
-
track.append(node);
|
|
503
|
-
if(i<steps.length-1) track.append(el('div','wf-conn'+(s.enabled&&steps[i+1].enabled?'':' off')));
|
|
506
|
+
const step=el('div','wf-step2'+(s.enabled?' on':' off')+(s.name===wfPopStep?' is-selected':'')); step.style.setProperty('--i',i); step.dataset.idx=i;
|
|
507
|
+
step.tabIndex=0; step.setAttribute('role','button'); step.setAttribute('aria-expanded',String(s.name===wfPopStep));
|
|
508
|
+
step.title=s.name;
|
|
509
|
+
const circle=el('div','wf-circle');
|
|
510
|
+
circle.innerHTML=(typeof ICON!=='undefined'&&ICON.wf)?ICON.wf(s.name):'';
|
|
511
|
+
step.append(circle);
|
|
512
|
+
const cap=el('div','wf-caption'); cap.append(document.createTextNode(s.name));
|
|
513
|
+
if(s.optional) cap.append(el('span','wf-opt2','optional'));
|
|
514
|
+
step.append(cap);
|
|
515
|
+
const sel=(e)=>{ if(e) e.stopPropagation(); if(wfPopStep===s.name) closeWfPop(); else openWfPop(s.name); };
|
|
516
|
+
step.addEventListener('click',sel);
|
|
517
|
+
step.addEventListener('keydown',e=>{ if(e.key==='Enter'||e.key===' '){ e.preventDefault(); sel(e); } });
|
|
518
|
+
pipe.append(step);
|
|
519
|
+
if(i<steps.length-1) pipe.append(el('div','wf-link'+(s.enabled&&steps[i+1].enabled?' on':'')));
|
|
504
520
|
});
|
|
505
|
-
box.append(
|
|
521
|
+
box.append(pipe);
|
|
522
|
+
if(wfPopStep) renderWfPop(); // re-anchor / refresh an open popover after a live re-render
|
|
523
|
+
}
|
|
524
|
+
function wfDetailRow(k,v){ const r=el('div','wf-detail-row'); r.append(el('span','wf-detail-k',k), el('span','wf-detail-v',v)); return r; }
|
|
525
|
+
function wfPopFill(pop, s, idx){
|
|
526
|
+
const skill=(P.skills||[]).find(x=>x.name===s.skill);
|
|
527
|
+
const agent=(P.agents||[]).find(a=>a.capability===s.cap);
|
|
528
|
+
pop.innerHTML='';
|
|
529
|
+
const head=el('div','wf-detail-head');
|
|
530
|
+
head.append(el('span','wf-detail-num',String(idx+1)));
|
|
531
|
+
head.append(el('span','wf-detail-name',s.name));
|
|
532
|
+
head.append(el('span','wf-detail-status '+(s.enabled?'on':'off'), s.enabled?'enabled':'disabled'));
|
|
533
|
+
if(s.optional) head.append(el('span','wf-opt','optional'));
|
|
534
|
+
pop.append(head);
|
|
535
|
+
pop.append(el('p','wf-detail-desc', wfDesc(s)));
|
|
536
|
+
const grid=el('div','wf-detail-grid');
|
|
537
|
+
grid.append(wfDetailRow('Capability', s.cap||'—'));
|
|
538
|
+
grid.append(wfDetailRow('Handled by', agent?(agent.title||agent.name):'—'));
|
|
539
|
+
grid.append(wfDetailRow('Skill', s.skill||'—'));
|
|
540
|
+
if(skill&&skill.standard) grid.append(wfDetailRow('Standard', skill.standard));
|
|
541
|
+
if(skill&&(skill.inputs||skill.outputs)) grid.append(wfDetailRow('Flow', (skill.inputs||'—')+' → '+(skill.outputs||'—')));
|
|
542
|
+
pop.append(grid);
|
|
543
|
+
if(skill&&skill.description){ const sk=el('div','wf-detail-skill'); sk.append(el('b',null,'The “'+skill.name+'” skill — ')); sk.append(document.createTextNode(skill.description)); pop.append(sk); }
|
|
544
|
+
const btn=el('button','btn '+(s.enabled?'':'primary')+' wf-detail-btn', s.enabled?'Disable step':'Enable step');
|
|
545
|
+
btn.addEventListener('click',(e)=>{ e.stopPropagation(); toggleStep(s.name); });
|
|
546
|
+
const actions=el('div','wf-pop-actions'); actions.append(btn); pop.append(actions);
|
|
547
|
+
}
|
|
548
|
+
function openWfPop(name){ wfPopStep=name; renderWfPop(); }
|
|
549
|
+
function closeWfPop(){ wfPopStep=null; const p=$('#wfPop'); if(p) p.hidden=true; $$('#wfDiagram .wf-step2.is-selected').forEach(x=>x.classList.remove('is-selected')); }
|
|
550
|
+
function renderWfPop(){
|
|
551
|
+
const p=$('#wfPop'); if(!p) return;
|
|
552
|
+
const steps=P.workflow||[]; const idx=steps.findIndex(s=>s.name===wfPopStep);
|
|
553
|
+
if(idx<0){ closeWfPop(); return; }
|
|
554
|
+
const anchor=$('#wfDiagram .wf-step2[data-idx="'+idx+'"]');
|
|
555
|
+
if(!anchor){ p.hidden=true; return; }
|
|
556
|
+
$$('#wfDiagram .wf-step2.is-selected').forEach(x=>x.classList.remove('is-selected')); anchor.classList.add('is-selected');
|
|
557
|
+
wfPopFill(p, steps[idx], idx);
|
|
558
|
+
positionWfPop(anchor.querySelector('.wf-circle')||anchor, p);
|
|
559
|
+
}
|
|
560
|
+
function positionWfPop(anchorEl, pop){
|
|
561
|
+
const r=anchorEl.getBoundingClientRect();
|
|
562
|
+
pop.style.visibility='hidden'; pop.hidden=false; pop.classList.remove('above');
|
|
563
|
+
const pw=pop.offsetWidth, ph=pop.offsetHeight;
|
|
564
|
+
const left=Math.max(10, Math.min(r.left + r.width/2 - pw/2, window.innerWidth-pw-10));
|
|
565
|
+
let top=r.bottom + 12, above=false;
|
|
566
|
+
if(top + ph > window.innerHeight-10 && r.top - ph - 12 > 10){ top=r.top - ph - 12; above=true; }
|
|
567
|
+
pop.style.left=left+'px'; pop.style.top=top+'px';
|
|
568
|
+
pop.classList.toggle('above',above);
|
|
569
|
+
pop.style.setProperty('--caret-x', ((r.left + r.width/2) - left)+'px');
|
|
570
|
+
pop.style.visibility='';
|
|
506
571
|
}
|
|
507
572
|
|
|
508
573
|
// ---- Attention tab: agent/user-raised points; validate → real task ----------
|
|
@@ -552,29 +617,59 @@ async function patchAttn(id,patch){ flash(); await fetch('/api/attention/'+encod
|
|
|
552
617
|
async function deleteAttn(id){ flash(); await fetch('/api/attention/'+encodeURIComponent(id),{method:'DELETE'}); }
|
|
553
618
|
async function promoteAttn(id){ flash(); await fetch('/api/attention/'+encodeURIComponent(id)+'/promote',{method:'POST'}); }
|
|
554
619
|
|
|
555
|
-
// ---- Settings
|
|
556
|
-
function openSettings(open){
|
|
557
|
-
const pop=$('#settingsPop'); if(!pop) return;
|
|
558
|
-
if(open){ const c=P&&P.config||{}; if($('#setMode')) $('#setMode').value=c.mode||'semi'; setLangSelect(c.language||'en'); }
|
|
559
|
-
pop.hidden=!open;
|
|
560
|
-
}
|
|
620
|
+
// ---- Settings tab: change autonomy mode + output language (writes config.json) ----
|
|
561
621
|
function setLangSelect(lang){
|
|
562
622
|
const sel=$('#setLang'); if(!sel) return;
|
|
563
623
|
if(![...sel.options].some(o=>o.value===lang)){ const o=document.createElement('option'); o.value=lang; o.textContent=lang; sel.append(o); }
|
|
564
624
|
sel.value=lang;
|
|
565
625
|
}
|
|
566
|
-
|
|
567
|
-
|
|
626
|
+
// ---- design skins (data-design) — switchable, persisted per viewer + as the project default ----
|
|
627
|
+
function currentDesign(){ return document.documentElement.getAttribute('data-design')||'control-room'; }
|
|
628
|
+
function applyDesign(id){ document.documentElement.setAttribute('data-design',id); try{ localStorage.setItem('spf-design',id); }catch{} }
|
|
629
|
+
async function saveDesign(id){ applyDesign(id); if(P) render(); /* re-read token colours into the SVG charts */ flash(); try{ await fetch('/api/settings',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({design:id})}); }catch{} }
|
|
630
|
+
|
|
631
|
+
function renderSettings(){
|
|
632
|
+
const c=(P&&P.config)||{};
|
|
633
|
+
if($('#setMode')) $('#setMode').value=c.mode||'semi';
|
|
634
|
+
setLangSelect(c.language||'en');
|
|
635
|
+
// design switcher — options from the DESIGNS registry (designs.js)
|
|
636
|
+
const dsel=$('#setDesign');
|
|
637
|
+
if(dsel){
|
|
638
|
+
const designs=(typeof DESIGNS!=='undefined')?DESIGNS:[{id:'control-room',name:'Control Room'}];
|
|
639
|
+
if(dsel.options.length!==designs.length){ dsel.innerHTML=''; designs.forEach(d=>{ const o=document.createElement('option'); o.value=d.id; o.textContent=d.name; if(d.desc) o.title=d.desc; dsel.append(o); }); }
|
|
640
|
+
// reconcile: a per-viewer choice (localStorage) wins; else the project default (config.design)
|
|
641
|
+
let active; try{ active=localStorage.getItem('spf-design'); }catch{}
|
|
642
|
+
if(!active && c.design) active=c.design;
|
|
643
|
+
if(active && active!==currentDesign() && designs.some(d=>d.id===active)) document.documentElement.setAttribute('data-design',active);
|
|
644
|
+
dsel.value=currentDesign();
|
|
645
|
+
}
|
|
646
|
+
const box=$('#settingsReadonly');
|
|
647
|
+
if(box){
|
|
648
|
+
box.innerHTML='';
|
|
649
|
+
const rows=[['Active agent',c.agent||'—'],['Project type',c.projectType||'—'],
|
|
650
|
+
['Plans folder',c.plansDir||'plans'],['Specs folder',c.specsDir||'specs'],
|
|
651
|
+
['Framework version', P&&P.version?('v'+P.version):'—']];
|
|
652
|
+
rows.forEach(([k,v])=>{ const r=el('div','settings-ro-row'); r.append(el('span','settings-ro-k',k), el('span','settings-ro-v',String(v))); box.append(r); });
|
|
653
|
+
}
|
|
654
|
+
const fv=$('#footerVer'); if(fv) fv.textContent = (P&&P.version) ? ('v'+P.version) : '';
|
|
655
|
+
}
|
|
656
|
+
async function saveSettings(){
|
|
657
|
+
flash(); const mode=$('#setMode').value, language=$('#setLang').value;
|
|
658
|
+
await fetch('/api/settings',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({mode,language})});
|
|
659
|
+
const s=$('#settingsSaved'); if(s){ s.hidden=false; setTimeout(()=>{ s.hidden=true; },1500); }
|
|
660
|
+
}
|
|
568
661
|
|
|
569
662
|
// ---- client-side routing: /<tab>[/<taskId>] via the History API ------------
|
|
570
|
-
const ROUTES=['board','requests','attention','backlog','workflow','team','chat','info'];
|
|
663
|
+
const ROUTES=['board','requests','attention','backlog','workflow','team','chat','info','settings'];
|
|
571
664
|
function tabFromPath(){ const s=location.pathname.split('/').filter(Boolean); return ROUTES.includes(s[0])?s[0]:null; }
|
|
572
665
|
function taskFromPath(){ const s=location.pathname.split('/').filter(Boolean); return (ROUTES.includes(s[0])&&s[1])?decodeURIComponent(s[1]):null; }
|
|
573
666
|
function navigateTab(t,push){
|
|
574
667
|
activeTab=t; try{ localStorage.setItem('spf-tab',t); }catch{}
|
|
575
668
|
if(push!==false) history.pushState(null,'','/'+t);
|
|
576
669
|
applyActiveTab();
|
|
670
|
+
closeNav(); // a tab pick closes the mobile menu
|
|
577
671
|
}
|
|
672
|
+
function closeNav(){ document.body.classList.remove('nav-open'); const nt=$('#navToggle'); if(nt) nt.setAttribute('aria-expanded','false'); }
|
|
578
673
|
|
|
579
674
|
// chip row: a small uppercase kicker label followed by one chip per item — used for
|
|
580
675
|
// agent standards/uses and the skill standard.
|
|
@@ -804,6 +899,16 @@ function applyActiveTab(){
|
|
|
804
899
|
$$('.panel').forEach(p=> p.classList.toggle('is-active', p.dataset.panel===activeTab));
|
|
805
900
|
}
|
|
806
901
|
$$('#tabs .tab').forEach(tab=> tab.addEventListener('click',()=> navigateTab(tab.dataset.tab)));
|
|
902
|
+
// mobile hamburger — toggles the tab dropdown (body.nav-open); closes on tab pick / outside / Esc
|
|
903
|
+
const navToggle=$('#navToggle');
|
|
904
|
+
if(navToggle) navToggle.addEventListener('click',e=>{ e.stopPropagation(); const open=!document.body.classList.contains('nav-open'); document.body.classList.toggle('nav-open',open); navToggle.setAttribute('aria-expanded',String(open)); });
|
|
905
|
+
document.addEventListener('click',e=>{ if(!document.body.classList.contains('nav-open')) return; if(e.target.closest('#tabs')||e.target.closest('#navToggle')) return; closeNav(); });
|
|
906
|
+
document.addEventListener('keydown',e=>{ if(e.key==='Escape') closeNav(); });
|
|
907
|
+
// workflow step popover — close on outside click / scroll / resize / Esc
|
|
908
|
+
document.addEventListener('click',e=>{ if(wfPopStep && !e.target.closest('#wfPop') && !e.target.closest('.wf-step2')) closeWfPop(); });
|
|
909
|
+
document.addEventListener('keydown',e=>{ if(e.key==='Escape') closeWfPop(); });
|
|
910
|
+
window.addEventListener('scroll',()=>{ if(wfPopStep) closeWfPop(); },true);
|
|
911
|
+
window.addEventListener('resize',()=>{ if(wfPopStep) closeWfPop(); });
|
|
807
912
|
// keep the URL and the path in sync when the user uses the browser back/forward buttons
|
|
808
913
|
window.addEventListener('popstate',()=>{
|
|
809
914
|
activeTab = tabFromPath() || 'board'; applyActiveTab();
|
|
@@ -828,11 +933,12 @@ $$('#backlogTable thead th').forEach(th=> th.addEventListener('click', ()=>{
|
|
|
828
933
|
$('#attnAddBtn').addEventListener('click',()=>{ const t=$('#attnInput'); const v=t.value.trim(); if(v){ addAttn(v); t.value=''; } });
|
|
829
934
|
$('#attnInput').addEventListener('keydown',e=>{ if((e.metaKey||e.ctrlKey)&&e.key==='Enter'){ const v=e.target.value.trim(); if(v){ addAttn(v); e.target.value=''; } } });
|
|
830
935
|
$$('.attn-filters .fchip').forEach(b=> b.addEventListener('click',()=>{ attnFilter=b.dataset.attn; renderAttention(); }));
|
|
831
|
-
// settings
|
|
832
|
-
|
|
936
|
+
// settings — the footer link opens the Settings tab; selects save on change
|
|
937
|
+
const footerSettingsBtn=$('#footerSettings'); if(footerSettingsBtn) footerSettingsBtn.addEventListener('click',()=>navigateTab('settings'));
|
|
938
|
+
const footerLogo=$('.footer-logo'); if(footerLogo) footerLogo.addEventListener('click',e=>{ e.preventDefault(); navigateTab('board'); });
|
|
833
939
|
$('#setMode').addEventListener('change',saveSettings);
|
|
834
940
|
$('#setLang').addEventListener('change',saveSettings);
|
|
835
|
-
|
|
941
|
+
const setDesignSel=$('#setDesign'); if(setDesignSel) setDesignSel.addEventListener('change',()=>saveDesign(setDesignSel.value));
|
|
836
942
|
// theme
|
|
837
943
|
(function(){ const s=localStorage.getItem('spf-theme'); if(s)document.documentElement.setAttribute('data-theme',s);
|
|
838
944
|
$('#themeToggle').addEventListener('click',()=>{ const c=document.documentElement.getAttribute('data-theme'); const n=c==='dark'?'light':'dark'; document.documentElement.setAttribute('data-theme',n); localStorage.setItem('spf-theme',n); }); })();
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/*
|
|
3
|
+
* Dashboard design registry. Each entry is a selectable visual "skin" applied via the
|
|
4
|
+
* `data-design="<id>"` attribute on <html>. The switcher in Settings is built from this list.
|
|
5
|
+
*
|
|
6
|
+
* TO ADD A DESIGN (3 easy steps, no other code changes needed):
|
|
7
|
+
* 1. Append an entry here: { id, name, desc }. The id must be a slug (kebab-case).
|
|
8
|
+
* 2. In styles.css, add a scoped block that overrides the design tokens (and, if the design
|
|
9
|
+
* needs it, component rules) under that id:
|
|
10
|
+
* :root[data-design="<id>"] { --bg:…; --surface:…; --signal:…; … }
|
|
11
|
+
* :root[data-design="<id>"][data-theme="light"] { … light-mode overrides … }
|
|
12
|
+
* :root[data-design="<id>"] .kpi { … optional component tweaks … }
|
|
13
|
+
* Everything is token-driven, so most designs are just a palette override.
|
|
14
|
+
* 3. That's it — the design shows up in Settings → Dashboard design and can be switched live.
|
|
15
|
+
*
|
|
16
|
+
* The active design is persisted per viewer (localStorage 'spf-design') and, when changed, also
|
|
17
|
+
* written to config.json (config.design) as the project default. Fonts stay system-only (the
|
|
18
|
+
* dashboard is zero-dependency and offline-capable), so designs express themselves through colour,
|
|
19
|
+
* surfaces, radius, shadow and spacing rather than web fonts.
|
|
20
|
+
*/
|
|
21
|
+
(function (root) {
|
|
22
|
+
const DESIGNS = [
|
|
23
|
+
{ id: 'control-room', name: 'Control Room', desc: 'Dark violet engineering control room — the original.' },
|
|
24
|
+
{ id: 'obsidian', name: 'Obsidian Ops', desc: 'Near-black mission-control — electric lime + cyan, mono type, Linear/Vercel precision.' },
|
|
25
|
+
{ id: 'neon-command', name: 'Neon Command', desc: 'Glassmorphism — aurora violet + cyan, Space Grotesk display, control-room ambiance.' },
|
|
26
|
+
// one more from the design set coming: nord…
|
|
27
|
+
];
|
|
28
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = DESIGNS;
|
|
29
|
+
else root.DESIGNS = DESIGNS;
|
|
30
|
+
})(typeof window !== 'undefined' ? window : globalThis);
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -23,8 +23,39 @@
|
|
|
23
23
|
run: wrap('<path d="M5.4 3.6v10.8l9-5.4z" fill="currentColor" stroke="none"/>'),
|
|
24
24
|
// flag — points needing attention
|
|
25
25
|
attention: wrap('<line x1="4.5" y1="2.4" x2="4.5" y2="15.6"/><path d="M4.5 3.4h8.2l-1.7 2.6 1.7 2.6H4.5z"/>'),
|
|
26
|
-
// gear — settings
|
|
27
|
-
settings: wrap('<circle cx="9" cy="9" r="2.
|
|
26
|
+
// gear — settings (proper cog with teeth)
|
|
27
|
+
settings: wrap('<circle cx="9" cy="9" r="2.4"/><path d="M9 1.6v2.2M9 14.2v2.2M16.4 9h-2.2M3.8 9H1.6M14.2 3.8l-1.55 1.55M5.35 12.65 3.8 14.2M14.2 14.2l-1.55-1.55M5.35 5.35 3.8 3.8"/>'),
|
|
28
|
+
// crescent moon — theme toggle
|
|
29
|
+
theme: wrap('<path d="M14.6 10.8A5.6 5.6 0 0 1 7.2 3.4 5.7 5.7 0 1 0 14.6 10.8z"/>'),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Representative icons for workflow pipeline steps, resolved from the step name.
|
|
33
|
+
const WF = {
|
|
34
|
+
brainstorm: wrap('<path d="M6.4 12.2a4.3 4.3 0 1 1 5.2 0c-.5.4-.8.9-.9 1.5H7.3c-.1-.6-.4-1.1-.9-1.5z"/><line x1="7.2" y1="15.4" x2="10.8" y2="15.4"/>'),
|
|
35
|
+
analysis: wrap('<circle cx="7.8" cy="7.8" r="4"/><line x1="10.8" y1="10.8" x2="15" y2="15"/>'),
|
|
36
|
+
spec: wrap('<path d="M5 2.5h5l3 3v10H5z"/><path d="M10 2.5v3h3"/><line x1="6.8" y1="9" x2="11" y2="9"/><line x1="6.8" y1="11.5" x2="11" y2="11.5"/>'),
|
|
37
|
+
plan: wrap('<rect x="4" y="3.4" width="10" height="12" rx="1.4"/><path d="M6.8 3.4V3a1.1 1.1 0 0 1 1.1-1.1h2.2A1.1 1.1 0 0 1 11.2 3v.4"/><path d="M6.4 8.2 7.3 9 9 7"/><line x1="10.4" y1="8" x2="11.7" y2="8"/><line x1="6.4" y1="11.6" x2="11.7" y2="11.6"/>'),
|
|
38
|
+
develop: wrap('<path d="M6.4 5.6 3 9l3.4 3.4"/><path d="M11.6 5.6 15 9l-3.4 3.4"/>'),
|
|
39
|
+
unit: wrap('<circle cx="9" cy="9" r="6.2"/><path d="M6.2 9.2 8 11l3.9-4.1"/>'),
|
|
40
|
+
integration:wrap('<path d="M7.2 12.1 5.9 13.4a2.4 2.4 0 0 1-3.3-3.3L4 8.7"/><path d="M10.8 5.9 12.1 4.6a2.4 2.4 0 0 1 3.3 3.3L14 9.3"/><line x1="7.4" y1="10.6" x2="10.6" y2="7.4"/>'),
|
|
41
|
+
e2e: wrap('<rect x="2.8" y="3.6" width="12.4" height="8.2" rx="1.2"/><line x1="7" y1="15" x2="11" y2="15"/><line x1="9" y1="11.8" x2="9" y2="15"/><line x1="2.8" y1="6.4" x2="15.2" y2="6.4"/>'),
|
|
42
|
+
review: wrap('<path d="M2.4 9S5 4.6 9 4.6 15.6 9 15.6 9 13 13.4 9 13.4 2.4 9 2.4 9z"/><circle cx="9" cy="9" r="2.1"/>'),
|
|
43
|
+
deploy: wrap('<path d="M15.6 2.4 8.2 9.2"/><path d="M15.6 2.4 11.1 15.6 8.2 9.2 2.4 6.6z"/>'),
|
|
44
|
+
fallback: wrap('<circle cx="9" cy="9" r="3.4"/>'),
|
|
45
|
+
};
|
|
46
|
+
ICON.wf = function (name) {
|
|
47
|
+
const n = String(name || '').toLowerCase();
|
|
48
|
+
if (/brainstorm|idea/.test(n)) return WF.brainstorm;
|
|
49
|
+
if (/analy/.test(n)) return WF.analysis;
|
|
50
|
+
if (/spec/.test(n)) return WF.spec;
|
|
51
|
+
if (/plan/.test(n)) return WF.plan;
|
|
52
|
+
if (/develop|implement|\bcode\b|build/.test(n)) return WF.develop;
|
|
53
|
+
if (/integration/.test(n)) return WF.integration;
|
|
54
|
+
if (/end.?to.?end|e2e/.test(n)) return WF.e2e;
|
|
55
|
+
if (/unit/.test(n) || /\btest/.test(n)) return WF.unit;
|
|
56
|
+
if (/review/.test(n)) return WF.review;
|
|
57
|
+
if (/deploy|ship|release|publish/.test(n)) return WF.deploy;
|
|
58
|
+
return WF.fallback;
|
|
28
59
|
};
|
|
29
60
|
|
|
30
61
|
if (typeof module !== 'undefined' && module.exports) module.exports = ICON;
|