spectoflow 0.23.5 → 0.27.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/README.md +49 -16
- package/bin/postinstall.js +1 -0
- package/bin/spectoflow.js +226 -35
- package/lib/adapters.js +8 -2
- package/{templates/lib → lib}/custom-dashboard.js +2 -2
- package/{templates/lib → lib}/customize-prompts.js +1 -1
- package/lib/dashboard/connector.js +193 -0
- package/lib/dashboard/handlers.js +53 -0
- package/lib/{hub-server.js → dashboard/hub-server.js} +134 -51
- package/lib/dashboard/inject-design.js +41 -0
- package/lib/dashboard/meeting.js +116 -0
- package/lib/dashboard/ops.js +257 -0
- package/{templates → lib}/dashboard/orchestrator.js +17 -8
- package/{templates → lib}/dashboard/public/app.js +743 -124
- package/{templates → lib}/dashboard/public/charts.js +3 -3
- package/lib/dashboard/public/commands.js +77 -0
- package/{templates → lib}/dashboard/public/designs/console.css +7 -19
- package/{templates → lib}/dashboard/public/designs/orbit.css +3 -4
- package/{templates → lib}/dashboard/public/designs.js +2 -2
- package/lib/dashboard/public/fonts/bricolage-grotesque-400.woff2 +0 -0
- package/lib/dashboard/public/fonts/bricolage-grotesque-600.woff2 +0 -0
- package/lib/dashboard/public/fonts/bricolage-grotesque-700.woff2 +0 -0
- package/{templates → lib}/dashboard/public/hub.html +1 -1
- package/{templates → lib}/dashboard/public/hub.js +27 -4
- package/{templates → lib}/dashboard/public/i18n.js +60 -18
- package/{templates → lib}/dashboard/public/icons.js +2 -0
- package/{templates → lib}/dashboard/public/index.html +106 -13
- package/{templates → lib}/dashboard/public/styles.css +161 -26
- package/lib/dashboard/public/vendor/prism/prism-bash.min.js +1 -0
- package/lib/dashboard/public/vendor/prism/prism-c.min.js +1 -0
- package/lib/dashboard/public/vendor/prism/prism-clike.min.js +1 -0
- package/lib/dashboard/public/vendor/prism/prism-core.min.js +1 -0
- package/lib/dashboard/public/vendor/prism/prism-cpp.min.js +1 -0
- package/lib/dashboard/public/vendor/prism/prism-csharp.min.js +1 -0
- package/lib/dashboard/public/vendor/prism/prism-css.min.js +1 -0
- package/lib/dashboard/public/vendor/prism/prism-docker.min.js +1 -0
- package/lib/dashboard/public/vendor/prism/prism-go.min.js +1 -0
- package/lib/dashboard/public/vendor/prism/prism-java.min.js +1 -0
- package/lib/dashboard/public/vendor/prism/prism-json.min.js +1 -0
- package/lib/dashboard/public/vendor/prism/prism-kotlin.min.js +1 -0
- package/lib/dashboard/public/vendor/prism/prism-markdown.min.js +1 -0
- package/lib/dashboard/public/vendor/prism/prism-markup-templating.min.js +1 -0
- package/lib/dashboard/public/vendor/prism/prism-markup.min.js +1 -0
- package/lib/dashboard/public/vendor/prism/prism-php.min.js +1 -0
- package/lib/dashboard/public/vendor/prism/prism-python.min.js +1 -0
- package/lib/dashboard/public/vendor/prism/prism-ruby.min.js +1 -0
- package/lib/dashboard/public/vendor/prism/prism-rust.min.js +1 -0
- package/lib/dashboard/public/vendor/prism/prism-sql.min.js +1 -0
- package/lib/dashboard/public/vendor/prism/prism-swift.min.js +1 -0
- package/lib/dashboard/public/vendor/prism/prism-yaml.min.js +1 -0
- package/lib/dashboard/routes.js +38 -0
- package/{templates → lib}/dashboard/runner.js +13 -7
- package/{templates → lib}/dashboard/summarize.js +1 -1
- package/lib/detect.js +16 -1
- package/lib/global-config.js +65 -0
- package/lib/init.js +10 -4
- package/lib/registry.js +8 -8
- package/{templates/lib → lib}/store.js +16 -12
- package/lib/update.js +69 -1
- package/lib/workspace.js +119 -0
- package/package.json +3 -3
- package/templates/AGENTS.md +6 -6
- package/templates/README.md +4 -3
- package/templates/agents/framework-curator.md +11 -11
- package/templates/capabilities.md +1 -1
- package/templates/config.json +23 -0
- package/templates/dashboards/.gitkeep +0 -0
- package/templates/skills/generate-dashboard/SKILL.md +15 -13
- package/templates/dashboard/custom/.gitkeep +0 -3
- package/templates/dashboard/handlers.js +0 -251
- 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/server.js +0 -73
- package/templates/lib/agents-registry.js +0 -65
- /package/{templates → lib}/dashboard/files.js +0 -0
- /package/{templates → lib}/dashboard/public/designs/console.js +0 -0
- /package/{templates → lib}/dashboard/public/designs/orbit.js +0 -0
- /package/{templates → lib}/dashboard/public/fonts/ibm-plex-sans-400.woff2 +0 -0
- /package/{templates → lib}/dashboard/public/fonts/ibm-plex-sans-500.woff2 +0 -0
- /package/{templates → lib}/dashboard/public/fonts/ibm-plex-sans-600.woff2 +0 -0
- /package/{templates → lib}/dashboard/public/fonts/jetbrains-mono-400.woff2 +0 -0
- /package/{templates → lib}/dashboard/public/fonts/jetbrains-mono-500.woff2 +0 -0
- /package/{templates → lib}/dashboard/public/fonts/jetbrains-mono-700.woff2 +0 -0
- /package/{templates → lib}/dashboard/public/fonts/sora-400.woff2 +0 -0
- /package/{templates → lib}/dashboard/public/fonts/sora-600.woff2 +0 -0
- /package/{templates → lib}/dashboard/public/fonts/sora-700.woff2 +0 -0
- /package/{templates → lib}/dashboard/public/logo-dark.png +0 -0
- /package/{templates → lib}/dashboard/public/logo-white.png +0 -0
- /package/{templates → lib}/dashboard/public/stats.js +0 -0
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
13
13
|
<a href="https://www.npmjs.com/package/spectoflow"><img src="https://img.shields.io/npm/v/spectoflow.svg?color=e6a54b" alt="npm version"></a>
|
|
14
|
-
<img src="https://img.shields.io/badge/node-%3E%
|
|
14
|
+
<img src="https://img.shields.io/badge/node-%3E%3D22-5fb2cc" alt="node >= 22">
|
|
15
15
|
<img src="https://img.shields.io/badge/dependencies-0-4caf72" alt="zero dependencies">
|
|
16
16
|
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT license"></a>
|
|
17
17
|
</p>
|
|
@@ -87,6 +87,9 @@ spectoflow dashboard status is it running? (url + pid)
|
|
|
87
87
|
spectoflow dashboard stop (or: stop) stop the running dashboard
|
|
88
88
|
spectoflow dashboard restart stop then start
|
|
89
89
|
spectoflow dashboard create "..." | --auto generate a custom dashboard
|
|
90
|
+
spectoflow dashboard init [--path=<dir>] create/move the dashboard workspace (default ~/.spectoflow/dashboard)
|
|
91
|
+
spectoflow dashboard validate <file> check a custom-view JSON against the block schema
|
|
92
|
+
spectoflow config [get|set <key> [<value>]] global defaults + dashboard URL/path (~/.spectoflow/config.json)
|
|
90
93
|
|
|
91
94
|
spectoflow skill create "..." | --auto generate a project skill
|
|
92
95
|
spectoflow agent create "..." | --auto generate a project agent
|
|
@@ -101,7 +104,7 @@ spectoflow --help (-h) show help (append -h to any com
|
|
|
101
104
|
```
|
|
102
105
|
|
|
103
106
|
`init` scaffolds:
|
|
104
|
-
- `.spectoflow/` — the framework (brain, `workflow.md`, `agents/`, `skills/`, `policy.md`, `config.json`, dashboard,
|
|
107
|
+
- `.spectoflow/` — the framework (brain, `workflow.md`, `agents/`, `skills/`, `policy.md`, `config.json`, engine). The dashboard itself ships in the spectoflow package, not in your project.
|
|
105
108
|
- `specs/` and `plans/` — **markdown artifacts**, your versioned source of truth.
|
|
106
109
|
- per-agent shims: `CLAUDE.md` (Claude Code), `AGENTS.md` (Codex/Cursor), `GEMINI.md` (Gemini),
|
|
107
110
|
`.claude/commands/spectoflow.md`.
|
|
@@ -119,8 +122,9 @@ existing `plans/*.md` tasks are given stable ids.
|
|
|
119
122
|
## Update
|
|
120
123
|
|
|
121
124
|
`init` is idempotent (it never overwrites), so it can't refresh an installed project. `spectoflow
|
|
122
|
-
update` refreshes **framework-owned** files (engine,
|
|
123
|
-
|
|
125
|
+
update` refreshes **framework-owned** files (engine, `AGENTS.md`, `capabilities.md`, `policy.md`,
|
|
126
|
+
default agents & skills) to the CLI's version — retiring the project's own vendored dashboard folder
|
|
127
|
+
along the way for anyone updating from before v0.24 — while **preserving your work** —
|
|
124
128
|
`config.json`, `workflow.md`, `specs/`, `plans/`, and any agent/skill you created or edited are never
|
|
125
129
|
touched. A file you edited is preserved and its new version is written next to it as `<file>.new`
|
|
126
130
|
for you to merge by hand. Add `--dry-run` to preview, or `--force`/`-f` to overwrite a diverged file
|
|
@@ -128,6 +132,14 @@ in place instead of dropping a `.new` — use it once you're sure you have no lo
|
|
|
128
132
|
in that file (e.g. it's been stuck diverged since an earlier update); it still never touches
|
|
129
133
|
`config.json`, `workflow.md`, `specs/` or `plans/`.
|
|
130
134
|
|
|
135
|
+
**Coming from before v0.24?** The dashboard used to be vendored into every project; now it ships once
|
|
136
|
+
in the package. `npm install -g spectoflow@0.24`, then `spectoflow dashboard` anywhere — the dashboard
|
|
137
|
+
workspace is created automatically and any project you'd already registered is carried over. Your
|
|
138
|
+
project already opens in the new dashboard at this point, whether or not you've run `update` in it
|
|
139
|
+
yet; running `spectoflow update` inside it retires its now-obsolete `.spectoflow/dashboard/` folder
|
|
140
|
+
(only if you never edited anything in it — an edited file is kept and reported, never deleted) and
|
|
141
|
+
moves any custom dashboard views you generated to `.spectoflow/dashboards/`.
|
|
142
|
+
|
|
131
143
|
First refresh the kit, then run update from your project — the flow is the same whichever way you
|
|
132
144
|
installed:
|
|
133
145
|
|
|
@@ -167,35 +179,46 @@ agent never clobber each other.
|
|
|
167
179
|
|
|
168
180
|
```bash
|
|
169
181
|
spectoflow dashboard # → http://localhost:4319 (or --port=NNNN)
|
|
170
|
-
# manual (no global install): node
|
|
182
|
+
# manual (no global install): node /path/to/spectoflow/bin/spectoflow.js dashboard
|
|
171
183
|
```
|
|
172
184
|
|
|
173
185
|
`spectoflow dashboard` is the simple way — it prints the URL and won't double-start (it detects a
|
|
174
|
-
dashboard already running on the port). `spectoflow status` tells you whether one is up.
|
|
175
|
-
|
|
176
|
-
|
|
186
|
+
dashboard already running on the port). `spectoflow status` tells you whether one is up. Zero
|
|
187
|
+
dependencies, updates live via SSE + file watching.
|
|
188
|
+
|
|
189
|
+
Want your own hosted, online dashboard instead of (or alongside) the local one? `server/` is a
|
|
190
|
+
separate, self-hostable relay with a ready-made Docker deployment path — see
|
|
191
|
+
[`server/docs/deploy-vps.md`](server/docs/deploy-vps.md).
|
|
177
192
|
|
|
178
193
|
The header bar always shows the brand, the **active agent**, autonomy mode, language, a global-progress
|
|
179
|
-
meter, a sync dot, and a **Run** quick-action.
|
|
194
|
+
meter, a sync dot, and a **Run** quick-action. Thirteen tabs — and **which ones you see, and in what
|
|
195
|
+
order, is up to you** (Personalize → *Navigation tabs*: enable / disable / reorder; two of them ship
|
|
196
|
+
off by default):
|
|
180
197
|
|
|
181
198
|
- **Board** — the control-room Overview (compact KPI cards, a status donut, a **scope-vs-delivered
|
|
182
199
|
area curve**, a workflow-at-a-glance strip, per-phase progress bars, filter chips + search) plus the
|
|
183
|
-
phase board
|
|
184
|
-
|
|
200
|
+
phase board, as a **List** or a **Kanban** whose columns you can show/hide and that pages long
|
|
201
|
+
columns instead of scrolling them.
|
|
202
|
+
- **Chat** — a full-height group-chat panel with **Summarize** / **Clear** and **slash commands** (see
|
|
203
|
+
below).
|
|
185
204
|
- **Requests** — tasks awaiting you (`to_validate` / `to_analyze`).
|
|
186
205
|
- **Attention** — points the agent raised (a `::spectoflow attention msg=…` sentinel) or that you
|
|
187
206
|
noted yourself — edit / resolve / delete, or **validate → task**.
|
|
188
207
|
- **Backlog** — a flat sortable/filterable, paginated table of every task, defaulting to open work.
|
|
189
208
|
- **Workflow** — the pipeline as step cards; click one to enable/disable it, which edits `workflow.md`.
|
|
190
209
|
- **Agents & Skills** — enriched cards that open a full-body markdown drawer.
|
|
210
|
+
- **Files** — a project file tree with read / write / create, syntax-highlighted (self-hosted Prism.js).
|
|
211
|
+
- **Bloc note** *(off by default)* — a per-project post-it Markdown scratchpad.
|
|
212
|
+
- **Daily meeting** *(off by default)* — dated notes (`.spectoflow/meetings/<date>.md`), written by
|
|
213
|
+
hand or generated by the active agent.
|
|
191
214
|
- **Info** — a project-at-a-glance summary.
|
|
192
215
|
- **Documentation** — the live supported-agents table (your own install status + links) plus the CLI
|
|
193
216
|
command reference.
|
|
194
|
-
- **Personalize** — autonomy mode, language, design, the active agent,
|
|
195
|
-
*Customize* below).
|
|
217
|
+
- **Personalize** — autonomy mode, language, design, the active agent, **navigation tabs**, **slash
|
|
218
|
+
commands**, and **Extend spectoflow** (see *Customize* below).
|
|
196
219
|
|
|
197
220
|
URLs are real routes (`/board`, `/backlog/T-012`, …). Charts are zero-dep, hand-rolled SVG in
|
|
198
|
-
`dashboard/public/charts.js` (donut/area/bars/ring, animated, `prefers-reduced-motion`-aware), and
|
|
221
|
+
`lib/dashboard/public/charts.js` (donut/area/bars/ring, animated, `prefers-reduced-motion`-aware), and
|
|
199
222
|
every aggregate is computed client-side.
|
|
200
223
|
|
|
201
224
|
### Designs & theme
|
|
@@ -205,8 +228,8 @@ The dashboard ships **switchable designs** (Personalize → *Dashboard design*):
|
|
|
205
228
|
(violet), **Obsidian Ops** (near-black lime/cyan, mono), **Neon Command** (glassmorphism aurora), and
|
|
206
229
|
**Mission Control** (indigo control panel). Each works in light and dark (the moon toggle). A design
|
|
207
230
|
is a `data-design` skin — a scoped CSS token block plus a one-line entry in
|
|
208
|
-
`dashboard/public/designs.js`, so adding one is trivial. Fonts are **self-hosted**
|
|
209
|
-
(`dashboard/public/fonts/*.woff2`), keeping the dashboard fully offline and dependency-free. Your
|
|
231
|
+
`lib/dashboard/public/designs.js`, so adding one is trivial. Fonts are **self-hosted**
|
|
232
|
+
(`lib/dashboard/public/fonts/*.woff2`), keeping the dashboard fully offline and dependency-free. Your
|
|
210
233
|
choice persists per viewer (localStorage) and as the project default (`config.design`).
|
|
211
234
|
|
|
212
235
|
### Chat
|
|
@@ -221,6 +244,16 @@ drawer is served by the one read-only endpoint, `GET /api/agentfile?path=` (scop
|
|
|
221
244
|
`.spectoflow/agents/**` + `.spectoflow/skills/**`, path-traversal-safe) — the framework's only other
|
|
222
245
|
server surface is unchanged.
|
|
223
246
|
|
|
247
|
+
### Slash commands
|
|
248
|
+
|
|
249
|
+
Type `/` in either chat surface to open an autocomplete of reusable **prompt macros** — pick one,
|
|
250
|
+
type your text, send. Five ship built-in (`/spec`, `/plan`, `/revue`, `/resume`, `/rapport_jour`);
|
|
251
|
+
add your own from **Personalize → Commands** (a trigger, a short description, and the instruction the
|
|
252
|
+
agent receives — put `{{input}}` where your text should land, or it's appended). Expansion happens
|
|
253
|
+
**client-side**, so it's fully agent-agnostic: the agent never sees the `/`, it gets the full
|
|
254
|
+
instruction, while the chat log keeps the short `/rapport_jour focus bugs` you typed. Commands persist
|
|
255
|
+
in `config.json` (the DB online), so they survive a refresh and travel with the project.
|
|
256
|
+
|
|
224
257
|
### Customize
|
|
225
258
|
|
|
226
259
|
Personalize → **Extend spectoflow** lets you extend the project's own spectoflow install: add a
|
package/bin/postinstall.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
try {
|
|
10
10
|
if (process.env.npm_config_global === 'true' && process.stdout.isTTY) {
|
|
11
|
+
try { require('../lib/global-config').ensure(); } catch {}
|
|
11
12
|
const brand = require('../lib/brand');
|
|
12
13
|
const version = require('../package.json').version;
|
|
13
14
|
const useColor = !process.env.NO_COLOR;
|
package/bin/spectoflow.js
CHANGED
|
@@ -3,23 +3,30 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const http = require('http');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const readline = require('readline');
|
|
6
8
|
const { spawn } = require('child_process');
|
|
7
|
-
const store = require('../
|
|
9
|
+
const store = require('../lib/store');
|
|
8
10
|
const adapters = require('../lib/adapters');
|
|
9
11
|
const detect = require('../lib/detect');
|
|
10
12
|
const ownership = require('../lib/ownership');
|
|
11
13
|
const manifest = require('../lib/manifest');
|
|
12
14
|
const registry = require('../lib/registry');
|
|
15
|
+
const workspace = require('../lib/workspace');
|
|
13
16
|
const initLib = require('../lib/init');
|
|
17
|
+
const globalConfig = require('../lib/global-config');
|
|
14
18
|
const mcp = require('../lib/mcp');
|
|
15
|
-
const { startRun } = require('../
|
|
16
|
-
const { buildCustomizePrompt } = require('../
|
|
19
|
+
const { startRun } = require('../lib/dashboard/runner');
|
|
20
|
+
const { buildCustomizePrompt } = require('../lib/customize-prompts');
|
|
17
21
|
|
|
18
22
|
const KIT = path.resolve(__dirname, '..');
|
|
19
23
|
const TPL = path.join(KIT, 'templates');
|
|
20
24
|
const VERSION = require('../package.json').version;
|
|
21
25
|
const argv = process.argv.slice(2);
|
|
22
26
|
const cmd = argv[0] || 'help';
|
|
27
|
+
// `--name=value` flag reader — joins remaining `=`-split parts back (a URL's own query string may
|
|
28
|
+
// contain one), returns undefined when absent so callers can tell "not passed" from an empty string.
|
|
29
|
+
const flag = (name) => (argv.find((a) => a.startsWith(`--${name}=`)) || '').split('=').slice(1).join('=') || undefined;
|
|
23
30
|
|
|
24
31
|
// Tiny ANSI colouriser — no dependency; disabled when not a TTY or NO_COLOR is set.
|
|
25
32
|
const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
@@ -77,10 +84,13 @@ function readWorkflowSteps(dir) {
|
|
|
77
84
|
}
|
|
78
85
|
|
|
79
86
|
// ---- dashboard port + running-state probe ------------------------------------
|
|
80
|
-
// Precedence: --port=NNNN > SPECTOFLOW_PORT env >
|
|
87
|
+
// Precedence: --port=NNNN > SPECTOFLOW_PORT env > the workspace's own port (matches
|
|
88
|
+
// lib/dashboard/hub-server.js).
|
|
81
89
|
function resolvePort(args) {
|
|
82
90
|
const arg = (args.find((a) => a.startsWith('--port=')) || '').split('=')[1];
|
|
83
|
-
return Number(arg
|
|
91
|
+
if (arg) return Number(arg);
|
|
92
|
+
if (process.env.SPECTOFLOW_PORT) return Number(process.env.SPECTOFLOW_PORT);
|
|
93
|
+
try { return workspace.settings().port; } catch { return 4319; }
|
|
84
94
|
}
|
|
85
95
|
|
|
86
96
|
// Native http probe, ~500ms timeout, never throws — resolves true/false.
|
|
@@ -101,7 +111,7 @@ function init() {
|
|
|
101
111
|
const r = initLib.runInit({ target, templatesDir: TPL, version: VERSION, agentsArg });
|
|
102
112
|
console.log(logo());
|
|
103
113
|
console.log(`${c.g('✓')} installed in ${c.bold(r.target)}`);
|
|
104
|
-
console.log(` ${c.dim('.spectoflow/')} framework — brain, workflow, agents, skills, policy,
|
|
114
|
+
console.log(` ${c.dim('.spectoflow/')} framework — brain, workflow, agents, skills, policy, config`);
|
|
105
115
|
console.log(` ${c.dim('specs/ plans/')} markdown artifacts (your source of truth)`);
|
|
106
116
|
r.written.forEach((w) => console.log(` ${c.cy('+')} ${w}`));
|
|
107
117
|
r.notes.forEach((n) => console.log(` ${c.y('!')} ${c.dim(n)}`));
|
|
@@ -123,7 +133,7 @@ async function update() {
|
|
|
123
133
|
const r = require('../lib/update').runUpdate({ projectRoot: root, templatesDir: TPL, version: VERSION, dryRun, force });
|
|
124
134
|
|
|
125
135
|
const from = r.fromVersion || 'unknown';
|
|
126
|
-
const changed = r.refreshed.length + r.created.length + r.adopted.length + r.newSidecar.length + r.forced.length;
|
|
136
|
+
const changed = r.refreshed.length + r.created.length + r.adopted.length + r.newSidecar.length + r.forced.length + r.removed.length + r.migration.movedViews.length;
|
|
127
137
|
const row = (sym, label, list, painter, note) => {
|
|
128
138
|
if (!list.length) return;
|
|
129
139
|
const n = c.dim(String(list.length).padStart(2));
|
|
@@ -138,6 +148,11 @@ async function update() {
|
|
|
138
148
|
row(c.b('~'), 'adopted', r.adopted, c.b);
|
|
139
149
|
row(c.y('!'), 'forced', r.forced, c.y, 'overwrote a diverged file — its previous content is gone');
|
|
140
150
|
row(c.y('!'), '.new', r.newSidecar, c.y, 'you edited these — new version saved as *.new, merge by hand (or re-run with --force)');
|
|
151
|
+
row(c.dim('−'), 'removed', r.removed, c.dim, 'no longer part of the kit (the dashboard lives in the spectoflow package now)');
|
|
152
|
+
row(c.y('!'), 'kept', r.kept, c.y, 'you modified these and they are no longer part of the kit — delete them yourself when ready');
|
|
153
|
+
if (r.migration.movedViews.length) console.log(` ${c.cy('→')} ${c.cy('views'.padEnd(9))} ${c.dim(String(r.migration.movedViews.length).padStart(2))} ${c.dim('custom views moved to .spectoflow/dashboards/')}`);
|
|
154
|
+
r.migration.conflicts.forEach((f) => console.log(` ${c.y('!')} ${c.y('conflict'.padEnd(9))} ${c.dim(`dashboards/${f} already exists — the old copy stays in dashboard/custom/ for you to merge`)}`));
|
|
155
|
+
if (r.legacyLeftovers.length) console.log(` ${c.y('!')} ${c.dim('this project has no install manifest, so nothing was deleted. Safe to remove by hand: ' + r.legacyLeftovers.map((p) => '.spectoflow/' + p).join(', '))}`);
|
|
141
156
|
if (r.unchanged.length) console.log(` ${c.dim('·')} ${c.dim('unchanged'.padEnd(9))} ${c.dim(String(r.unchanged.length).padStart(2))}`);
|
|
142
157
|
console.log(` ${c.dim('=')} ${c.dim('preserved'.padEnd(9))} ${c.dim('config.json · workflow.md · specs/ · plans/ · your custom agents & skills')}`);
|
|
143
158
|
console.log('');
|
|
@@ -151,9 +166,7 @@ async function update() {
|
|
|
151
166
|
// is invalidated. Do that via a surgical per-project reload so an update always actually takes
|
|
152
167
|
// effect, without restarting the whole hub (which would disturb every other project open in it).
|
|
153
168
|
if (!dryRun && changed) {
|
|
154
|
-
const
|
|
155
|
-
let info = null;
|
|
156
|
-
try { info = JSON.parse(fs.readFileSync(lockPath, 'utf8')); } catch {}
|
|
169
|
+
const info = workspace.readLock();
|
|
157
170
|
if (info && info.port && (await probeDashboard(info.port, 2000))) {
|
|
158
171
|
const entry = registry.findByPath(root);
|
|
159
172
|
if (entry) {
|
|
@@ -161,7 +174,7 @@ async function update() {
|
|
|
161
174
|
const res = await fetch(`http://localhost:${info.port}/api/hub/reload/${entry.id}`, { method: 'POST' });
|
|
162
175
|
const body = await res.json().catch(() => ({}));
|
|
163
176
|
console.log(` ${c.dim(body.reloaded
|
|
164
|
-
? 'Hub is running — reloaded this project
|
|
177
|
+
? 'Hub is running — reloaded this project (other open projects unaffected).'
|
|
165
178
|
: 'Hub is running, but this project wasn\'t loaded in it yet — nothing to reload.')}`);
|
|
166
179
|
} catch {
|
|
167
180
|
console.log(` ${c.y('!')} Hub is running on port ${info.port} but the reload request failed — restart it yourself if changes don't seem to take effect: ${c.g('spectoflow dashboard restart')}`);
|
|
@@ -171,18 +184,158 @@ async function update() {
|
|
|
171
184
|
}
|
|
172
185
|
}
|
|
173
186
|
|
|
187
|
+
// `spectoflow dashboard validate <file>` — the generate-dashboard skill's verification step (it used
|
|
188
|
+
// to require() a lib file vendored in the project; the validator lives in this package now).
|
|
189
|
+
function validateDashboardFile(file) {
|
|
190
|
+
if (!file) { console.log('Usage: spectoflow dashboard validate <file.json>'); process.exitCode = 1; return; }
|
|
191
|
+
let spec;
|
|
192
|
+
try { spec = JSON.parse(fs.readFileSync(path.resolve(file), 'utf8')); }
|
|
193
|
+
catch (e) { console.log(`${c.y('invalid')} — cannot read/parse ${file}: ${e.message}`); process.exitCode = 1; return; }
|
|
194
|
+
const r = require('../lib/custom-dashboard').validateSpec(spec);
|
|
195
|
+
if (r.valid) { console.log(`${c.g('valid')} — ${spec.id} (${(spec.blocks || []).length} block(s))`); return; }
|
|
196
|
+
console.log(`${c.y('invalid')} — ${file}`);
|
|
197
|
+
(r.errors || []).forEach((e) => console.log(` ${c.y('!')} ${e}`));
|
|
198
|
+
process.exitCode = 1;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// `spectoflow dashboard init [--path <dir>] [--port N] [--name "…"] [--design <id>]` — create (or
|
|
202
|
+
// move) the dashboard workspace from the command line, mirroring what `startDashboard()` does lazily
|
|
203
|
+
// on first `spectoflow dashboard`, but explicit and inspectable.
|
|
204
|
+
function dashboardInit() {
|
|
205
|
+
try {
|
|
206
|
+
const r = workspace.init({ path: flag('path'), port: flag('port'), name: flag('name'), design: flag('design') });
|
|
207
|
+
console.log(logo());
|
|
208
|
+
console.log(`${c.g('✓')} dashboard workspace ${r.created ? 'created' : 'updated'} at ${c.bold(r.dir)}`);
|
|
209
|
+
if (r.registryCarried) console.log(` ${c.dim('your project list was carried over from the previous workspace')}`);
|
|
210
|
+
const s = workspace.settings();
|
|
211
|
+
console.log(` ${c.dim('name')} ${s.name} ${c.dim('port')} ${s.port} ${c.dim('design')} ${s.design}`);
|
|
212
|
+
console.log(`\n start it: ${c.g('spectoflow dashboard')}\n`);
|
|
213
|
+
} catch (e) { console.log(`${c.y('!')} ${e.message}`); process.exitCode = 1; }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function isLocalUrl(u) { try { return ['localhost', '127.0.0.1', '::1'].includes(new URL(u).hostname); } catch { return true; } }
|
|
217
|
+
// The one-time question every dashboard-starting command answers before doing anything else: which
|
|
218
|
+
// dashboard should this project talk to? `--url` answers it without prompting. Otherwise, only when
|
|
219
|
+
// dashboard.url has never been set AND both stdin/stdout are a real TTY do we ask (Enter keeps the
|
|
220
|
+
// local default) — any non-interactive invocation (tests spawn with stdio:'pipe', CI, scripts) must
|
|
221
|
+
// never block waiting on stdin that will never arrive, so it silently keeps the local default instead.
|
|
222
|
+
// The answer (typed, defaulted, or flagged) is saved via globalConfig.set so it is asked at most once.
|
|
223
|
+
// Returns the resolved URL on success, or null on validation error (error message already printed).
|
|
224
|
+
async function resolveDashboardUrl() {
|
|
225
|
+
const fromFlag = flag('url');
|
|
226
|
+
if (fromFlag) {
|
|
227
|
+
try {
|
|
228
|
+
globalConfig.set('dashboard.url', fromFlag);
|
|
229
|
+
} catch (e) {
|
|
230
|
+
console.log(`${c.y('!')} ${e.message}`);
|
|
231
|
+
process.exitCode = 1;
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
else if (globalConfig.get('dashboard.url').source === 'default') {
|
|
236
|
+
let answer = '';
|
|
237
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
238
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
239
|
+
answer = await new Promise((r) => rl.question(`Dashboard URL [${globalConfig.get('dashboard.url').value}]: `, (a) => { rl.close(); r(a.trim()); }));
|
|
240
|
+
}
|
|
241
|
+
globalConfig.set('dashboard.url', answer || globalConfig.get('dashboard.url').value);
|
|
242
|
+
}
|
|
243
|
+
const url = globalConfig.get('dashboard.url').value;
|
|
244
|
+
return url;
|
|
245
|
+
}
|
|
246
|
+
|
|
174
247
|
// THE launch command — routes the subcommands, then starts. Starting spawns the server DETACHED and
|
|
175
248
|
// hands the prompt straight back (no foreground blocking), then prints the commands to drive it.
|
|
176
249
|
async function dashboard() {
|
|
177
250
|
const sub = argv[1];
|
|
251
|
+
if (sub === 'init') return dashboardInit();
|
|
252
|
+
if (sub === 'login') return dashboardLogin();
|
|
253
|
+
if (sub === 'logout') return dashboardLogout();
|
|
254
|
+
if (sub === 'publish') return dashboardPublish(true);
|
|
255
|
+
if (sub === 'unpublish') return dashboardPublish(false);
|
|
178
256
|
if (sub === 'stop') return stopDashboard();
|
|
179
257
|
if (sub === 'status') return dashboardStatus();
|
|
180
258
|
if (sub === 'restart') return restartDashboard();
|
|
181
259
|
if (sub === 'create') return runCustomize('dashboard');
|
|
260
|
+
if (sub === 'validate') return validateDashboardFile(argv[2]);
|
|
182
261
|
return startDashboard();
|
|
183
262
|
}
|
|
184
263
|
|
|
185
|
-
// ----
|
|
264
|
+
// ---- online dashboard (C1): login / logout / publish / unpublish ----
|
|
265
|
+
// The machine token authenticates this machine to a hosted dashboard (server/). `login` checks it
|
|
266
|
+
// against POST /connector/whoami, stores it in the workspace's remote.json (0600) and points
|
|
267
|
+
// dashboard.url at the server; the hub — running or next started — opens the outbound connection.
|
|
268
|
+
async function fetchJSON(url, init = {}, timeoutMs = 10000) {
|
|
269
|
+
const res = await fetch(url, { ...init, signal: AbortSignal.timeout(timeoutMs) });
|
|
270
|
+
let data = {}; try { data = await res.json(); } catch {}
|
|
271
|
+
return { status: res.status, ok: res.ok, data };
|
|
272
|
+
}
|
|
273
|
+
// Best-effort call on the running local hub; null when no hub is up (the caller says "next start will").
|
|
274
|
+
async function hubCall(pathname, payload) {
|
|
275
|
+
const info = workspace.readLock();
|
|
276
|
+
if (!info || !info.port || !(await probeDashboard(info.port))) return null;
|
|
277
|
+
try { return await fetchJSON(`http://localhost:${info.port}${pathname}`, payload === undefined ? {} : { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); }
|
|
278
|
+
catch { return null; }
|
|
279
|
+
}
|
|
280
|
+
function fail(msg) { console.log(`${c.y('!')} ${msg}`); process.exitCode = 1; }
|
|
281
|
+
async function dashboardLogin() {
|
|
282
|
+
const url = flag('url'), token = flag('token'), transport = flag('transport') || 'ws';
|
|
283
|
+
if (!url || !token) { console.log(`Usage: spectoflow dashboard login --url=<https://…> --token=<spf_…> ${c.dim('[--name=<machine name>] [--transport=ws|http]')}`); process.exitCode = 1; return; }
|
|
284
|
+
let base;
|
|
285
|
+
try { base = new URL(url); if (!/^https?:$/.test(base.protocol)) throw new Error(); } catch { return fail('--url must be an http(s) URL, e.g. https://dashboard.example.com'); }
|
|
286
|
+
if (!['ws', 'http'].includes(transport)) return fail('--transport must be ws or http');
|
|
287
|
+
const clean = base.toString().replace(/\/+$/, '');
|
|
288
|
+
let who;
|
|
289
|
+
try { who = await fetchJSON(`${clean}/connector/whoami`, { method: 'POST', headers: { Authorization: `Bearer ${token}` } }); }
|
|
290
|
+
catch (e) { return fail(`could not reach ${clean} (${e.cause && e.cause.code ? e.cause.code : e.name === 'TimeoutError' ? 'timeout' : e.message})`); }
|
|
291
|
+
if (who.status === 401) return fail(`${clean} rejected this token — create one on the server: node cli.js token create --name="${os.hostname()}"`);
|
|
292
|
+
if (!who.ok) return fail(`${clean} answered HTTP ${who.status} — is that the spectoflow server?`);
|
|
293
|
+
const machineName = flag('name') || os.hostname();
|
|
294
|
+
workspace.migrateLegacyHome(); if (!workspace.exists()) workspace.init({});
|
|
295
|
+
workspace.writeRemote({ url: clean, token, machineName, transport });
|
|
296
|
+
globalConfig.set('dashboard.url', clean);
|
|
297
|
+
console.log(`${c.g('✓')} logged in to ${c.bold(clean)} as ${c.bold(machineName)} ${c.dim(`(machine ${who.data.machineId || '?'}, ${transport})`)}`);
|
|
298
|
+
const r = await hubCall('/api/hub/remote/reconnect', {});
|
|
299
|
+
console.log(` ${c.dim(r ? 'the running hub is connecting now' : 'the next `spectoflow dashboard` will connect')}`);
|
|
300
|
+
console.log(` publish a project from inside it: ${c.g('spectoflow dashboard publish')}`);
|
|
301
|
+
}
|
|
302
|
+
async function dashboardLogout() {
|
|
303
|
+
const had = workspace.clearRemote();
|
|
304
|
+
globalConfig.set('dashboard.url', `http://localhost:${resolvePort(argv)}`);
|
|
305
|
+
await hubCall('/api/hub/remote/reconnect', {});
|
|
306
|
+
console.log(had ? `${c.g('✓')} logged out — this machine no longer connects to an online dashboard` : `${c.dim('○')} not logged in to any online dashboard`);
|
|
307
|
+
}
|
|
308
|
+
async function dashboardPublish(published) {
|
|
309
|
+
let id = flag('id');
|
|
310
|
+
if (!id) {
|
|
311
|
+
const entry = registry.findByPath(process.cwd());
|
|
312
|
+
if (!entry) return fail(`this folder isn't registered in the dashboard yet — run ${c.g('spectoflow dashboard')} here first, or pass --id=<id> (see ${c.g('spectoflow projects')})`);
|
|
313
|
+
id = entry.id;
|
|
314
|
+
}
|
|
315
|
+
const meta = workspace.setPublished(id, published);
|
|
316
|
+
if (!meta) return fail(`no project registered with id ${id}`);
|
|
317
|
+
await hubCall(`/api/hub/projects/${encodeURIComponent(id)}/publish`, { published });
|
|
318
|
+
if (!published) return console.log(`${c.g('✓')} ${id} is no longer published`);
|
|
319
|
+
console.log(`${c.g('✓')} ${id} is published`);
|
|
320
|
+
const remote = workspace.readRemote();
|
|
321
|
+
if (remote) console.log(` online: ${c.bold(remote.url)}`);
|
|
322
|
+
else console.log(` ${c.dim('not logged in yet —')} ${c.g('spectoflow dashboard login --url=… --token=…')}`);
|
|
323
|
+
}
|
|
324
|
+
// The one-line "online" status under the local one — used by `dashboard` and `dashboard status`.
|
|
325
|
+
async function printOnlineLine(port, running) {
|
|
326
|
+
const remote = workspace.readRemote();
|
|
327
|
+
if (remote) {
|
|
328
|
+
let s = null;
|
|
329
|
+
if (running) { try { s = (await fetchJSON(`http://localhost:${port}/api/hub/remote`)).data; } catch {} }
|
|
330
|
+
if (s && s.connected) return console.log(`${c.g('●')} online → ${c.bold(remote.url)} ${c.dim(`(connected via ${s.transport}, machine "${remote.machineName}")`)}`);
|
|
331
|
+
const why = !running ? 'hub not running' : s && s.lastError ? `not connected: ${s.lastError}` : 'connecting…';
|
|
332
|
+
return console.log(`${c.dim('○')} online → ${c.bold(remote.url)} ${c.dim(`(${why})`)}`);
|
|
333
|
+
}
|
|
334
|
+
const url = globalConfig.get('dashboard.url').value;
|
|
335
|
+
if (!isLocalUrl(url)) console.log(`${c.dim('○')} online → ${url} ${c.dim('— not logged in:')} ${c.g('spectoflow dashboard login --url=… --token=…')}`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ---- projects: the multi-project registry's CLI surface (the workspace's projects.json) ----
|
|
186
339
|
function projectsCmd() {
|
|
187
340
|
const sub = argv[1];
|
|
188
341
|
if (sub === 'remove') return projectsRemove(argv[2]);
|
|
@@ -204,6 +357,20 @@ function projectsRemove(id) {
|
|
|
204
357
|
console.log(ok ? `${c.g('✓')} removed ${id}` : `${c.y('!')} no project registered with id ${id}`);
|
|
205
358
|
}
|
|
206
359
|
|
|
360
|
+
// ---- config: global settings (~/.spectoflow/config.json), editable from anywhere ----
|
|
361
|
+
function configCmd() {
|
|
362
|
+
const sub = argv[1];
|
|
363
|
+
try {
|
|
364
|
+
if (sub === 'get') { if (!argv[2]) throw new Error('Usage: spectoflow config get <key>'); console.log(globalConfig.get(argv[2]).value); return; }
|
|
365
|
+
if (sub === 'set') { if (!argv[2] || argv[3] === undefined) throw new Error('Usage: spectoflow config set <key> <value>'); const v = globalConfig.set(argv[2], argv[3]); console.log(`${c.g('✓')} ${argv[2]} = ${v}`); return; }
|
|
366
|
+
console.log(wordmark());
|
|
367
|
+
const rows = globalConfig.list();
|
|
368
|
+
const w = Math.max(...rows.map((r) => r.key.length));
|
|
369
|
+
rows.forEach((r) => console.log(` ${c.g(r.key.padEnd(w))} ${String(r.value).padEnd(28)} ${c.dim('(' + r.source + ')')}`));
|
|
370
|
+
console.log(c.dim(`\n file: ${globalConfig.configPath()} · spectoflow config set <key> <value>`));
|
|
371
|
+
} catch (e) { console.log(`${c.y('!')} ${e.message}`); process.exitCode = 1; }
|
|
372
|
+
}
|
|
373
|
+
|
|
207
374
|
// ---- Customize: `spectoflow skill/agent/dashboard create` — the CLI mirror of the dashboard's
|
|
208
375
|
// Settings → Customize UI. Both surfaces build the same natural-language prompt (customize-prompts.js)
|
|
209
376
|
// and post it through the same pipeline (runner.js's startRun — the function /api/run itself calls),
|
|
@@ -260,18 +427,21 @@ async function runCustomize(kind) {
|
|
|
260
427
|
// global registry first, then either joins an already-running hub or spawns a new one — probing first
|
|
261
428
|
// so a second start just reports the running one instead of spawning a duplicate.
|
|
262
429
|
async function startDashboard() {
|
|
430
|
+
if (!(await resolveDashboardUrl())) return;
|
|
263
431
|
const root = process.cwd();
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
432
|
+
workspace.migrateLegacyHome();
|
|
433
|
+
if (!workspace.exists()) workspace.init({});
|
|
434
|
+
const hasProject = fs.existsSync(path.join(root, '.spectoflow'));
|
|
435
|
+
const entry = hasProject ? workspace.registerProject(root) : null;
|
|
436
|
+
const boardUrl = (p) => (entry ? `http://localhost:${p}/p/${entry.id}/board` : `http://localhost:${p}/`);
|
|
437
|
+
const info = workspace.readLock();
|
|
269
438
|
if (info && info.port && await probeDashboard(info.port)) {
|
|
270
439
|
console.log(`${c.g('●')} hub already running → ${c.bold(boardUrl(info.port))}`);
|
|
440
|
+
await printOnlineLine(info.port, true);
|
|
271
441
|
return printDashboardCommands();
|
|
272
442
|
}
|
|
273
443
|
const port = resolvePort(argv);
|
|
274
|
-
const hubPath = path.join(KIT, 'lib', 'hub-server.js');
|
|
444
|
+
const hubPath = path.join(KIT, 'lib', 'dashboard', 'hub-server.js');
|
|
275
445
|
const env = Object.assign({}, process.env, { SPECTOFLOW_PORT: String(port) });
|
|
276
446
|
const child = spawn('node', [hubPath], { detached: true, stdio: 'ignore', env });
|
|
277
447
|
child.unref(); // let this CLI exit while the hub keeps running
|
|
@@ -281,6 +451,7 @@ async function startDashboard() {
|
|
|
281
451
|
for (let i = 0; i < 20 && !up; i++) { await new Promise((r) => setTimeout(r, 250)); up = await probeDashboard(port, 300); }
|
|
282
452
|
if (up) console.log(`${c.g('✓')} hub started → ${c.bold(boardUrl(port))} ${c.dim('(pid ' + child.pid + ')')}`);
|
|
283
453
|
else console.log(`${c.y('!')} spawned (pid ${child.pid}) but it isn't responding on http://localhost:${port} yet — check ${c.g('spectoflow dashboard status')} in a moment, or its own output if something's wrong.`);
|
|
454
|
+
await printOnlineLine(port, up);
|
|
284
455
|
printDashboardCommands();
|
|
285
456
|
}
|
|
286
457
|
|
|
@@ -293,16 +464,17 @@ function printDashboardCommands() {
|
|
|
293
464
|
}
|
|
294
465
|
|
|
295
466
|
async function dashboardStatus() {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
try { info = JSON.parse(fs.readFileSync(lockPath, 'utf8')); } catch {}
|
|
467
|
+
if (!(await resolveDashboardUrl())) return;
|
|
468
|
+
const info = workspace.readLock();
|
|
299
469
|
const port = (info && info.port) || resolvePort(argv);
|
|
300
470
|
const running = await probeDashboard(port);
|
|
301
471
|
if (running) console.log(`${c.g('●')} hub running → ${c.bold('http://localhost:' + port)}${info && info.pid ? c.dim(' (pid ' + info.pid + ')') : ''}`);
|
|
302
472
|
else console.log(`${c.dim('○')} hub not running`);
|
|
473
|
+
await printOnlineLine(port, running);
|
|
303
474
|
}
|
|
304
475
|
|
|
305
476
|
async function restartDashboard() {
|
|
477
|
+
if (!(await resolveDashboardUrl())) return;
|
|
306
478
|
await stopDashboard();
|
|
307
479
|
// Windows doesn't deliver real signals — process.kill() returns once the request is issued, not
|
|
308
480
|
// once the process (and the port it held) is actually gone. A short gap here, plus startDashboard()
|
|
@@ -315,20 +487,23 @@ async function restartDashboard() {
|
|
|
315
487
|
// Stop the running hub: read the global lock it wrote, verify it's actually up, then terminate it
|
|
316
488
|
// and clear the lock. Safe against a stale lock (a recycled pid) because it only kills when the port
|
|
317
489
|
// still responds.
|
|
490
|
+
function unlinkLocks() {
|
|
491
|
+
try { fs.unlinkSync(workspace.lockPath()); } catch {}
|
|
492
|
+
try { fs.unlinkSync(path.join(globalConfig.homeDir(), 'hub.lock')); } catch {}
|
|
493
|
+
}
|
|
318
494
|
async function stopDashboard() {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
try { info = JSON.parse(fs.readFileSync(lockPath, 'utf8')); } catch {}
|
|
495
|
+
if (!(await resolveDashboardUrl())) return;
|
|
496
|
+
const info = workspace.readLock();
|
|
322
497
|
const port = (info && info.port) || resolvePort(argv);
|
|
323
498
|
const running = await probeDashboard(port);
|
|
324
499
|
if (!running) {
|
|
325
|
-
if (info)
|
|
500
|
+
if (info) unlinkLocks(); // stale lock
|
|
326
501
|
return console.log('No spectoflow hub is running.');
|
|
327
502
|
}
|
|
328
503
|
if (info && info.pid) {
|
|
329
504
|
try {
|
|
330
505
|
process.kill(info.pid); // SIGTERM → hub clears its own lock (POSIX)
|
|
331
|
-
|
|
506
|
+
unlinkLocks(); // and we clear it too (Windows has no real signals)
|
|
332
507
|
return console.log(`spectoflow hub stopped (pid ${info.pid}, was on http://localhost:${port}).`);
|
|
333
508
|
} catch {}
|
|
334
509
|
}
|
|
@@ -348,9 +523,7 @@ async function status() {
|
|
|
348
523
|
console.log(`${(p.config && p.config.projectType) || 'project'} — mode ${p.config.mode} · lang ${p.config.language}`);
|
|
349
524
|
console.log(`${done}/${tasks.length} tasks done · ${p.specs.length} spec(s) · ${p.agents.length} agents · ${p.skills.length} skills`);
|
|
350
525
|
tasks.filter((t) => t.status === 'in_progress').forEach((t) => console.log(` > in progress: ${t.id} ${t.title}`));
|
|
351
|
-
const
|
|
352
|
-
let info = null;
|
|
353
|
-
try { info = JSON.parse(fs.readFileSync(lockPath, 'utf8')); } catch {}
|
|
526
|
+
const info = workspace.readLock();
|
|
354
527
|
const port = (info && info.port) || resolvePort(argv);
|
|
355
528
|
const running = await probeDashboard(port);
|
|
356
529
|
console.log(`dashboard: ${running ? `running → http://localhost:${port}` : 'not running'}`);
|
|
@@ -402,13 +575,16 @@ ${c.bold('Project')}
|
|
|
402
575
|
${c.g('init')} ${c.dim('[dir] [--agent=a,b]')} scaffold a project (auto-detects agents; wires Playwright MCP)
|
|
403
576
|
${c.g('update')} ${c.dim('[--dry-run|--force]')} refresh framework files to this kit version
|
|
404
577
|
${c.g('status')} progress + whether the dashboard is running
|
|
578
|
+
${c.g('config')} ${c.dim('[get <key>|set <key> <value>]')} global settings for every project (~/.spectoflow/config.json)
|
|
405
579
|
|
|
406
580
|
${c.bold('Dashboard')}
|
|
407
581
|
${c.g('dashboard')} ${c.dim('[--port=NNNN]')} start the control plane in the background (default 4319)
|
|
582
|
+
${c.g('dashboard init')} ${c.dim('[--path=<dir>] [--port=N] [--name=…]')} create/move the dashboard workspace ${c.dim('(default ~/.spectoflow/dashboard)')}
|
|
408
583
|
${c.g('dashboard status')} is it running? (url + pid)
|
|
409
584
|
${c.g('dashboard stop')} stop it ${c.dim('(alias: stop)')}
|
|
410
585
|
${c.g('dashboard restart')} stop then start
|
|
411
|
-
${c.g('
|
|
586
|
+
${c.g('dashboard login')} ${c.dim('--url=<u> --token=<t>')} connect this machine to an online dashboard ${c.dim('(logout · publish · unpublish)')}
|
|
587
|
+
${c.g('projects')} ${c.dim('[remove <id>]')} list every project seen so far
|
|
412
588
|
|
|
413
589
|
${c.bold('Customize')} ${c.dim('— same as Settings → Customize, from the terminal')}
|
|
414
590
|
${c.g('skill create')} ${c.dim('"<description>" | --auto')} generate a project skill
|
|
@@ -438,22 +614,30 @@ const HELP = {
|
|
|
438
614
|
${c.dim('An existing CLAUDE.md is preserved as CLAUDE.md.tomerge for you to merge on first run.')}
|
|
439
615
|
${c.dim('Full list with docs links: the dashboard\'s Documentation tab, or the README.')}`,
|
|
440
616
|
update: `${c.bold('spectoflow update')} ${c.dim('[--dry-run] [--force|-f]')}\n
|
|
441
|
-
Refresh framework-owned files (engine,
|
|
617
|
+
Refresh framework-owned files (engine, default agents & skills, AGENTS.md, policy…)
|
|
442
618
|
to this CLI's version, ${c.bold('preserving your work')}: config.json, workflow.md, specs/, plans/
|
|
443
619
|
and any agent/skill you edited are never overwritten (an edited file's new version lands as
|
|
444
620
|
${c.dim('*.new')} for you to merge). ${c.g('--dry-run')} previews without writing.
|
|
445
621
|
${c.g('--force')} (${c.g('-f')}) overwrites a diverged file in place instead of dropping a ${c.dim('*.new')}
|
|
446
622
|
— use it when you know you have no local edits worth keeping (e.g. a file stuck diverged from an
|
|
447
623
|
earlier update). It never touches config.json, workflow.md, specs/ or plans/.`,
|
|
448
|
-
dashboard: `${c.bold('spectoflow dashboard')} ${c.dim('[--port=NNNN] [status|stop|restart|create]')}\n
|
|
624
|
+
dashboard: `${c.bold('spectoflow dashboard')} ${c.dim('[--port=NNNN] [--url=<u>] [init|status|stop|restart|create|validate|login|logout|publish|unpublish]')}\n
|
|
449
625
|
Start the local control plane in the ${c.bold('background')} (default ${c.dim('4319')} or
|
|
450
|
-
${c.dim('$SPECTOFLOW_PORT')}) and hand the prompt back.
|
|
626
|
+
${c.dim('$SPECTOFLOW_PORT')}) and hand the prompt back. ${c.g('--url=<u>')} sets which dashboard this
|
|
627
|
+
project talks to (${c.dim('~/.spectoflow/config.json → dashboard.url')}) — asked once, interactively,
|
|
628
|
+
the first time you ever run a dashboard command; answer it up front with this flag instead. Subcommands:
|
|
629
|
+
${c.g('init')} create/move the dashboard workspace: ${c.dim('[--path=<dir>] [--port=N] [--name=…] [--design=<id>]')}
|
|
451
630
|
${c.g('status')} is it running? (url + pid)
|
|
452
631
|
${c.g('stop')} stop it ${c.dim('(alias: spectoflow stop)')}
|
|
453
632
|
${c.g('restart')} stop then start
|
|
454
|
-
${c.g('create')} generate a custom dashboard, e.g. ${c.dim('spectoflow dashboard create "..." --auto')}
|
|
633
|
+
${c.g('create')} generate a custom dashboard, e.g. ${c.dim('spectoflow dashboard create "..." --auto')}
|
|
634
|
+
${c.g('validate <file>')} check a custom-view JSON against the block schema
|
|
635
|
+
${c.g('login')} connect this machine to an online dashboard: ${c.dim('--url=<https://…> --token=<spf_…> [--name=<machine>] [--transport=ws|http]')}
|
|
636
|
+
${c.g('logout')} forget it (remote.json removed, dashboard.url back to local)
|
|
637
|
+
${c.g('publish')} make the current project (or ${c.dim('--id=<id>')}) visible online — nothing is published by default
|
|
638
|
+
${c.g('unpublish')} take it back offline`,
|
|
455
639
|
projects: `${c.bold('spectoflow projects')} ${c.dim('[remove <id>]')}\n
|
|
456
|
-
List every registered project in the global registry
|
|
640
|
+
List every registered project in the global registry (the dashboard workspace's projects.json) (stored by
|
|
457
641
|
${c.g('spectoflow dashboard')}) — id, name, path. ${c.g('remove <id>')} drops one (e.g. a project that moved
|
|
458
642
|
or was deleted) from this list only; it never touches that project's own files.`,
|
|
459
643
|
skill: `${c.bold('spectoflow skill create')} ${c.dim('"<description>" [--agent=name]')}\n${c.bold('spectoflow skill create')} ${c.dim('--auto [--agent=name]')}\n
|
|
@@ -475,6 +659,12 @@ const HELP = {
|
|
|
475
659
|
skills: `${c.bold('spectoflow skills')}\n List the evolving procedures (name · capability · what it does).`,
|
|
476
660
|
workflow: `${c.bold('spectoflow workflow')}\n Show the pipeline steps, marking which are enabled (●) or disabled (○).`,
|
|
477
661
|
stop: `${c.bold('spectoflow stop')}\n Stop the running dashboard (alias for ${c.g('spectoflow dashboard stop')}).`,
|
|
662
|
+
config: `${c.bold('spectoflow config')} ${c.dim('[get <key> | set <key> <value>]')}\n
|
|
663
|
+
Global settings that apply to every project on this machine, stored in ${c.dim('~/.spectoflow/config.json')}:
|
|
664
|
+
${c.g('dashboard.url')} the dashboard projects talk to (default http://localhost:4319)
|
|
665
|
+
${c.g('dashboard.path')} where the dashboard workspace lives (default ~/.spectoflow/dashboard)
|
|
666
|
+
${c.g('defaults.agent')} ${c.g('defaults.language')} ${c.g('defaults.mode')} ${c.g('defaults.design')} seeds for ${c.g('spectoflow init')}
|
|
667
|
+
A project's own .spectoflow/config.json always wins over these defaults.`,
|
|
478
668
|
};
|
|
479
669
|
const showHelp = (name) => console.log('\n' + HELP[name].trim() + '\n');
|
|
480
670
|
|
|
@@ -482,6 +672,7 @@ const showHelp = (name) => console.log('\n' + HELP[name].trim() + '\n');
|
|
|
482
672
|
const fns = {
|
|
483
673
|
init, update, dashboard, stop: stopDashboard, status, list: listAll, help, version,
|
|
484
674
|
projects: projectsCmd,
|
|
675
|
+
config: configCmd,
|
|
485
676
|
agents: () => { console.log(wordmark()); printAgents(false); },
|
|
486
677
|
skills: () => { console.log(wordmark()); printSkills(false); },
|
|
487
678
|
workflow: () => { console.log(wordmark()); printWorkflow(false); },
|
package/lib/adapters.js
CHANGED
|
@@ -26,7 +26,7 @@ reflect it back and ask **one targeted question at a time** (each with a recomme
|
|
|
26
26
|
is clear — then execute. See the Clarify reflex in \`.spectoflow/AGENTS.md\`.
|
|
27
27
|
|
|
28
28
|
- Command: \`/spectoflow\` (\`init\` / \`status\` / or just a request).
|
|
29
|
-
- Dashboard: \`
|
|
29
|
+
- Dashboard: \`spectoflow dashboard\` → http://localhost:4319
|
|
30
30
|
- Artifacts are markdown in \`specs/\` and \`plans/\`; volatile state in \`.spectoflow/runtime.json\`.
|
|
31
31
|
`;
|
|
32
32
|
|
|
@@ -254,4 +254,10 @@ function defaultRunners(agents) {
|
|
|
254
254
|
return out;
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
-
|
|
257
|
+
// The dashboard's flat view of REGISTRY (id/label/bin/dirs/runner/headless/docsUrl) — one roster for
|
|
258
|
+
// the CLI and the dashboard, now that the dashboard ships in this package (D64).
|
|
259
|
+
function knownAgents() {
|
|
260
|
+
return REGISTRY.map((a) => ({ id: a.id, label: a.label, bin: a.detect.bin, dirs: a.detect.dirs || [], runner: a.runner, headless: a.headless, docsUrl: a.docsUrl }));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
module.exports = { generate, defaultRunners, REGISTRY, knownAgents };
|