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 line = '.spectoflow/runtime.json';
139
- if (!fs.existsSync(gi) || !fs.readFileSync(gi, 'utf8').includes(line)) {
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.0",
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
- const rows=s.phases.map(ph=>({label:ph.title,pct:ph.pct,sub:`${ph.done}/${ph.total}`}));
292
- box.append(ocard('Phase progress', bars(rows)));
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
- server.listen(PORT,()=>{ console.log(`spectoflow · dashboard → http://localhost:${PORT}`); console.log(`project root: ${ROOT}`); });
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}`); });