spectoflow 0.14.0 → 0.14.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/README.md
CHANGED
|
@@ -41,6 +41,7 @@ Every command works both ways — `spectoflow <cmd>` when installed globally, or
|
|
|
41
41
|
spectoflow init [dir] [--agent=claude,codex] scaffold a project (auto-detects installed agents)
|
|
42
42
|
spectoflow update [--dry-run] refresh framework files to this kit version
|
|
43
43
|
spectoflow dashboard [--port=NNNN] run the local control plane (default 4319)
|
|
44
|
+
spectoflow dashboard stop (or: stop) stop the running dashboard
|
|
44
45
|
spectoflow status progress + whether the dashboard is running
|
|
45
46
|
spectoflow --version (-v) print the version
|
|
46
47
|
spectoflow --help (-h) show help
|
package/bin/spectoflow.js
CHANGED
|
@@ -135,9 +135,9 @@ function init() {
|
|
|
135
135
|
|
|
136
136
|
// gitignore the volatile runtime
|
|
137
137
|
const gi = path.join(target, '.gitignore');
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
fs.appendFileSync(gi, (fs.existsSync(gi) ? '\n' : '') + line + '\n');
|
|
138
|
+
const giText = fs.existsSync(gi) ? fs.readFileSync(gi, 'utf8') : '';
|
|
139
|
+
for (const line of ['.spectoflow/runtime.json', '.spectoflow/.dashboard.lock']) {
|
|
140
|
+
if (!giText.includes(line)) fs.appendFileSync(gi, ((fs.existsSync(gi) && fs.readFileSync(gi, 'utf8').length) ? '\n' : '') + line + '\n');
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
console.log('spectoflow installed in', target);
|
|
@@ -187,6 +187,7 @@ function update() {
|
|
|
187
187
|
// THE launch command — prints the URL clearly and won't crash on EADDRINUSE: it probes first
|
|
188
188
|
// and, if a dashboard is already up on that port, just reports it instead of spawning a second one.
|
|
189
189
|
async function dashboard() {
|
|
190
|
+
if (argv[1] === 'stop' || argv.includes('stop')) return stopDashboard();
|
|
190
191
|
const port = resolvePort(argv);
|
|
191
192
|
const url = `http://localhost:${port}`;
|
|
192
193
|
if (await probeDashboard(port)) {
|
|
@@ -200,6 +201,30 @@ async function dashboard() {
|
|
|
200
201
|
console.log(`spectoflow dashboard → ${url}`);
|
|
201
202
|
}
|
|
202
203
|
|
|
204
|
+
// Stop the running dashboard: read the pidfile it wrote, verify it's actually up, then terminate it
|
|
205
|
+
// and clear the lock. Safe against a stale lock (a recycled pid) because it only kills when the port
|
|
206
|
+
// still responds.
|
|
207
|
+
async function stopDashboard() {
|
|
208
|
+
const root = process.cwd();
|
|
209
|
+
const lock = path.join(root, '.spectoflow', '.dashboard.lock');
|
|
210
|
+
let info = null;
|
|
211
|
+
try { info = JSON.parse(fs.readFileSync(lock, 'utf8')); } catch {}
|
|
212
|
+
const port = (info && info.port) || resolvePort(argv);
|
|
213
|
+
const running = await probeDashboard(port);
|
|
214
|
+
if (!running) {
|
|
215
|
+
if (info) { try { fs.unlinkSync(lock); } catch {} } // stale lock
|
|
216
|
+
return console.log('No spectoflow dashboard is running.');
|
|
217
|
+
}
|
|
218
|
+
if (info && info.pid) {
|
|
219
|
+
try {
|
|
220
|
+
process.kill(info.pid); // SIGTERM → server clears its own lock (POSIX)
|
|
221
|
+
try { fs.unlinkSync(lock); } catch {} // and we clear it too (Windows has no real signals)
|
|
222
|
+
return console.log(`spectoflow dashboard stopped (pid ${info.pid}, was on http://localhost:${port}).`);
|
|
223
|
+
} catch {}
|
|
224
|
+
}
|
|
225
|
+
console.log(`A dashboard is responding on http://localhost:${port} but isn't stoppable via the lock file — stop it where you launched it (Ctrl+C).`);
|
|
226
|
+
}
|
|
227
|
+
|
|
203
228
|
async function status() {
|
|
204
229
|
const root = process.cwd();
|
|
205
230
|
const cfg = store.readConfig(root);
|
|
@@ -228,6 +253,7 @@ ${c.bold('Commands:')}
|
|
|
228
253
|
${c.g('init')} [dir] [--agent=claude,codex] scaffold a project (auto-detects installed agents)
|
|
229
254
|
${c.g('update')} [--dry-run] refresh framework files to this kit version
|
|
230
255
|
${c.g('dashboard')} [--port=NNNN] run the local control plane (default 4319, or $SPECTOFLOW_PORT)
|
|
256
|
+
${c.g('dashboard stop')} stop the running dashboard (alias: ${c.g('stop')})
|
|
231
257
|
${c.g('status')} print progress + whether the dashboard is running
|
|
232
258
|
|
|
233
259
|
${c.bold('Options:')}
|
|
@@ -236,7 +262,7 @@ ${c.bold('Options:')}
|
|
|
236
262
|
|
|
237
263
|
${c.dim('Docs:')} https://github.com/georgesmomo/spectoflow`);
|
|
238
264
|
|
|
239
|
-
const fns = { init, update, dashboard, status, help, version };
|
|
265
|
+
const fns = { init, update, dashboard, stop: stopDashboard, status, help, version };
|
|
240
266
|
if (['-v', '-V', '--version', 'version'].includes(cmd)) version();
|
|
241
267
|
else if (['-h', '--help'].includes(cmd)) help();
|
|
242
268
|
else if (fns[cmd]) fns[cmd]();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spectoflow",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.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",
|
|
@@ -287,9 +287,15 @@ function renderOverview(){
|
|
|
287
287
|
if(!steps.length) strip.append(el('div','empty','No workflow defined.'));
|
|
288
288
|
box.append(ocard('Workflow at a glance', strip));
|
|
289
289
|
|
|
290
|
-
// Per-phase progress bars
|
|
291
|
-
|
|
292
|
-
|
|
290
|
+
// Per-phase progress bars — only phases that actually hold tasks (headings with no checkbox tasks
|
|
291
|
+
// are noise, not phases), and cap the list height with an internal scroll so a big project with
|
|
292
|
+
// dozens of phases can't blow up the overview.
|
|
293
|
+
const phaseRows=s.phases.filter(ph=>ph.total>0).map(ph=>({label:ph.title,pct:ph.pct,sub:`${ph.done}/${ph.total}`}));
|
|
294
|
+
if(phaseRows.length){
|
|
295
|
+
const barsEl=bars(phaseRows);
|
|
296
|
+
if(phaseRows.length>8) barsEl.classList.add('scroll-cap');
|
|
297
|
+
box.append(ocard(`Phase progress (${phaseRows.length})`, barsEl));
|
|
298
|
+
}
|
|
293
299
|
}
|
|
294
300
|
|
|
295
301
|
function taskMatches(t){
|
|
@@ -821,3 +821,6 @@ body.booting .wf-step2 { opacity:0; animation:rise .4s cubic-bezier(.2,.8,.2,1)
|
|
|
821
821
|
.mini-btn:hover { color:var(--ink); border-color:var(--cool); }
|
|
822
822
|
.main:has(.board.is-kanban) #phaseToggleAll { display:none; } /* phases are a List concept */
|
|
823
823
|
.kanban-col-body { max-height:64vh; overflow-y:auto; } /* keep columns compact on a big project */
|
|
824
|
+
|
|
825
|
+
/* Phase progress: cap the list so a big project (many phases) doesn't dominate the overview */
|
|
826
|
+
.bars-block.scroll-cap { max-height:340px; overflow-y:auto; padding-right:6px; }
|
|
@@ -222,4 +222,10 @@ const server = http.createServer(async (req,res)=>{
|
|
|
222
222
|
});
|
|
223
223
|
}catch(e){ sendJSON(res,500,{error:String(e&&e.message||e)}); }
|
|
224
224
|
});
|
|
225
|
-
|
|
225
|
+
// pidfile so `spectoflow dashboard stop` can find and stop this server; cleared on exit.
|
|
226
|
+
const LOCK = path.join(ROOT, '.spectoflow', '.dashboard.lock');
|
|
227
|
+
function writeLock(){ try{ fs.mkdirSync(path.dirname(LOCK),{recursive:true}); fs.writeFileSync(LOCK, JSON.stringify({ pid:process.pid, port:PORT, url:`http://localhost:${PORT}`, startedAt:new Date().toISOString() })+'\n'); }catch{} }
|
|
228
|
+
function clearLock(){ try{ const l=JSON.parse(fs.readFileSync(LOCK,'utf8')); if(l.pid===process.pid) fs.unlinkSync(LOCK); }catch{} }
|
|
229
|
+
process.on('exit', clearLock);
|
|
230
|
+
['SIGINT','SIGTERM'].forEach((s)=> process.on(s, ()=>{ clearLock(); process.exit(0); }));
|
|
231
|
+
server.listen(PORT,()=>{ writeLock(); console.log(`spectoflow · dashboard → http://localhost:${PORT}`); console.log(`project root: ${ROOT}`); });
|