paneltir 0.8.0 → 0.10.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/CLAUDE_MD_SNIPPET.md +34 -5
- package/INSTALL.md +68 -12
- package/README.md +1 -1
- package/bin/paneltir.mjs +304 -5
- package/dist/index.d.ts +327 -9
- package/dist/index.js +870 -417
- package/dist/style.css +122 -0
- package/fingerprint.json +5 -4
- package/package.json +50 -3
- package/template/.claude/skills/panel/SKILL.md +70 -0
package/CLAUDE_MD_SNIPPET.md
CHANGED
|
@@ -31,6 +31,13 @@ library shared with other projects. Rules:
|
|
|
31
31
|
src/dashboard-theme.ts -->. Colour changes go there, never in the kit. The
|
|
32
32
|
dashboard's **shape** is a `DashboardThemeForm` passed to the same provider
|
|
33
33
|
(`panel` or `ledger`); it too is this project's choice, not the kit's.
|
|
34
|
+
- **This project's theme goes to `PanelApp` as `extraThemes`, never as
|
|
35
|
+
`themes`.** `extraThemes` offers it alongside the four the kit ships;
|
|
36
|
+
`themes` means "exactly these and nothing else", so passing one theme there
|
|
37
|
+
deletes all four and the settings sheet ends up with a single row. That is
|
|
38
|
+
not hypothetical — it happened to two projects before the distinction
|
|
39
|
+
existed, and it looks like the kit having no themes rather than like a prop
|
|
40
|
+
being wrong.
|
|
34
41
|
- **The panel's gate is this project's, not the library's.** `middleware.ts`,
|
|
35
42
|
`lib/session.ts` and `api/login.ts` / `api/logout.ts` / `api/panel-state.ts`
|
|
36
43
|
were copied here by `npx paneltir init`. They hold this project's password
|
|
@@ -38,12 +45,34 @@ library shared with other projects. Rules:
|
|
|
38
45
|
change when the kit updates, and `paneltir init` will not overwrite them.
|
|
39
46
|
The password, the GitHub token and the target repository are environment
|
|
40
47
|
variables where this project is hosted — never in this repository.
|
|
48
|
+
- **Those copied files do not update, so they have to be asked.** Run
|
|
49
|
+
`npx paneltir doctor` when the panel misbehaves for no visible reason: it
|
|
50
|
+
compares each copied file against the version installed now and separates
|
|
51
|
+
the three answers that need different actions — you edited it, the template
|
|
52
|
+
moved underneath you, or both. The second is the one no `git diff` can find,
|
|
53
|
+
because there is nothing in this project's history to diff against, and it is
|
|
54
|
+
how a fix released upstream stayed out of a project for two releases.
|
|
41
55
|
- **The panel explains itself once.** New readers get a short guided tour the
|
|
42
|
-
first time they open the board; each note is dismissed and stays dismissed
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
56
|
+
first time they open the board; each note is dismissed and stays dismissed.
|
|
57
|
+
**Help** in the header reports what the panel needs — the password, the
|
|
58
|
+
GitHub token, the target repository — with what is missing and the steps to
|
|
59
|
+
fix it, asked from the server so it reports the truth rather than a guess.
|
|
60
|
+
"Show the guide again" in there brings the tour back.
|
|
61
|
+
- **A panel nobody has used yet walks its own setup.** On a board with no
|
|
62
|
+
cards and no runs, the panel asks the server what is configured and, if
|
|
63
|
+
something is missing, opens a walkthrough that does one variable at a time
|
|
64
|
+
and checks each rather than asking anyone to tick it. Closing it is
|
|
65
|
+
remembered in this browser, because the board it would otherwise be written
|
|
66
|
+
to is the very thing that cannot be saved yet. On a board already in use it
|
|
67
|
+
is a banner instead — said before a save fails rather than after — and the
|
|
68
|
+
walkthrough waits behind **Help**. Its last screen carries the one thing no
|
|
69
|
+
screen can check for you: `/admin` and its bundle must both refuse a signed
|
|
70
|
+
out visitor.
|
|
71
|
+
- **And it can hand the problem to Claude.** The walkthrough carries a line to
|
|
72
|
+
paste into a session in this repository. It names
|
|
73
|
+
`.claude/skills/panel/SKILL.md`, which `paneltir init` wrote, and that file
|
|
74
|
+
covers every variable the panel checks for — held together by a test, so the
|
|
75
|
+
prompt cannot come to point at a document that no longer answers it.
|
|
47
76
|
- **Say which version the dashboard runs on.** `paneltirBuild.short` from
|
|
48
77
|
`paneltir` renders as `vX.Y.Z · <first 12 of the fingerprint>`; keep it visible somewhere in
|
|
49
78
|
the panel. `npx paneltir version` answers the same question from a terminal,
|
package/INSTALL.md
CHANGED
|
@@ -19,7 +19,7 @@ identity** — never another project's.
|
|
|
19
19
|
npm install paneltir
|
|
20
20
|
|
|
21
21
|
# or, without npm access, straight from the repository:
|
|
22
|
-
# npm install github:daifukus/paneltir#v0.
|
|
22
|
+
# npm install github:daifukus/paneltir#v0.10.0
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
(Use the tag you were given; never install without pinning a version.)
|
|
@@ -39,10 +39,11 @@ identity** — never another project's.
|
|
|
39
39
|
not another project's theme.
|
|
40
40
|
|
|
41
41
|
3. **If no palette is defined yet**, do not make up values: `paneltir` ships
|
|
42
|
-
base themes that are already designed and tested
|
|
43
|
-
`
|
|
44
|
-
which one fits the project best
|
|
45
|
-
now
|
|
42
|
+
base themes that are already designed and tested — `midnightTheme`,
|
|
43
|
+
`oldMoneyTheme`, `cyberpunkTheme` and `claudeTheme`, the only light one, all
|
|
44
|
+
exported in `THEME_PRESETS`. Ask which one fits the project best, and if the
|
|
45
|
+
answer is not available right now use `midnightTheme`, the most neutral one,
|
|
46
|
+
and say so explicitly.
|
|
46
47
|
Store the decision in a new file (for example `src/dashboard-theme.ts`),
|
|
47
48
|
re-exporting the chosen preset, or copying its values if they are expected
|
|
48
49
|
to diverge:
|
|
@@ -138,18 +139,33 @@ identity** — never another project's.
|
|
|
138
139
|
import 'paneltir/style.css'
|
|
139
140
|
import { PanelApp } from 'paneltir'
|
|
140
141
|
import board from './data/panel-state.json'
|
|
141
|
-
|
|
142
|
-
|
|
142
|
+
import { dashboardTheme } from './dashboard-theme'
|
|
143
|
+
|
|
144
|
+
<PanelApp
|
|
145
|
+
state={board}
|
|
146
|
+
saveEndpoint="/admin/api/panel-state"
|
|
147
|
+
showHistory
|
|
148
|
+
// This project's identity, offered *alongside* the kit's presets. Use
|
|
149
|
+
// `extraThemes` and never `themes`: `themes` is the exact set, so passing
|
|
150
|
+
// one theme there deletes all four presets and the settings sheet ends up
|
|
151
|
+
// with a single row. That happened to two real projects.
|
|
152
|
+
extraThemes={{
|
|
153
|
+
thisProject: { label: 'This Project', tokens: dashboardTheme, note: 'derived from this repository' },
|
|
154
|
+
}}
|
|
155
|
+
/>
|
|
143
156
|
```
|
|
144
157
|
|
|
145
158
|
That is the whole page. `PanelApp` draws the board, the analysis, the card
|
|
146
|
-
sheet, the settings, the guide and the notifications, and
|
|
147
|
-
|
|
159
|
+
sheet, the settings, the guide and the notifications, and with no themes
|
|
160
|
+
passed at all it offers the kit's four. Everything a project wants
|
|
148
161
|
differently is a prop rather than a fork:
|
|
149
162
|
|
|
150
163
|
| Prop | What it changes |
|
|
151
164
|
| --- | --- |
|
|
152
|
-
| `
|
|
165
|
+
| `extraThemes` | This project's own identities, offered **as well as** the kit's presets. Theirs come first, so the panel opens in one of them. This is the one you want. |
|
|
166
|
+
| `themes` | The exact set: these and nothing else, presets included. Only for a caller deliberately curating the list — it replaces rather than adds. |
|
|
167
|
+
| `defaultTheme` | Which one to draw before the board has said. Defaults to the first key. |
|
|
168
|
+
| `gate` | `.paneltir/gate.json` from this project, imported. With it the setup list gains a row saying whether the files `init` copied are still the ones this version ships; without it the panel says nothing about them rather than guessing. |
|
|
153
169
|
| `brand` | What sits where a name goes in the header. Defaults to the board's `project`. |
|
|
154
170
|
| `capabilities`, `extras` | Sections of the caller's own. Not drawn when absent. |
|
|
155
171
|
| `links`, `footer`, `decoration`, `className` | Where else to go, what sits under the panel, and anything the caller draws behind it. |
|
|
@@ -169,19 +185,59 @@ identity** — never another project's.
|
|
|
169
185
|
| `PANEL_FILE` | Where the board lives in it, if not `src/data/panel-state.json`. |
|
|
170
186
|
| `SESSION_SECRET` | Optional. Defaults to `ADMIN_PASSWORD`, so changing the password ends every session already issued. |
|
|
171
187
|
|
|
188
|
+
None of that has to be done from a table. Open the panel on a board nobody
|
|
189
|
+
has worked yet and it walks the variables one at a time, checking each
|
|
190
|
+
against the server rather than asking anyone to tick it off — and if a step
|
|
191
|
+
does not land, it carries a line to paste into a Claude session in this
|
|
192
|
+
repository, which reads the skill `init` just wrote and takes it from
|
|
193
|
+
there. On a board already in use the same walkthrough is behind **Help**,
|
|
194
|
+
offered only while something is actually missing.
|
|
195
|
+
|
|
196
|
+
**The files `init` copied do not update, so ask them.** Everything under
|
|
197
|
+
`api/`, `lib/` and `middleware.ts` is this project's from the moment it is
|
|
198
|
+
copied — that is the point, since it holds this project's password rule —
|
|
199
|
+
and the cost is that a fix released upstream can be missing here while every
|
|
200
|
+
version number on screen looks current. It has happened: the `HTTP 500` on
|
|
201
|
+
saving was fixed in `template/api/panel-state.ts`, and projects installed
|
|
202
|
+
before that still had it.
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
npx paneltir doctor
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Commit `.paneltir/gate.json`.** `init` writes it to record what it copied
|
|
209
|
+
and what it copied it from, and it is provenance rather than a build
|
|
210
|
+
artifact: ignored by git it disappears on the next clone and every answer
|
|
211
|
+
below degrades to "differs", which is the one that cannot say who moved.
|
|
212
|
+
|
|
213
|
+
It separates the three answers that need different actions — *yours* (you
|
|
214
|
+
edited it), *BEHIND* (you did not touch it and the template moved), *BOTH* —
|
|
215
|
+
and the middle one is invisible to `git diff`, because this project's history
|
|
216
|
+
has nothing to compare against. `init --force` replaces the gate and never
|
|
217
|
+
the board; read the diff first where a file is marked BOTH, or the edit is
|
|
218
|
+
lost.
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
npx paneltir run "what this pass did" --cards=a,b
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
appends an entry to the board's `runs` so the record does not depend on
|
|
225
|
+
anyone remembering at the end of a session. It writes the file and commits
|
|
226
|
+
nothing.
|
|
227
|
+
|
|
172
228
|
Two things to check before trusting it: `/admin` signed out must land on
|
|
173
229
|
`/login`, and **the panel's own JavaScript bundle must answer 401 signed
|
|
174
230
|
out**. If it does not, point the last entry of the matcher in
|
|
175
231
|
`middleware.ts` at wherever this project's build emits it — protecting the
|
|
176
232
|
page but not the bundle leaves the board readable by anyone who opens the
|
|
177
|
-
HTML.
|
|
233
|
+
HTML. The walkthrough's last screen says both, for the same reason.
|
|
178
234
|
|
|
179
235
|
9. **Record the kit fingerprint.** The kit reports itself, so no one has to
|
|
180
236
|
read `node_modules` to find out what is installed:
|
|
181
237
|
|
|
182
238
|
```bash
|
|
183
239
|
npx paneltir version # version, fingerprint, where it came from
|
|
184
|
-
npx paneltir check 0.
|
|
240
|
+
npx paneltir check 0.10.0 # exits non-zero if that is not what is installed
|
|
185
241
|
npx paneltir board # read the board and say what is wrong with it
|
|
186
242
|
```
|
|
187
243
|
|
package/README.md
CHANGED
|
@@ -54,7 +54,7 @@ tag rather than a branch, so an unfinished push cannot reach a project:
|
|
|
54
54
|
npm install paneltir
|
|
55
55
|
|
|
56
56
|
# or, without npm access, straight from the repository:
|
|
57
|
-
# npm install github:daifukus/paneltir#v0.
|
|
57
|
+
# npm install github:daifukus/paneltir#v0.10.0
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
Installing from Git runs the `prepare` script, which builds `dist/`, so the
|
package/bin/paneltir.mjs
CHANGED
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
* to the project: readable, editable, in its own git history. Re-running never
|
|
15
15
|
* overwrites what is already there.
|
|
16
16
|
*/
|
|
17
|
-
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
|
17
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'
|
|
18
|
+
import { createHash } from 'node:crypto'
|
|
18
19
|
import { dirname, join, relative, resolve } from 'node:path'
|
|
19
20
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
20
21
|
|
|
@@ -110,6 +111,43 @@ function survey(target) {
|
|
|
110
111
|
return found
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Records the provenance of the gate now in the project.
|
|
116
|
+
*
|
|
117
|
+
* Merged rather than replaced: re-running `init` writes only the files that
|
|
118
|
+
* were missing, so a stamp that dropped the rest would forget where the
|
|
119
|
+
* majority of the gate came from. Only files this run actually wrote are
|
|
120
|
+
* re-stamped — for one it left alone we did not write it and cannot claim to
|
|
121
|
+
* know its origin, so the previous answer stands or there is none.
|
|
122
|
+
*
|
|
123
|
+
* The board is stamped like the rest and then deliberately ignored by
|
|
124
|
+
* `doctor`: it is meant to diverge from the seed on the first card, and a
|
|
125
|
+
* report that called that drift would be noise on every project that is
|
|
126
|
+
* working.
|
|
127
|
+
*/
|
|
128
|
+
function writeStamp(target, templateFiles, written) {
|
|
129
|
+
const previous = readStamp(target)
|
|
130
|
+
const files = { ...(previous?.files ?? {}) }
|
|
131
|
+
for (const file of written) files[file] = sha256(join(target, file))
|
|
132
|
+
|
|
133
|
+
const stamp = {
|
|
134
|
+
version: pkg.version,
|
|
135
|
+
templateHash: fingerprint.templateHash ?? '',
|
|
136
|
+
copiedAt: new Date().toISOString(),
|
|
137
|
+
files,
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
mkdirSync(dirname(join(target, STAMP)), { recursive: true })
|
|
141
|
+
writeFileSync(join(target, STAMP), JSON.stringify(stamp, null, 2) + '\n')
|
|
142
|
+
} catch (error) {
|
|
143
|
+
// Never fatal. The gate is copied and working; the stamp only makes a
|
|
144
|
+
// later report sharper, and failing the install over bookkeeping would
|
|
145
|
+
// trade something that matters for something that does not.
|
|
146
|
+
console.error(`\npaneltir: could not write ${STAMP} (${error.message}).`)
|
|
147
|
+
console.error('The gate is in place; `paneltir doctor` will have less to go on.')
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
113
151
|
function reportSurvey(found) {
|
|
114
152
|
const lines = []
|
|
115
153
|
|
|
@@ -149,6 +187,36 @@ Remove it once the board is moved over, or the project ships both.`)
|
|
|
149
187
|
/** The board. Never replaced once it exists — see the loop below. */
|
|
150
188
|
const BOARD_SEED = 'src/data/panel-state.json'
|
|
151
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Where `init` records what it copied and what it copied it from.
|
|
192
|
+
*
|
|
193
|
+
* The gate is the one part of this package that leaves as files a project
|
|
194
|
+
* keeps, and a kept file can never be told it is old. That has been paid for
|
|
195
|
+
* twice: the `HTTP 500` fix landed in `template/api/panel-state.ts`, so every
|
|
196
|
+
* project installed before it still has a save that cannot work, and the skill
|
|
197
|
+
* file is copied the same way, so a rule learned upstream never reaches the
|
|
198
|
+
* Claude that reads it.
|
|
199
|
+
*
|
|
200
|
+
* A plain diff cannot close that, because it cannot tell "you edited this"
|
|
201
|
+
* from "we changed this underneath you" — and those need opposite actions.
|
|
202
|
+
* Hashing each file as written is what separates them.
|
|
203
|
+
*/
|
|
204
|
+
const STAMP = '.paneltir/gate.json'
|
|
205
|
+
|
|
206
|
+
function sha256(path) {
|
|
207
|
+
return createHash('sha256').update(readFileSync(path)).digest('hex')
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function readStamp(target) {
|
|
211
|
+
try {
|
|
212
|
+
return JSON.parse(readFileSync(join(target, STAMP), 'utf8'))
|
|
213
|
+
} catch {
|
|
214
|
+
// Absent or unreadable is not a fault: every project installed before
|
|
215
|
+
// stamping existed is here, and `doctor` still compares the files.
|
|
216
|
+
return null
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
152
220
|
function init(args) {
|
|
153
221
|
const force = args.includes('--force')
|
|
154
222
|
const target = process.cwd()
|
|
@@ -180,6 +248,8 @@ function init(args) {
|
|
|
180
248
|
written.push(file)
|
|
181
249
|
}
|
|
182
250
|
|
|
251
|
+
writeStamp(target, files, written);
|
|
252
|
+
|
|
183
253
|
console.log(`paneltir ${pkg.version} — the panel's gate, into ${target}\n`)
|
|
184
254
|
|
|
185
255
|
const notes = reportSurvey(survey(target))
|
|
@@ -197,8 +267,20 @@ function init(args) {
|
|
|
197
267
|
for (const file of kept) console.log(` = ${file}`)
|
|
198
268
|
}
|
|
199
269
|
|
|
270
|
+
// Written the gate versus found it already there is the honest first-install
|
|
271
|
+
// signal on this side, and it changes what is worth saying: somebody
|
|
272
|
+
// re-running this has read the rest once already.
|
|
200
273
|
if (!written.length) {
|
|
201
274
|
console.log('\nNothing to do — every file is already in place.')
|
|
275
|
+
console.log(`
|
|
276
|
+
If the panel still will not save, open it and press Help: it asks the server
|
|
277
|
+
what is actually set and reports whether, never the value. Or ask this
|
|
278
|
+
project's own Claude:
|
|
279
|
+
|
|
280
|
+
Read .claude/skills/panel/SKILL.md and set up the Paneltir panel with me:
|
|
281
|
+
tell me each environment variable it needs, what it is for, where in this
|
|
282
|
+
project to put it, and check the ones already set.
|
|
283
|
+
`)
|
|
202
284
|
return
|
|
203
285
|
}
|
|
204
286
|
|
|
@@ -232,6 +314,17 @@ Then check two things before trusting it:
|
|
|
232
314
|
not, adjust the last entry of the matcher in middleware.ts to wherever
|
|
233
315
|
this project's build emits it — protecting the page but not the bundle
|
|
234
316
|
leaves the board readable by anyone who opens the HTML.
|
|
317
|
+
|
|
318
|
+
None of that has to be done from here. Open the panel and it walks the
|
|
319
|
+
variables one at a time, checking each against the server rather than asking
|
|
320
|
+
you to tick it. And if a step does not land, the walkthrough carries a line to
|
|
321
|
+
paste into a session in this repository:
|
|
322
|
+
|
|
323
|
+
Read .claude/skills/panel/SKILL.md and set up the Paneltir panel with me:
|
|
324
|
+
tell me each environment variable it needs, what it is for, where in this
|
|
325
|
+
project to put it, and check the ones already set.
|
|
326
|
+
|
|
327
|
+
That skill was just written into this project, and it covers exactly this.
|
|
235
328
|
`)
|
|
236
329
|
}
|
|
237
330
|
|
|
@@ -276,7 +369,7 @@ function check(args) {
|
|
|
276
369
|
* neither helps the person who has just edited the file by hand and wants to
|
|
277
370
|
* know before they push. This is that answer, from the same check.
|
|
278
371
|
*/
|
|
279
|
-
function board(args) {
|
|
372
|
+
async function board(args) {
|
|
280
373
|
const file = args.find((a) => !a.startsWith('-')) || process.env.PANEL_FILE || 'src/data/panel-state.json'
|
|
281
374
|
const path = resolve(process.cwd(), file)
|
|
282
375
|
|
|
@@ -286,7 +379,7 @@ function board(args) {
|
|
|
286
379
|
process.exit(1)
|
|
287
380
|
}
|
|
288
381
|
|
|
289
|
-
const check =
|
|
382
|
+
const check = (await boardReader())(readFileSync(path, 'utf8'))
|
|
290
383
|
if (check.ok) {
|
|
291
384
|
const cards = check.board.cards.length
|
|
292
385
|
console.log(`ok ${file} — ${cards} card${cards === 1 ? '' : 's'}, ${check.board.columns.length} columns`)
|
|
@@ -305,7 +398,205 @@ function board(args) {
|
|
|
305
398
|
process.exit(1)
|
|
306
399
|
}
|
|
307
400
|
|
|
308
|
-
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Says whether the files this project copied are still the files it shipped.
|
|
404
|
+
*
|
|
405
|
+
* The three answers it exists to tell apart:
|
|
406
|
+
*
|
|
407
|
+
* - **yours** — you edited it. Nothing to do; the gate is meant to be edited,
|
|
408
|
+
* and this is only here so the next line is believable.
|
|
409
|
+
* - **behind** — you did not touch it and the template moved. This is the one
|
|
410
|
+
* that has cost real money twice, and the one a bare `git diff` cannot see
|
|
411
|
+
* at all, because there is nothing in the project's history to diff
|
|
412
|
+
* against.
|
|
413
|
+
* - **both** — you edited it *and* the template moved. The only case that
|
|
414
|
+
* needs a person: `--force` would throw your edit away.
|
|
415
|
+
*
|
|
416
|
+
* It reports and never repairs, for the same reason the panel records a request
|
|
417
|
+
* instead of running it: replacing somebody's front door is a decision, and
|
|
418
|
+
* `init --force` is where a person makes it having read what changed.
|
|
419
|
+
*/
|
|
420
|
+
function doctor() {
|
|
421
|
+
const target = process.cwd()
|
|
422
|
+
const templateDir = join(packageRoot, 'template')
|
|
423
|
+
if (!existsSync(templateDir)) {
|
|
424
|
+
console.error('paneltir: this install has no template/ directory.')
|
|
425
|
+
process.exit(1)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const stamp = readStamp(target)
|
|
429
|
+
const files = walk(templateDir).sort()
|
|
430
|
+
|
|
431
|
+
console.log(`paneltir ${pkg.version} — the gate in ${target}\n`)
|
|
432
|
+
|
|
433
|
+
if (!stamp) {
|
|
434
|
+
console.log(`No ${STAMP}, so this gate was copied before stamping existed.`)
|
|
435
|
+
console.log('Files can still be compared; "you edited it" and "we changed it"')
|
|
436
|
+
console.log('cannot be told apart until the next `init` writes one.\n')
|
|
437
|
+
} else {
|
|
438
|
+
const behind = stamp.templateHash !== (fingerprint.templateHash ?? '')
|
|
439
|
+
console.log(`Copied from v${stamp.version} on ${String(stamp.copiedAt).slice(0, 10)}`)
|
|
440
|
+
console.log(behind ? 'The template has changed since.\n' : 'The template has not changed since.\n')
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const rows = []
|
|
444
|
+
for (const file of files) {
|
|
445
|
+
// The board is meant to diverge: it stops being a template on the first
|
|
446
|
+
// card. Calling that drift would put a warning on every working project.
|
|
447
|
+
if (file === BOARD_SEED) {
|
|
448
|
+
rows.push([file, existsSync(join(target, file)) ? 'yours' : 'missing', 'the board — never compared'])
|
|
449
|
+
continue
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const here = join(target, file)
|
|
453
|
+
if (!existsSync(here)) {
|
|
454
|
+
rows.push([file, 'missing', 'never copied; `paneltir init` writes it'])
|
|
455
|
+
continue
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const mine = sha256(here)
|
|
459
|
+
const theirs = sha256(join(templateDir, file))
|
|
460
|
+
const stamped = stamp?.files?.[file]
|
|
461
|
+
|
|
462
|
+
if (mine === theirs) {
|
|
463
|
+
rows.push([file, 'current', ''])
|
|
464
|
+
} else if (!stamped) {
|
|
465
|
+
rows.push([file, 'differs', 'no stamp, so who moved cannot be said'])
|
|
466
|
+
} else if (mine === stamped) {
|
|
467
|
+
rows.push([file, 'BEHIND', 'you did not touch it; the template moved'])
|
|
468
|
+
} else if (stamped === theirs) {
|
|
469
|
+
rows.push([file, 'yours', 'you edited it; the template has not moved'])
|
|
470
|
+
} else {
|
|
471
|
+
rows.push([file, 'BOTH', 'you edited it AND the template moved — read both'])
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const width = Math.max(...rows.map(([file]) => file.length))
|
|
476
|
+
for (const [file, state, note] of rows) {
|
|
477
|
+
console.log(` ${state.padEnd(8)} ${file.padEnd(width)}${note ? ` ${note}` : ''}`)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const stale = rows.filter(([, state]) => state === 'BEHIND' || state === 'BOTH')
|
|
481
|
+
const missing = rows.filter(([, state]) => state === 'missing')
|
|
482
|
+
|
|
483
|
+
if (!stale.length && !missing.length) {
|
|
484
|
+
console.log('\nNothing to do.')
|
|
485
|
+
return
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (stale.length) {
|
|
489
|
+
console.log(`\n${stale.length} file(s) are older than the package they came from.`)
|
|
490
|
+
console.log('See what changed, then decide:')
|
|
491
|
+
console.log(` diff -u <file> node_modules/paneltir/template/<file>`)
|
|
492
|
+
console.log('`paneltir init --force` replaces the gate and never the board.')
|
|
493
|
+
console.log('Anything marked BOTH would lose your edit — read that diff first.')
|
|
494
|
+
}
|
|
495
|
+
if (missing.length) {
|
|
496
|
+
console.log(`\n${missing.length} file(s) were never copied. \`paneltir init\` writes them.`)
|
|
497
|
+
}
|
|
498
|
+
// A gate that cannot work is a failure, not a remark: this runs in CI for
|
|
499
|
+
// some projects, and an exit code is the only part of it a script reads.
|
|
500
|
+
process.exit(1)
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Appends a run to the board, so the record does not depend on remembering.
|
|
505
|
+
*
|
|
506
|
+
* `runs` is what each pass actually did, and until now it was written by hand
|
|
507
|
+
* — by Claude, into JSON, at the end of a session, from memory. That is the
|
|
508
|
+
* shape of record that is complete right up until the session that mattered.
|
|
509
|
+
* A command can be called the moment the work lands.
|
|
510
|
+
*
|
|
511
|
+
* It writes the file and nothing else: the board is committed by the panel's
|
|
512
|
+
* Save or by the project's own commit, and a CLI that pushed on somebody's
|
|
513
|
+
* behalf would be the second thing in this project writing to their
|
|
514
|
+
* repository.
|
|
515
|
+
*/
|
|
516
|
+
async function run(args) {
|
|
517
|
+
const flags = args.filter((a) => a.startsWith('--'))
|
|
518
|
+
const plain = args.filter((a) => !a.startsWith('--'))
|
|
519
|
+
const summary = plain[0]
|
|
520
|
+
const file = plain[1] ?? BOARD_SEED
|
|
521
|
+
|
|
522
|
+
if (!summary) {
|
|
523
|
+
console.error('paneltir run "what this pass did" [--cards a,b] [--by you] [board.json]')
|
|
524
|
+
process.exit(1)
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const path = resolve(process.cwd(), file)
|
|
528
|
+
if (!existsSync(path)) {
|
|
529
|
+
console.error(`paneltir: no board at ${file}`)
|
|
530
|
+
process.exit(1)
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const text = readFileSync(path, 'utf8')
|
|
534
|
+
const check = (await boardReader())(text)
|
|
535
|
+
if (!check.ok) {
|
|
536
|
+
// Refused before writing, not after: appending to a board that is already
|
|
537
|
+
// broken buries the fault under a change nobody asked for.
|
|
538
|
+
console.error(`paneltir: ${file} is not a board this kit can read, so nothing was appended.\n`)
|
|
539
|
+
for (const problem of check.problems) {
|
|
540
|
+
console.error(problem.at ? ` ${problem.at}\n ${problem.says}` : ` ${problem.says}`)
|
|
541
|
+
}
|
|
542
|
+
process.exit(1)
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const board = check.board
|
|
546
|
+
const cards = (flags.find((f) => f.startsWith('--cards='))?.slice(8) ?? '')
|
|
547
|
+
.split(',')
|
|
548
|
+
.map((id) => id.trim())
|
|
549
|
+
.filter(Boolean)
|
|
550
|
+
|
|
551
|
+
// A run naming a card the board does not have is a claim that cannot be
|
|
552
|
+
// checked, which is the only thing `cards` was for.
|
|
553
|
+
const known = new Set(board.cards.map((card) => card.id))
|
|
554
|
+
const unknown = cards.filter((id) => !known.has(id))
|
|
555
|
+
if (unknown.length) {
|
|
556
|
+
console.error(`paneltir: ${file} has no card called ${unknown.join(', ')}.`)
|
|
557
|
+
process.exit(1)
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const by = flags.find((f) => f.startsWith('--by='))?.slice(5) === 'you' ? 'you' : 'claude'
|
|
561
|
+
const date = new Date().toISOString().slice(0, 10)
|
|
562
|
+
board.runs = Array.isArray(board.runs) ? board.runs : []
|
|
563
|
+
board.runs.unshift({
|
|
564
|
+
id: `run-${date}-${String(board.runs.length + 1).padStart(3, '0')}`,
|
|
565
|
+
date,
|
|
566
|
+
by,
|
|
567
|
+
summary,
|
|
568
|
+
cards,
|
|
569
|
+
fingerprint: shortHash,
|
|
570
|
+
})
|
|
571
|
+
|
|
572
|
+
writeFileSync(path, JSON.stringify(board, null, 2) + '\n')
|
|
573
|
+
console.log(`Appended to ${file}: ${summary}`)
|
|
574
|
+
console.log(` ${cards.length ? `cards ${cards.join(', ')}` : 'no cards named'} · by ${by} · ${shortHash}`)
|
|
575
|
+
console.log('\nThe file is changed and not committed. The panel commits it on Save,')
|
|
576
|
+
console.log('or commit it yourself — this writes nothing to your repository.')
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* The board reader, loaded only by the commands that read a board.
|
|
581
|
+
*
|
|
582
|
+
* It used to be imported at module scope, which meant every command pulled in
|
|
583
|
+
* `dist/index.js` — and that bundle imports React, because it is a component
|
|
584
|
+
* library. So `paneltir init`, whose entire job is to copy seven files, and
|
|
585
|
+
* `paneltir doctor`, whose job is to hash them, both failed outright in a
|
|
586
|
+
* project that had not installed React yet. Which is exactly when somebody runs
|
|
587
|
+
* `init`.
|
|
588
|
+
*
|
|
589
|
+
* It is the same rule as the gate's, one layer up: a thing whose job is files
|
|
590
|
+
* does not carry a component library to do it. Here the fix is smaller than
|
|
591
|
+
* duplicating anything — the import just moves to where it is used.
|
|
592
|
+
*/
|
|
593
|
+
let readBoard
|
|
594
|
+
async function boardReader() {
|
|
595
|
+
if (!readBoard) {
|
|
596
|
+
;({ readBoard } = await import(pathToFileURL(join(here, '../dist/index.js')).href))
|
|
597
|
+
}
|
|
598
|
+
return readBoard
|
|
599
|
+
}
|
|
309
600
|
|
|
310
601
|
const [command, ...rest] = process.argv.slice(2)
|
|
311
602
|
|
|
@@ -320,7 +611,13 @@ switch (command) {
|
|
|
320
611
|
check(rest)
|
|
321
612
|
break
|
|
322
613
|
case 'board':
|
|
323
|
-
board(rest)
|
|
614
|
+
await board(rest)
|
|
615
|
+
break
|
|
616
|
+
case 'doctor':
|
|
617
|
+
doctor()
|
|
618
|
+
break
|
|
619
|
+
case 'run':
|
|
620
|
+
await run(rest)
|
|
324
621
|
break
|
|
325
622
|
default:
|
|
326
623
|
console.log(`paneltir v${pkg.version} · ${shortHash}
|
|
@@ -329,6 +626,8 @@ switch (command) {
|
|
|
329
626
|
paneltir version what is installed, and where it came from
|
|
330
627
|
paneltir check [version] fail if the installed version is not the one meant
|
|
331
628
|
paneltir board [file] read this project's board and say what is wrong
|
|
629
|
+
paneltir doctor is the gate you copied still the gate we ship?
|
|
630
|
+
paneltir run "summary" append what this pass did to the board's runs
|
|
332
631
|
|
|
333
632
|
The components install as a library and are never edited. The gate — password,
|
|
334
633
|
session, write-back — is copied in by init and belongs to the project.`)
|