paneltir 0.9.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 +14 -0
- package/INSTALL.md +35 -2
- package/README.md +1 -1
- package/bin/paneltir.mjs +281 -5
- package/dist/index.d.ts +120 -2
- package/dist/index.js +60 -5
- package/fingerprint.json +5 -4
- package/package.json +22 -14
- package/template/.claude/skills/panel/SKILL.md +29 -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,6 +45,13 @@ 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
56
|
first time they open the board; each note is dismissed and stays dismissed.
|
|
43
57
|
**Help** in the header reports what the panel needs — the password, the
|
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.)
|
|
@@ -165,6 +165,7 @@ identity** — never another project's.
|
|
|
165
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
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
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. |
|
|
168
169
|
| `brand` | What sits where a name goes in the header. Defaults to the board's `project`. |
|
|
169
170
|
| `capabilities`, `extras` | Sections of the caller's own. Not drawn when absent. |
|
|
170
171
|
| `links`, `footer`, `decoration`, `className` | Where else to go, what sits under the panel, and anything the caller draws behind it. |
|
|
@@ -192,6 +193,38 @@ identity** — never another project's.
|
|
|
192
193
|
there. On a board already in use the same walkthrough is behind **Help**,
|
|
193
194
|
offered only while something is actually missing.
|
|
194
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
|
+
|
|
195
228
|
Two things to check before trusting it: `/admin` signed out must land on
|
|
196
229
|
`/login`, and **the panel's own JavaScript bundle must answer 401 signed
|
|
197
230
|
out**. If it does not, point the last entry of the matcher in
|
|
@@ -204,7 +237,7 @@ identity** — never another project's.
|
|
|
204
237
|
|
|
205
238
|
```bash
|
|
206
239
|
npx paneltir version # version, fingerprint, where it came from
|
|
207
|
-
npx paneltir check 0.
|
|
240
|
+
npx paneltir check 0.10.0 # exits non-zero if that is not what is installed
|
|
208
241
|
npx paneltir board # read the board and say what is wrong with it
|
|
209
242
|
```
|
|
210
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))
|
|
@@ -299,7 +369,7 @@ function check(args) {
|
|
|
299
369
|
* neither helps the person who has just edited the file by hand and wants to
|
|
300
370
|
* know before they push. This is that answer, from the same check.
|
|
301
371
|
*/
|
|
302
|
-
function board(args) {
|
|
372
|
+
async function board(args) {
|
|
303
373
|
const file = args.find((a) => !a.startsWith('-')) || process.env.PANEL_FILE || 'src/data/panel-state.json'
|
|
304
374
|
const path = resolve(process.cwd(), file)
|
|
305
375
|
|
|
@@ -309,7 +379,7 @@ function board(args) {
|
|
|
309
379
|
process.exit(1)
|
|
310
380
|
}
|
|
311
381
|
|
|
312
|
-
const check =
|
|
382
|
+
const check = (await boardReader())(readFileSync(path, 'utf8'))
|
|
313
383
|
if (check.ok) {
|
|
314
384
|
const cards = check.board.cards.length
|
|
315
385
|
console.log(`ok ${file} — ${cards} card${cards === 1 ? '' : 's'}, ${check.board.columns.length} columns`)
|
|
@@ -328,7 +398,205 @@ function board(args) {
|
|
|
328
398
|
process.exit(1)
|
|
329
399
|
}
|
|
330
400
|
|
|
331
|
-
|
|
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
|
+
}
|
|
332
600
|
|
|
333
601
|
const [command, ...rest] = process.argv.slice(2)
|
|
334
602
|
|
|
@@ -343,7 +611,13 @@ switch (command) {
|
|
|
343
611
|
check(rest)
|
|
344
612
|
break
|
|
345
613
|
case 'board':
|
|
346
|
-
board(rest)
|
|
614
|
+
await board(rest)
|
|
615
|
+
break
|
|
616
|
+
case 'doctor':
|
|
617
|
+
doctor()
|
|
618
|
+
break
|
|
619
|
+
case 'run':
|
|
620
|
+
await run(rest)
|
|
347
621
|
break
|
|
348
622
|
default:
|
|
349
623
|
console.log(`paneltir v${pkg.version} · ${shortHash}
|
|
@@ -352,6 +626,8 @@ switch (command) {
|
|
|
352
626
|
paneltir version what is installed, and where it came from
|
|
353
627
|
paneltir check [version] fail if the installed version is not the one meant
|
|
354
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
|
|
355
631
|
|
|
356
632
|
The components install as a library and are never edited. The gate — password,
|
|
357
633
|
session, write-back — is copied in by init and belongs to the project.`)
|
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,17 @@ declare const PANELTIR_VERSION: string;
|
|
|
15
15
|
declare const PANELTIR_FINGERPRINT: string;
|
|
16
16
|
/** How many files that fingerprint covers. */
|
|
17
17
|
declare const PANELTIR_FILE_COUNT: number;
|
|
18
|
+
/**
|
|
19
|
+
* SHA-256 over `template/` alone — the gate, the seed board and the skill file.
|
|
20
|
+
*
|
|
21
|
+
* Separate from the fingerprint above because it answers a different question.
|
|
22
|
+
* The fingerprint says which build this is; this says whether the files a
|
|
23
|
+
* project *copied out of* the package are the ones it ships now. Most releases
|
|
24
|
+
* do not touch `template/`, so those two move at different rates, and a gate
|
|
25
|
+
* checked against the version number would be reported stale on every release
|
|
26
|
+
* and believed on none.
|
|
27
|
+
*/
|
|
28
|
+
declare const PANELTIR_TEMPLATE_HASH: string;
|
|
18
29
|
/**
|
|
19
30
|
* Everything needed to report the kit in a panel's own footer or settings
|
|
20
31
|
* sheet, and to check it against the released version without opening
|
|
@@ -24,6 +35,8 @@ declare const paneltirBuild: {
|
|
|
24
35
|
readonly version: string;
|
|
25
36
|
readonly fingerprint: string;
|
|
26
37
|
readonly fileCount: number;
|
|
38
|
+
/** What `init` stamps, and what a gate is compared against. */
|
|
39
|
+
readonly templateHash: string;
|
|
27
40
|
/** The tag this build came from, ready to compare or to link. */
|
|
28
41
|
readonly tag: `v${string}`;
|
|
29
42
|
/** Short form, for a footer that has one line to spare. */
|
|
@@ -1361,6 +1374,9 @@ interface UiStrings {
|
|
|
1361
1374
|
needTokenEnables: string;
|
|
1362
1375
|
needRepo: string;
|
|
1363
1376
|
needRepoEnables: string;
|
|
1377
|
+
needGate: string;
|
|
1378
|
+
needGateEnables: string;
|
|
1379
|
+
fixGate: string;
|
|
1364
1380
|
fixTokenCreate: string;
|
|
1365
1381
|
fixTokenScope: string;
|
|
1366
1382
|
fixTokenPaste: string;
|
|
@@ -1591,6 +1607,21 @@ interface PanelAppProps {
|
|
|
1591
1607
|
* collides with a preset replaces it, because the caller named it.
|
|
1592
1608
|
*/
|
|
1593
1609
|
extraThemes?: PanelThemes;
|
|
1610
|
+
/**
|
|
1611
|
+
* `.paneltir/gate.json` from this project, if the caller imports it.
|
|
1612
|
+
*
|
|
1613
|
+
* The gate is copied once and then belongs to the project, which means it is
|
|
1614
|
+
* the one part of this package that can never be told it is old — and that
|
|
1615
|
+
* has cost two releases already. Handing the stamp in lets the panel say so,
|
|
1616
|
+
* with no filesystem and no network, because both halves of the comparison
|
|
1617
|
+
* are already data: the stamp from the project and `templateHash` from the
|
|
1618
|
+
* installed package.
|
|
1619
|
+
*
|
|
1620
|
+
* Omitted, the panel says nothing about the gate rather than guessing. This
|
|
1621
|
+
* is a prop and not a fetch for the reason the Tools screen is a prop in the
|
|
1622
|
+
* demo: the kit does not decide what a caller's server can be asked.
|
|
1623
|
+
*/
|
|
1624
|
+
gate?: unknown;
|
|
1594
1625
|
/** Which one to draw before the board has said. Defaults to the first. */
|
|
1595
1626
|
defaultTheme?: string;
|
|
1596
1627
|
/**
|
|
@@ -1657,7 +1688,94 @@ interface PanelAppProps {
|
|
|
1657
1688
|
fileCount: number;
|
|
1658
1689
|
};
|
|
1659
1690
|
}
|
|
1660
|
-
declare function PanelApp({ state: initialState, themes: exactThemes, extraThemes, defaultTheme, brand, decoration, className, footer, links, capabilities, extras, showLanguage, showHistory, checkForUpdates, saveEndpoint, marketplaceEndpoint, marketplaceReport, fingerprint, }: PanelAppProps): React.JSX.Element;
|
|
1691
|
+
declare function PanelApp({ state: initialState, themes: exactThemes, extraThemes, gate, defaultTheme, brand, decoration, className, footer, links, capabilities, extras, showLanguage, showHistory, checkForUpdates, saveEndpoint, marketplaceEndpoint, marketplaceReport, fingerprint, }: PanelAppProps): React.JSX.Element;
|
|
1692
|
+
|
|
1693
|
+
/**
|
|
1694
|
+
* Whether the gate a project is running is the gate this version ships.
|
|
1695
|
+
*
|
|
1696
|
+
* `template/` is the one thing here that leaves as *files a project keeps*.
|
|
1697
|
+
* The components arrive in `node_modules` and are replaced on every update;
|
|
1698
|
+
* the gate is copied once by `init` and is then that project's own — which is
|
|
1699
|
+
* the point, since it holds their password rule and points at their
|
|
1700
|
+
* repository, and a front door that moves when a library updates is not a
|
|
1701
|
+
* front door.
|
|
1702
|
+
*
|
|
1703
|
+
* The cost of that is a copy that can never be told it is old. It has already
|
|
1704
|
+
* been paid twice. The `HTTP 500` fix landed in `template/api/panel-state.ts`,
|
|
1705
|
+
* so every project installed before it still has a save that cannot work, and
|
|
1706
|
+
* nothing anywhere says so; `.claude/skills/panel/SKILL.md` is copied the same
|
|
1707
|
+
* way, so a rule learned upstream never reaches the Claude that reads it.
|
|
1708
|
+
* Upstream the copy is held honest by `test:auth` and `test:board`. Downstream
|
|
1709
|
+
* there was nothing at all.
|
|
1710
|
+
*
|
|
1711
|
+
* So `init` records what it copied and what it copied it from, and this says
|
|
1712
|
+
* what that record means. Two rules shape it:
|
|
1713
|
+
*
|
|
1714
|
+
* - **It compares hashes, not version numbers.** Most releases do not touch
|
|
1715
|
+
* `template/`, so "you are on 0.6.0 and 0.9.0 exists" would cry wolf for
|
|
1716
|
+
* every release and be ignored by the one that mattered. `templateHash`
|
|
1717
|
+
* ships inside `fingerprint.json`, so the comparison is exact.
|
|
1718
|
+
* - **It needs no filesystem and no network**, because the panel runs in a
|
|
1719
|
+
* browser and has neither. Everything it judges is data the caller already
|
|
1720
|
+
* holds: the stamp from the project, the hash from the installed package.
|
|
1721
|
+
*
|
|
1722
|
+
* It reports and never repairs. Replacing somebody's gate is `init --force`,
|
|
1723
|
+
* typed by a person who has read what changed.
|
|
1724
|
+
*/
|
|
1725
|
+
/**
|
|
1726
|
+
* What `init` wrote, and what it wrote it from.
|
|
1727
|
+
*
|
|
1728
|
+
* Lives at `.paneltir/gate.json` in the project. It is not a lockfile and
|
|
1729
|
+
* nothing resolves against it — it exists so a later session can tell "you
|
|
1730
|
+
* edited this" from "we changed this underneath you", which is the distinction
|
|
1731
|
+
* a bare diff cannot make.
|
|
1732
|
+
*/
|
|
1733
|
+
interface GateStamp {
|
|
1734
|
+
/** The kit version that copied these files. */
|
|
1735
|
+
version: string;
|
|
1736
|
+
/** `templateHash` from that version's `fingerprint.json`. */
|
|
1737
|
+
templateHash: string;
|
|
1738
|
+
/** When, so a reader can date it against their own git history. */
|
|
1739
|
+
copiedAt: string;
|
|
1740
|
+
/**
|
|
1741
|
+
* SHA-256 of each file as written, keyed by its path in the project.
|
|
1742
|
+
*
|
|
1743
|
+
* This is what separates a file the project edited from one the template
|
|
1744
|
+
* moved under. Absent for a file `init` left alone: we did not write it, so
|
|
1745
|
+
* we cannot claim to know where it came from.
|
|
1746
|
+
*/
|
|
1747
|
+
files: Record<string, string>;
|
|
1748
|
+
}
|
|
1749
|
+
type GateVerdict =
|
|
1750
|
+
/** The gate is the one this version ships. */
|
|
1751
|
+
'current'
|
|
1752
|
+
/** `template/` has changed since this gate was copied. */
|
|
1753
|
+
| 'behind'
|
|
1754
|
+
/**
|
|
1755
|
+
* No stamp. Every project installed before stamping existed is here, and it
|
|
1756
|
+
* is not a fault — it is the absence of an answer, which is why it is not
|
|
1757
|
+
* called one. `paneltir doctor` can still compare the files themselves.
|
|
1758
|
+
*/
|
|
1759
|
+
| 'unstamped';
|
|
1760
|
+
interface GateReading {
|
|
1761
|
+
verdict: GateVerdict;
|
|
1762
|
+
/** The version that copied the gate, when a stamp says so. */
|
|
1763
|
+
copiedFrom?: string;
|
|
1764
|
+
/** The version running now. */
|
|
1765
|
+
installed: string;
|
|
1766
|
+
}
|
|
1767
|
+
/**
|
|
1768
|
+
* Reads a stamp against the installed package.
|
|
1769
|
+
*
|
|
1770
|
+
* Deliberately total: a malformed or absent stamp answers `unstamped` rather
|
|
1771
|
+
* than throwing. This is called while a panel is drawing, and a panel that
|
|
1772
|
+
* fails to open because it could not classify its own gate would be a worse
|
|
1773
|
+
* outcome than the drift it was looking for.
|
|
1774
|
+
*/
|
|
1775
|
+
declare function gateStatus(stamp: unknown, installed: {
|
|
1776
|
+
version: string;
|
|
1777
|
+
templateHash?: string;
|
|
1778
|
+
}): GateReading;
|
|
1661
1779
|
|
|
1662
1780
|
/**
|
|
1663
1781
|
* Applies a board move to the flat card list.
|
|
@@ -1717,4 +1835,4 @@ interface CardSheetProps {
|
|
|
1717
1835
|
}
|
|
1718
1836
|
declare function CardSheet({ card, state, lang, ui, onChange, onDelete, onClose }: CardSheetProps): React.JSX.Element | null;
|
|
1719
1837
|
|
|
1720
|
-
export { ANALYSIS_SECTIONS, AlertBanner, type AlertBannerProps, type Analysis, type AnalysisSection, AnalysisView, BOARD_VERSION, Board, type BoardCardData, type BoardCheck, type BoardColumnData, type BoardFacts, type BoardMoveResult, type BoardProblem, type BoardProps, type BoardTag, CAPABILITIES, type Capability, Card$1 as Card, type CardProps, CardSheet, type Check, type Choice, type ChoiceOption, type ColumnDef, type Competitor, type Constraint, CopyBlock, type CopyBlockLabels, type CopyBlockProps, type Counts, DashboardHeader, type DashboardHeaderProps, type DashboardThemeForm, DashboardThemeProvider, type DashboardThemeTokens, DetailSection, type DetailSectionProps, DetailSheet, type DetailSheetProps, FilterChip, type FilterChipProps, FilterChipRow, type Goal, GuideNote, type GuideNoteProps, GuideTour, type GuideTourProps, type HealthFact, HealthPill, type HealthPillProps, HealthPillRow, type HealthStatus, INTENTS, INTENT_COPY, type ImportedTheme, type InstallState, type Intent, type IntentCopy, LANGS, LANG_LABELS, LEVELS, type Lang, type Level, MARKETPLACE_NAME, MARKETPLACE_URL, type Market, type MarketplaceReport, type MarketplaceTool, MarketplaceView, type Metric, type Note, NotificationBell, type NotificationBellProps, type NotificationLabels, type NotificationTone, OWNERS, type Owner, PANELTIR_FILE_COUNT, PANELTIR_FINGERPRINT, PANELTIR_VERSION, PRESET_PANEL_THEMES, PanelApp, type PanelAppProps, type Card as PanelCard, type PanelNotification, type PanelState, type PanelTheme, type PanelThemes, type Preferences, type Reading, type Run, SetupGuide, type SetupGuideProps, type SetupRequirement, type SetupStatus, SetupWizard, type SetupWizardLabels, type SetupWizardProps, type Severity, StatTile, StatTileGrid, type StatTileProps, type Strategy, type Suggestion, type SuggestionStatus, THEME_FORMS, THEME_PRESETS, type Text, type ThemeFormName, type ThemePresetName, type Trend, UI, type UiStrings, type Weight, type WizardStep, applyMove, boardFacts, checkProgress, claimedWithoutStarting, claudeTheme, commandSnippet, countCards, cyberpunkTheme, decided, emptyCard, explainBoard, isCard, isFirstRun, ledgerForm, midnightTheme, moveCardInColumns, newId, offeredThemes, oldMoneyTheme, panelForm, paneltirBuild, paperForm, readBoard, readStoredLang, repositorySnippet, resetGuide, sparkPoints, stampForColumn, storeLang, text, today, trendOf, undecided, untranslated, useDashboardForm, useDashboardTheme, useDelegatedClick, useGuideNote, useMediaQuery, validateBoard, wizardStep, writeText };
|
|
1838
|
+
export { ANALYSIS_SECTIONS, AlertBanner, type AlertBannerProps, type Analysis, type AnalysisSection, AnalysisView, BOARD_VERSION, Board, type BoardCardData, type BoardCheck, type BoardColumnData, type BoardFacts, type BoardMoveResult, type BoardProblem, type BoardProps, type BoardTag, CAPABILITIES, type Capability, Card$1 as Card, type CardProps, CardSheet, type Check, type Choice, type ChoiceOption, type ColumnDef, type Competitor, type Constraint, CopyBlock, type CopyBlockLabels, type CopyBlockProps, type Counts, DashboardHeader, type DashboardHeaderProps, type DashboardThemeForm, DashboardThemeProvider, type DashboardThemeTokens, DetailSection, type DetailSectionProps, DetailSheet, type DetailSheetProps, FilterChip, type FilterChipProps, FilterChipRow, type GateReading, type GateStamp, type GateVerdict, type Goal, GuideNote, type GuideNoteProps, GuideTour, type GuideTourProps, type HealthFact, HealthPill, type HealthPillProps, HealthPillRow, type HealthStatus, INTENTS, INTENT_COPY, type ImportedTheme, type InstallState, type Intent, type IntentCopy, LANGS, LANG_LABELS, LEVELS, type Lang, type Level, MARKETPLACE_NAME, MARKETPLACE_URL, type Market, type MarketplaceReport, type MarketplaceTool, MarketplaceView, type Metric, type Note, NotificationBell, type NotificationBellProps, type NotificationLabels, type NotificationTone, OWNERS, type Owner, PANELTIR_FILE_COUNT, PANELTIR_FINGERPRINT, PANELTIR_TEMPLATE_HASH, PANELTIR_VERSION, PRESET_PANEL_THEMES, PanelApp, type PanelAppProps, type Card as PanelCard, type PanelNotification, type PanelState, type PanelTheme, type PanelThemes, type Preferences, type Reading, type Run, SetupGuide, type SetupGuideProps, type SetupRequirement, type SetupStatus, SetupWizard, type SetupWizardLabels, type SetupWizardProps, type Severity, StatTile, StatTileGrid, type StatTileProps, type Strategy, type Suggestion, type SuggestionStatus, THEME_FORMS, THEME_PRESETS, type Text, type ThemeFormName, type ThemePresetName, type Trend, UI, type UiStrings, type Weight, type WizardStep, applyMove, boardFacts, checkProgress, claimedWithoutStarting, claudeTheme, commandSnippet, countCards, cyberpunkTheme, decided, emptyCard, explainBoard, gateStatus, isCard, isFirstRun, ledgerForm, midnightTheme, moveCardInColumns, newId, offeredThemes, oldMoneyTheme, panelForm, paneltirBuild, paperForm, readBoard, readStoredLang, repositorySnippet, resetGuide, sparkPoints, stampForColumn, storeLang, text, today, trendOf, undecided, untranslated, useDashboardForm, useDashboardTheme, useDelegatedClick, useGuideNote, useMediaQuery, validateBoard, wizardStep, writeText };
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
// src/version.ts
|
|
2
|
-
var PANELTIR_VERSION = "0.
|
|
3
|
-
var PANELTIR_FINGERPRINT = "sha256:
|
|
4
|
-
var PANELTIR_FILE_COUNT =
|
|
2
|
+
var PANELTIR_VERSION = "0.10.0";
|
|
3
|
+
var PANELTIR_FINGERPRINT = "sha256:4de7ef7076563e4c3118ac16b645737a44c4c0675115d9a9d7bbff562d5ecb74";
|
|
4
|
+
var PANELTIR_FILE_COUNT = 163;
|
|
5
|
+
var PANELTIR_TEMPLATE_HASH = "sha256:7f9accd11920a0c189dd683bdb96635a606960afeca71e02c2a26a0257acefd8";
|
|
5
6
|
var paneltirBuild = {
|
|
6
7
|
version: PANELTIR_VERSION,
|
|
7
8
|
fingerprint: PANELTIR_FINGERPRINT,
|
|
8
9
|
fileCount: PANELTIR_FILE_COUNT,
|
|
10
|
+
/** What `init` stamps, and what a gate is compared against. */
|
|
11
|
+
templateHash: PANELTIR_TEMPLATE_HASH,
|
|
9
12
|
/** The tag this build came from, ready to compare or to link. */
|
|
10
13
|
tag: `v${PANELTIR_VERSION}`,
|
|
11
14
|
/** Short form, for a footer that has one line to spare. */
|
|
@@ -1472,6 +1475,18 @@ function offeredThemes(themes, extraThemes) {
|
|
|
1472
1475
|
return offered;
|
|
1473
1476
|
}
|
|
1474
1477
|
|
|
1478
|
+
// src/gate/stamp.ts
|
|
1479
|
+
function gateStatus(stamp, installed) {
|
|
1480
|
+
const reading = { verdict: "unstamped", installed: installed.version };
|
|
1481
|
+
if (typeof stamp !== "object" || stamp === null) return reading;
|
|
1482
|
+
const candidate = stamp;
|
|
1483
|
+
if (typeof candidate.templateHash !== "string" || candidate.templateHash === "") return reading;
|
|
1484
|
+
if (typeof candidate.version === "string" && candidate.version) reading.copiedFrom = candidate.version;
|
|
1485
|
+
if (typeof installed.templateHash !== "string" || installed.templateHash === "") return reading;
|
|
1486
|
+
reading.verdict = candidate.templateHash === installed.templateHash ? "current" : "behind";
|
|
1487
|
+
return reading;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1475
1490
|
// src/panel/AnalysisView.tsx
|
|
1476
1491
|
import { useMemo as useMemo3, useState as useState7 } from "react";
|
|
1477
1492
|
|
|
@@ -1599,6 +1614,9 @@ var UI = {
|
|
|
1599
1614
|
needTokenEnables: "Lets Save commit the board back to the repository. Without it everything else works and Save fails.",
|
|
1600
1615
|
needRepo: "The repository to write to",
|
|
1601
1616
|
needRepoEnables: "Which repository the board is committed to. There is no default on purpose: writing to the wrong one is worse than not writing.",
|
|
1617
|
+
needGate: "The gate this project copied",
|
|
1618
|
+
needGateEnables: "The sign-in, the session and the write-back are files this project owns, copied once and never updated with the library. When one of them is older than the package, the panel does not look misconfigured \u2014 it looks broken.",
|
|
1619
|
+
fixGate: "The gate is older than the installed kit. See what changed before replacing anything:",
|
|
1602
1620
|
fixTokenCreate: "On GitHub, make a fine-grained personal access token.",
|
|
1603
1621
|
fixTokenScope: "Give it Contents: read and write, on this repository only \u2014 nothing else, and no other repository.",
|
|
1604
1622
|
fixTokenPaste: "Paste it into the hosting project as GH_TOKEN. Never into the repository: a token in a commit is a token to revoke.",
|
|
@@ -1920,6 +1938,9 @@ var UI = {
|
|
|
1920
1938
|
needTokenEnables: "Permite que Guardar confirme el tablero en el repositorio. Sin \xE9l todo lo dem\xE1s funciona y Guardar falla.",
|
|
1921
1939
|
needRepo: "El repositorio donde escribir",
|
|
1922
1940
|
needRepoEnables: "En qu\xE9 repositorio se confirma el tablero. No hay valor por defecto a prop\xF3sito: escribir en el equivocado es peor que no escribir.",
|
|
1941
|
+
needGate: "La puerta que copi\xF3 este proyecto",
|
|
1942
|
+
needGateEnables: "El acceso, la sesi\xF3n y la escritura de vuelta son archivos de este proyecto, copiados una vez y que no se actualizan con la librer\xEDa. Cuando uno se queda m\xE1s viejo que el paquete, el panel no parece mal configurado: parece roto.",
|
|
1943
|
+
fixGate: "La puerta es m\xE1s vieja que el kit instalado. Mira qu\xE9 cambi\xF3 antes de reemplazar nada:",
|
|
1923
1944
|
fixTokenCreate: "En GitHub, crea un token de acceso personal de tipo fine-grained.",
|
|
1924
1945
|
fixTokenScope: "Dale Contents: read and write, solo sobre este repositorio \u2014 nada m\xE1s, y ning\xFAn otro repositorio.",
|
|
1925
1946
|
fixTokenPaste: "P\xE9galo en el proyecto de hosting como GH_TOKEN. Nunca en el repositorio: un token en un commit es un token que hay que revocar.",
|
|
@@ -2977,6 +2998,7 @@ function PanelApp({
|
|
|
2977
2998
|
state: initialState,
|
|
2978
2999
|
themes: exactThemes,
|
|
2979
3000
|
extraThemes,
|
|
3001
|
+
gate,
|
|
2980
3002
|
defaultTheme,
|
|
2981
3003
|
brand,
|
|
2982
3004
|
decoration,
|
|
@@ -2994,6 +3016,7 @@ function PanelApp({
|
|
|
2994
3016
|
fingerprint
|
|
2995
3017
|
}) {
|
|
2996
3018
|
const themes = useMemo5(() => offeredThemes(exactThemes, extraThemes), [exactThemes, extraThemes]);
|
|
3019
|
+
const gateReading = useMemo5(() => gateStatus(gate, paneltirBuild), [gate]);
|
|
2997
3020
|
const [themeName, setThemeName] = useState10(
|
|
2998
3021
|
() => resolveTheme(
|
|
2999
3022
|
initialState.preferences?.theme,
|
|
@@ -3065,9 +3088,39 @@ function PanelApp({
|
|
|
3065
3088
|
variable: "PANEL_REPO",
|
|
3066
3089
|
enables: ui.needRepoEnables,
|
|
3067
3090
|
fix: /* @__PURE__ */ jsx20("p", { children: ui.fixRepo })
|
|
3068
|
-
}
|
|
3091
|
+
},
|
|
3092
|
+
// The gate is the only requirement here that is not an environment
|
|
3093
|
+
// variable: it is files this project owns, and the only one whose
|
|
3094
|
+
// failure is silent. A stale `api/panel-state.ts` does not read as
|
|
3095
|
+
// missing configuration — it reads as the panel being broken, which is
|
|
3096
|
+
// how the HTTP 500 cost a week.
|
|
3097
|
+
//
|
|
3098
|
+
// Drawn only when a stamp was passed. The kit does not invent a
|
|
3099
|
+
// requirement out of the absence of a prop: this project's own panel has
|
|
3100
|
+
// no copied gate — `template/` is the original, held honest by
|
|
3101
|
+
// `test:auth` — so a row here would invite wiring something that cannot
|
|
3102
|
+
// exist. A caller who passes nothing still has `paneltir doctor`, which
|
|
3103
|
+
// is what the skill file sends their Claude to.
|
|
3104
|
+
//
|
|
3105
|
+
// Passed but unreadable is a different thing and *is* reported, as
|
|
3106
|
+
// `unknown` rather than `missing`: telling somebody their gate is old on
|
|
3107
|
+
// the strength of a file we failed to parse is the mistake
|
|
3108
|
+
// `SetupWizard` already refuses to make.
|
|
3109
|
+
...gate === void 0 ? [] : [{
|
|
3110
|
+
id: "gate",
|
|
3111
|
+
label: ui.needGate,
|
|
3112
|
+
status: gateReading.verdict === "current" ? "ok" : gateReading.verdict === "behind" ? "missing" : "unknown",
|
|
3113
|
+
enables: ui.needGateEnables,
|
|
3114
|
+
fix: /* @__PURE__ */ jsxs18(Fragment5, { children: [
|
|
3115
|
+
/* @__PURE__ */ jsxs18("p", { children: [
|
|
3116
|
+
ui.fixGate,
|
|
3117
|
+
gateReading.copiedFrom ? ` (v${gateReading.copiedFrom} \u2192 v${gateReading.installed})` : ""
|
|
3118
|
+
] }),
|
|
3119
|
+
/* @__PURE__ */ jsx20("pre", { className: "pt-copy__pre", children: "npx paneltir doctor" })
|
|
3120
|
+
] })
|
|
3121
|
+
}]
|
|
3069
3122
|
];
|
|
3070
|
-
}, [setup, ui]);
|
|
3123
|
+
}, [setup, ui, gate, gateReading]);
|
|
3071
3124
|
useEffect9(() => {
|
|
3072
3125
|
document.documentElement.lang = lang;
|
|
3073
3126
|
if (showLanguage) storeLang(lang);
|
|
@@ -4236,6 +4289,7 @@ export {
|
|
|
4236
4289
|
OWNERS,
|
|
4237
4290
|
PANELTIR_FILE_COUNT,
|
|
4238
4291
|
PANELTIR_FINGERPRINT,
|
|
4292
|
+
PANELTIR_TEMPLATE_HASH,
|
|
4239
4293
|
PANELTIR_VERSION,
|
|
4240
4294
|
PRESET_PANEL_THEMES,
|
|
4241
4295
|
PanelApp,
|
|
@@ -4257,6 +4311,7 @@ export {
|
|
|
4257
4311
|
decided,
|
|
4258
4312
|
emptyCard,
|
|
4259
4313
|
explainBoard,
|
|
4314
|
+
gateStatus,
|
|
4260
4315
|
isCard,
|
|
4261
4316
|
isFirstRun,
|
|
4262
4317
|
ledgerForm,
|
package/fingerprint.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paneltir",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"hash": "sha256:
|
|
5
|
-
"fileCount":
|
|
6
|
-
"
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"hash": "sha256:4de7ef7076563e4c3118ac16b645737a44c4c0675115d9a9d7bbff562d5ecb74",
|
|
5
|
+
"fileCount": 163,
|
|
6
|
+
"templateHash": "sha256:7f9accd11920a0c189dd683bdb96635a606960afeca71e02c2a26a0257acefd8",
|
|
7
|
+
"generatedAt": "2026-09-11T02:49:51.562Z"
|
|
7
8
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paneltir",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "A React UI kit for admin panels: page frame, a board with touch-ready drag and drop, detail sheets, and a whole panel in one component. Structure and behaviour are fixed; colour and brand belong to each project.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": "DFKlabs (https://DFKlabs.com)",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"check:boards": "node scripts/check-boards.mjs",
|
|
65
65
|
"demo:install": "npm --prefix examples/demo ci",
|
|
66
66
|
"demo:build": "npm --prefix examples/demo run build",
|
|
67
|
-
"verify": "npm run typecheck && npm run build && npm run icons:check && npm run seo:check && npm run test:auth && npm run test:marketplace && npm run test:init && npm run test:board && npm run test:move && npm run test:theme && npm run test:setup && npm run test:updates && npm run release-notes:check && npm run check:boards && npm run check:docs && npm run check:package && npm run fingerprint:check",
|
|
67
|
+
"verify": "npm run typecheck && npm run build && npm run icons:check && npm run seo:check && npm run test:auth && npm run test:marketplace && npm run test:init && npm run test:board && npm run test:gate && npm run test:move && npm run test:theme && npm run test:setup && npm run test:updates && npm run release-notes:check && npm run check:boards && npm run check:docs && npm run check:package && npm run fingerprint:check",
|
|
68
68
|
"site:install": "npm --prefix site ci",
|
|
69
69
|
"site:dev": "npm --prefix site run dev",
|
|
70
70
|
"site:build": "npm --prefix site run build",
|
|
@@ -78,7 +78,8 @@
|
|
|
78
78
|
"test:board": "node scripts/test-board.mjs",
|
|
79
79
|
"test:move": "node scripts/test-move.mjs",
|
|
80
80
|
"test:theme": "node scripts/test-theme.mjs",
|
|
81
|
-
"test:setup": "node scripts/test-setup.mjs"
|
|
81
|
+
"test:setup": "node scripts/test-setup.mjs",
|
|
82
|
+
"test:gate": "node scripts/test-gate.mjs"
|
|
82
83
|
},
|
|
83
84
|
"peerDependencies": {
|
|
84
85
|
"react": ">=18",
|
|
@@ -98,35 +99,42 @@
|
|
|
98
99
|
"node": ">=20"
|
|
99
100
|
},
|
|
100
101
|
"paneltirRelease": {
|
|
101
|
-
"version": "0.
|
|
102
|
-
"date": "2026-09-
|
|
102
|
+
"version": "0.10.0",
|
|
103
|
+
"date": "2026-09-11",
|
|
103
104
|
"entries": [
|
|
104
105
|
{
|
|
105
106
|
"kind": "feature",
|
|
106
107
|
"text": {
|
|
107
|
-
"en": "
|
|
108
|
-
"es": "
|
|
108
|
+
"en": "`paneltir doctor` says whether the files this project copied are still the ones the installed kit ships — and separates \"you edited it\" from \"we changed it underneath you\", which a diff cannot.",
|
|
109
|
+
"es": "`paneltir doctor` dice si los archivos que copió este proyecto siguen siendo los del kit instalado — y separa «lo editaste tú» de «lo cambiamos nosotros debajo», que un diff no puede."
|
|
109
110
|
}
|
|
110
111
|
},
|
|
111
112
|
{
|
|
112
|
-
"kind": "
|
|
113
|
+
"kind": "feature",
|
|
114
|
+
"text": {
|
|
115
|
+
"en": "The setup list can carry a row for the copied gate. Import `.paneltir/gate.json` and pass it as `gate`; without it the panel says nothing about those files rather than guessing.",
|
|
116
|
+
"es": "La lista de configuración puede llevar una fila para la puerta copiada. Importa `.paneltir/gate.json` y pásalo como `gate`; sin él el panel no dice nada de esos archivos en vez de suponer."
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"kind": "feature",
|
|
113
121
|
"text": {
|
|
114
|
-
"en": "
|
|
115
|
-
"es": "
|
|
122
|
+
"en": "`paneltir run \"what this pass did\"` appends to the board's revisions, so the record does not depend on anyone remembering at the end of a session.",
|
|
123
|
+
"es": "`paneltir run \"qué hizo esta pasada\"` añade una entrada a las revisiones del tablero, así el registro no depende de que alguien se acuerde al final de la sesión."
|
|
116
124
|
}
|
|
117
125
|
},
|
|
118
126
|
{
|
|
119
127
|
"kind": "fix",
|
|
120
128
|
"text": {
|
|
121
|
-
"en": "
|
|
122
|
-
"es": "
|
|
129
|
+
"en": "`paneltir init` and `paneltir doctor` no longer need React installed. They loaded the component bundle at startup, so they failed in a project that had not installed it yet — which is exactly when `init` is run.",
|
|
130
|
+
"es": "`paneltir init` y `paneltir doctor` ya no necesitan React instalado. Cargaban el bundle de componentes al arrancar, así que fallaban en un proyecto que aún no lo tenía — que es justo cuando se ejecuta `init`."
|
|
123
131
|
}
|
|
124
132
|
},
|
|
125
133
|
{
|
|
126
134
|
"kind": "chore",
|
|
127
135
|
"text": {
|
|
128
|
-
"en": "The
|
|
129
|
-
"es": "
|
|
136
|
+
"en": "The rules a project needs are checked to actually reach it: the documents a project keeps must name `extraThemes` and `paneltir doctor`, or the build fails saying why.",
|
|
137
|
+
"es": "Se comprueba que las reglas que un proyecto necesita le lleguen de verdad: los documentos que el proyecto conserva tienen que nombrar `extraThemes` y `paneltir doctor`, o la build falla diciendo por qué."
|
|
130
138
|
}
|
|
131
139
|
}
|
|
132
140
|
]
|
|
@@ -132,6 +132,35 @@ Never invent a palette. If the project has no colours of its own, say so and
|
|
|
132
132
|
leave the theme alone — the kit ships presets for exactly that case, and one
|
|
133
133
|
of them is a light theme.
|
|
134
134
|
|
|
135
|
+
And when you hand this project's own theme to `PanelApp`, it goes in
|
|
136
|
+
**`extraThemes`** and never in `themes`. `extraThemes` offers it as well as the
|
|
137
|
+
kit's four; `themes` means "exactly these", so one theme passed there deletes
|
|
138
|
+
every preset and the settings sheet shows a single row. Two projects were
|
|
139
|
+
installed that way before the distinction existed, and the symptom reads as the
|
|
140
|
+
kit having no themes rather than as a prop being wrong.
|
|
141
|
+
|
|
142
|
+
## When the panel behaves oddly for no visible reason
|
|
143
|
+
|
|
144
|
+
The gate — `middleware.ts`, `lib/session.ts`, `api/*.ts` — was copied into this
|
|
145
|
+
project once and does **not** update when the kit does. So a fix released
|
|
146
|
+
upstream can be missing here while every version number on the screen looks
|
|
147
|
+
current, and nothing says so: the `HTTP 500` on saving lived in
|
|
148
|
+
`api/panel-state.ts` and stayed in projects installed before the fix.
|
|
149
|
+
|
|
150
|
+
npx paneltir doctor
|
|
151
|
+
|
|
152
|
+
`.paneltir/gate.json` is what makes that answer sharp, and it belongs in git:
|
|
153
|
+
it records what `init` copied and from where. Ignored, it vanishes on the next
|
|
154
|
+
clone and every file degrades to "differs" — the one answer that cannot say
|
|
155
|
+
whether you moved it or we did.
|
|
156
|
+
|
|
157
|
+
It separates the three answers that need different actions: *yours* (you edited
|
|
158
|
+
it), *BEHIND* (you did not touch it and the template moved), and *BOTH*. The
|
|
159
|
+
middle one is invisible to `git diff`, because there is nothing in this
|
|
160
|
+
project's history to compare against. `paneltir init --force` replaces the gate
|
|
161
|
+
and never the board — read the diff first when a file is marked BOTH, or the
|
|
162
|
+
edit is lost.
|
|
163
|
+
|
|
135
164
|
## Setting the panel up in the first place
|
|
136
165
|
|
|
137
166
|
The panel is asked for this by name: its walkthrough carries a line the owner
|